SSH-accessible terminal portfolio in Go —
ssh r-that.comand you're in a TUI, no browser required.
Live demo: ssh r-that.com (or clone and go run .)
Stack: Go · Bubble Tea (TUI) · Lip Gloss (styling) · Wish (SSH server) · go:embed
Status: Active
The entire portfolio is one Go binary. Content lives in a single data.json that's embedded into the compiled executable at build time via Go's embed package — no filesystem access at runtime, no config deploys, no CDN. The SSH server (Wish) lets anyone view the portfolio without needing a browser or installing anything; they just ssh in and get an MVU-architected TUI. Deploy is a single binary push. Tradeoff: data.json updates require a rebuild, but that's the same lifecycle as any compiled application — and it means content integrity matches binary version exactly.
(formerly Terminal Portfolio)
An interactive, SSH-accessible terminal portfolio built with Go and the Charm Bracelet ecosystem. It renders a full TUI (terminal user interface) with multiple navigable views, styled layouts, and keyboard-driven navigation. Connect via SSH or run the binary directly to browse through About, Skills, Projects, Experience, Education, and Contact sections.
This project uses the MVU (Model-View-Update) architecture provided by Bubble Tea. All portfolio content is defined in a single data.json file, which gets embedded into the compiled binary at build time using Go's embed package. The SSH server is powered by Wish, allowing anyone to view the portfolio by connecting to your server without needing a browser.
Model (state) --> View (render) --> Update (handle input) --> Model
- SSH server: Serve the portfolio over SSH on port 2222 using Wish
- Rich TUI design: Styled borders, colors, and layouts using Lip Gloss
- Keyboard navigation: Vim-style controls (h/j/k/l) and arrow keys
- Six views: About, Skills, Projects, Experience, Education, Contact
- Responsive layout: Adapts to terminal resize events, with separate layouts for wide and narrow terminals
- Loading animation: Spinner on startup before rendering content
- Data-driven content: All portfolio data loaded from a single JSON file
- Cross-platform binaries: GitHub Actions workflow cross-compiles for Windows, macOS, and Linux (amd64 + arm64)
| Component | Technology |
|---|---|
| Language | Go 1.25 |
| TUI Framework | Bubble Tea |
| Styling | Lip Gloss |
| Components | Bubbles (viewport, spinner) |
| SSH Server | Wish |
| Logging | Charm Log |
| Rate Limiting | golang.org/x/time |
| CI/CD | GitHub Actions (cross-compile on tag push) |
- Go 1.21+
- A terminal with color and Unicode support
git clone https://github.com/RogerSquare/cairn.git
cd cairn
go mod download
go build -o cairn
./cairnThe SSH server starts on port 2222. Connect from another terminal:
ssh localhost -p 2222| Key | Action |
|---|---|
q / Ctrl+C |
Quit |
] / Right Arrow |
Next view |
[ / Left Arrow |
Previous view |
+ / = |
Jump to Home (About) |
k / Up Arrow |
Scroll up |
j / Down Arrow |
Scroll down |
Edit data.json in the project root to change the portfolio content:
{
"Contact": {
"Name": "Your Name",
"Title": "Your Title",
"Email": "you@email.com"
},
"About": "Your bio here",
"Skills": [],
"Projects": [],
"Experience": [],
"Education": []
}The file is embedded into the binary at compile time, so rebuild after making changes.
The live site at r-that.com runs the TypeScript/Ink version in the ts/ directory on a Hostinger VPS — not the Go binary above. This section is specifically for pushing updates to that live deployment.
| Thing | Value |
|---|---|
| Repo path on VPS | /opt/portfolio (run from /opt/portfolio/ts) |
| Mutable data dir | /var/lib/cairn (via CAIRN_DATA_DIR env var) |
| SSH portfolio service | portfolio.service (port 22) |
| Web server service | portfolio-web.service (listens on :3000, behind nginx) |
| Reverse proxy | nginx — r-that.com -> localhost:3000, wiki.r-that.com -> /var/www/wiki |
| Admin SSH | ssh -p 2200 <user>@r-that.com |
| DNS | Cloudflare A records -> VPS IP, proxy enabled (TLS termination at Cloudflare) |
Runtime-mutable content (data.json, blog posts) lives outside the repo at $CAIRN_DATA_DIR. That way admin CMS edits survive git pull. The repo only contains seed templates (ts/data.example.json, ts/posts.example/) which are copied on first boot if the target is empty.
After committing and pushing locally:
ssh -p 2200 <user>@r-that.com
cd /opt/portfolio && git pull
sudo systemctl restart portfolio.service portfolio-web.serviceThree commands, done.
Content edits (bio, skills, projects, blog posts): done through the admin UI at https://r-that.com/admin. Writes go to $CAIRN_DATA_DIR, not the repo — no deploy needed, changes are live immediately.
Seeding new content from the repo: if you want to push a new blog post through git instead of the admin UI, add the file to ts/posts.example/ and commit. It only gets copied to the live posts dir on first boot into an empty directory — it won't overwrite or merge with existing posts. For ongoing content, use the admin UI.
Changed ts/src/*.ts / .tsx: same 3 commands. Both services run via tsx (no compile step).
Changed ts/package.json (added a dependency): extra step before restarting:
cd /opt/portfolio/ts && npm install
sudo systemctl restart portfolio.service portfolio-web.serviceTail live logs:
sudo journalctl -u portfolio.service -f
sudo journalctl -u portfolio-web.service -fCheck service status:
systemctl status portfolio.service portfolio-web.serviceIf a deploy breaks the live site:
cd /opt/portfolio
git log --oneline -5 # find the last good commit
git reset --hard <good-commit-sha>
sudo systemctl restart portfolio.service portfolio-web.serviceIf ssh r-that.com itself is broken, you can still reach the box on the admin port (ssh -p 2200) — that's the whole point of splitting them.
If you ever set up a fresh VPS, do this once to move mutable data out of the repo:
sudo mkdir -p /var/lib/cairn
sudo mv /opt/portfolio/ts/data.json /var/lib/cairn/
sudo mv /opt/portfolio/ts/posts /var/lib/cairn/
sudo chown -R <service-user>:<service-user> /var/lib/cairnThen add the env var to both systemd units (systemctl edit portfolio.service and systemctl edit portfolio-web.service):
[Service]
Environment="CAIRN_DATA_DIR=/var/lib/cairn"systemctl daemon-reload && systemctl restart portfolio.service portfolio-web.service.
A GitHub Actions workflow (.github/workflows/release.yml) triggers on tag pushes. It cross-compiles the application for six targets:
- Windows (amd64, arm64)
- macOS (amd64, arm64)
- Linux (amd64, arm64)
Create a release by pushing a tag:
git tag v1.0.0
git push --tagsCompiled binaries are attached to the GitHub release automatically.
- Bubble Tea -- Terminal UI framework (MVU pattern)
- Lip Gloss -- Declarative terminal styling
- Bubbles -- Pre-built TUI components (viewport, spinner)
- Wish -- SSH server for Bubble Tea applications
- Charm Log -- Structured logging
MIT