SSH-based interactive portfolio application served over SSH, built with Go, Wish, and Bubble Tea.
This project runs a terminal user interface over SSH so visitors can browse a personal portfolio without a browser. The application includes menu-driven sections for about, projects, education, contact, privacy controls, and a live RSS feed view.
- SSH server using Wish and Bubble Tea.
- Keyboard-driven TUI with themed styling and animated logo.
- Privacy page that lets a visitor opt in or out of IP-based visit tracking.
- SQLite-backed unique visitor counter with opt-out persistence.
- RSS feed page that fetches and caches posts from
https://note.toshiki.dev/feed.xml.
upanddownorjandk: move selection.enterorspace: open selected page or confirm choice.escorbackspace: return to menu.t: cycle theme.qorctrl+c: quit from menu.
Connect directly to the live instance:
ssh ssh.toshiki.devNot strictly required, but helpful for fast onboarding and release troubleshooting.
flowchart LR
client["SSH client"] --> server["Wish SSH server main go"]
server --> config["config loader"]
server --> counter["visitor counter store"]
server --> tui["Bubble Tea app model"]
tui --> pages["page renderers"]
tui --> view["theme and logo styles"]
tui --> feed["rss fetch and cache"]
feed --> rss["note toshiki feed xml"]
counter --> sqlite["sqlite visitors db"]
- Go
1.24.2or compatible Go1.24.x. ssh-keygenavailable inPATHfor host key generation.
- Local development is easiest on a non-privileged port such as
2222. - If you bind to port
22, elevated privileges or a free privileged port may be required.
If you want to run on a privileged port such as 22 without running the app as root, grant the built binary bind capability:
sudo setcap 'cap_net_bind_service=+ep' ./bin/termfolioUse the example file for local development:
cp config.yaml.example config.local.yamlGenerate a host key before first run:
make keysIf the configured key file is missing, the app can also prompt to generate one during startup.
Run with an explicit local config:
go run . -c config.local.yamlOr run the default command:
make runConnect from another terminal:
ssh -p 2222 localhostmake run: run application withgo run ..make build: build binary tobin/termfolio.make build-linux: build Linux binary tobin/termfolio-linux2.make keys: create.ssh/host_ed25519.make fmt: rungo fmt ./....make clean: removebin/.
Default configuration keys:
ssh:
port: 2222
address: "0.0.0.0"
hostKeyPath: ".ssh/host_ed25519"
counter:
enabled: true
dbPath: "data/visitors.db"
stats:
enabled: false
geoLiteDbPath: "data/GeoLite2-Country.mmdb"The counter section supports either:
- Mapping form with
enabledand optionaldbPath. - Scalar boolean form such as
counter: false.
These variables override file values:
SSH_PORTSSH_ADDRESSSSH_HOST_KEY_PATH
- Visitor count tracks unique IPs in SQLite.
- Opted-out IPs are stored in a dedicated table and removed from counted visitors.
- If tracking is disabled, the app still displays the current count without recording new visits.
- Optional
statsblock can show privacy-page stats when enabled. - Country stats read from
stats.geoLiteDbPathand report top 5 countries by unique visitors.
- Multi-stage build compiles a static Linux binary.
- Runtime image is Alpine and includes
openssh-keygen. - Entry point generates host key when missing, then starts the app.
Build and run example:
docker build -t termfolio .
docker run --rm -p 2222:2222 termfolioconfig/ configuration loading and defaults
counter/ SQLite visitor tracking store
pages/ TUI page renderers and content models
ui/ Bubble Tea app model and update loop
view/ theme palette and shared view helpers
main.go SSH server bootstrap and middleware wiring
entrypoint.sh container startup and host key bootstrap
Version constants are defined in version/version.go.
At this snapshot:
- App name:
termfolio - Version:
0.2.0
Print version:
go run . -vThis project is licensed under the MIT License. See licnese for the full text.
