Lingon is a secure, multi-client terminal relay with a CLI, Web UI, and Android app. It is built around four personas:
- Interactive host (default CLI): runs a real terminal session and streams it to the relay.
- Attach: connects to a session (TUI or programmatic SDK) to view/control it.
- Serve: runs the relay server (HTTPS + WSS + Web UI).
- Web UI: browser-based client served by the relay.
The host/attach terminal UI paths use a full-framebuffer MVU runtime
(internal/mvu) as the canonical renderer and state-transition model.
lingon bootstrapThis creates a default config at ~/.lingon/config.yaml and a TLS directory at
~/.lingon/tls. Paths inside the config are relative by default (resolved
against the config file location). Use -c/--config to write elsewhere, --set
to override values, --force (or -f) to overwrite, and
--regenerate-certificates to rotate TLS assets.
Examples:
lingon bootstrap -c /app/config/config.yaml -s server.base=/lingon -s server.tls.dir=/app/certs
lingon bootstrap -f -c /app/config/config.yaml -s server.base=/lingon
lingon bootstrap -c /app/config/config.yaml --regenerate-certificates
lingon bootstrap -s server.tls.hostname=lingon.my.domain.tld -s server.listen=:8443The last example generates a CA and server cert for lingon.my.domain.tld
(CN/SAN) and listens on all interfaces at port 8443. Typically you want to
change the listen address to something other than 127.0.0.1:... to serve over
the network.
lingon users add aliceBy default Lingon generates a password and prints it along with the TOTP seed.
Use --prompt if you want to enter a custom password.
lingon serveBy default this listens on 127.0.0.1:12843 with base path /v1.
lingon login -e https://localhost:12843/v1Tokens are stored at ~/.lingon/auth.json by default.
The file stores auth per normalized endpoint key, so switching -e/--endpoint
reuses the matching cached refresh/access tokens.
Log out from an endpoint (remote revoke + local token removal):
lingon logout -e https://localhost:12843/v1lingon -e https://localhost:12843/v1 -s my-session
# Or more commonly: use config and no -s (will generate a session ID)
lingonLingon uses prefix keystroke Ctrl+l as command introducer, see Ctrl+l h (ctrl+l followed by h) for help.
lingon attach -e https://localhost:12843/v1 my-sessionhttps://localhost:12843/v1/
The Web UI is served by the relay at the base path.
Lingon CLI host/attach rendering follows a single MVU pipeline:
- Model: typed runtime state (
mvu.State) for tabs, overlays, connectivity, help, wall, scrollback status. - Update: typed actions via
Runtime.ApplyAction(...). - View: full-framebuffer composition + delta emission from composed snapshots.
- Effects: overlay/tab timers scheduled through
mvu.EffectScheduler.
This replaced the previous hybrid/non-framebuffer compositor implementation.
internal/compositor has been removed; new TUI behavior should integrate
through internal/mvu.
Run a terminal host that becomes a shared session:
lingon -e https://relay.example.com/v1 -s prod-shellInteractive host options (selection):
-e, --endpointrelay endpoint (https/wss base URL; assumeshttps://if omitted)-s, --sessionsession id--tokenaccess token (overrides stored auth)--auth-filepath to auth file--shelloverride login shell path--cols,--rowsterminal size--scrollback-linesscrollback lines--termTERM value for the PTY-r, --respawnrespawn the shell on exit-o, --offlinestart offline; toggle relay publishing withCtrl+l o--hostname-onlyshow hostname-only connect/disconnect banners--themeTUI chrome theme (lingon themes)-k, --insecureskip TLS verification--log-fileclient log file path (empty disables)--trace,--trace-filewrite a JSONL trace
Run a detached local PTY host (no local interactive host TUI), accessible through local unix-socket attach/send/sessions commands:
lingon -x -s local-shellYou can also invoke the same mode by binary name:
lingonx -s local-shell(lingonx includes symlink invocations where argv[0] basename is lingonx.)
Headless local mode notes:
-x, --headlessswitches root/attach/send/sessions to local headless mode.- Metadata and sockets are stored under
~/.lingon/headless/. lingon attach -xworks even when relay publishing is offline/disconnected.- In
attach -x,Ctrl+l ois local-host control and is allowed.
Attach to a session over a TUI client:
lingon attach -e https://relay.example.com/v1 prod-shellAttach to a share token:
lingon attach --token <token>Attach options (selection):
-e, --endpointrelay endpoint (https/wss base URL; assumeshttps://if omitted)-x, --headlessattach via local headless unix socket sessions-t, --tokenshare token for anonymous attach--access-tokenaccess token for authenticated attach--request-controlrequest controller lease on connect--pickinteractively pick a session--hostname-onlyshow hostname-only connect/disconnect banners--themeTUI chrome theme (lingon themes)-k, --insecureskip TLS verification--auth-filepath to auth file--log-fileclient log file path (empty disables)--trace,--trace-filewrite a JSONL trace
Run the relay server:
lingon serveCommon flags:
--listenlisten address (default127.0.0.1:12843)--basebase path (default/v1)--data-dirdata directory--users-fileuser store--connect-limit-count,--connect-limit-burst,--connect-limit-window,--connect-limit-disableglobal connection rate limiting--tls-modeauto|bundle|acme--tls-dirlocal TLS directory (auto mode)--tls-bundlePEM bundle file (bundle mode)--tls-hostnamehostname for ACME or server cert--tls-cache-dirACME cache directory--wall-timeout,--wall-inactive-afterwall banner behavior-n, --no-bannerremove login-page branding from served Web UI assets
Reverse proxy note: WebSocket endpoints (.../ws/...) must be proxied to
the relay over HTTP/1.1. If your reverse proxy negotiates HTTP/2 to the
upstream, force HTTP/1.1 for the WebSocket routes (or disable h2 to the relay).
The Web UI is served by the relay server at the base path. Login uses the same
user credentials as the CLI. You can also attach directly from the login form
using a share token (token mode disables username/password/TOTP fields). The
existing URL flow still works via ?token=<share-token>. Session switching,
sharing, resize control, and themes are all accessible from the menu.
The Android app provides a native client with session switching, sharing, cert management, and UI themes.
Build and run from the root:
make android-sdk
make android-avd PRESET=medium
make android-emulator
make android-build
make android-installOr, directly from android/:
make -C android sdk
make -C android avd PRESET=medium
make -C android emulator
make -C android build
make -C android installThe Android Makefile includes targets for release builds, AVD setup, and
integration tests.
- The CLI no-flicker/tab-overlay invariants are enforced by integration and
architecture tests in
internal/session,internal/attach, andinternal/mvu. - Web UI and Android do not use the
internal/mvurenderer directly; they use the same relay/session protocol but maintain independent UI renderers. Minor visual/timing differences across clients can still occur. - In host mode, switching between local and remote tabs intentionally resets render baseline in some paths (to avoid composing against incompatible prior snapshots), so a full repaint on those transitions is expected.
Lingon supports three server TLS modes:
auto: generates and stores a local CA and server cert intls.dir.bundle: loads a PEM bundle (cert + key) fromtls.bundle.acme: uses ACME (TLS-ALPN-01) withtls.hostnameandtls.cache_dir.
When server.tls.mode (or serve --tls-mode) is set to acme, Lingon uses
ACME TLS-ALPN-01 to obtain and renew certificates automatically. This requires:
server.tls.hostname/--tls-hostname(public DNS name to request certs for).server.tls.cache_dir/--tls-cache-dir(directory for cert/issuer cache).
The relay must be reachable at the configured hostname over TLS for ACME to
validate the domain (typically on port 443). Certificates are cached under
tls.cache_dir and renewed automatically by the ACME manager.
If you use auto mode, export the CA and trust it on clients:
lingon tls export -o ./lingon-ca.pemThen import the CA into your OS or browser trust store. This is required for
Web UI and Android unless you use --insecure.
You can skip TLS verification with -k/--insecure on client commands. This is
useful for quick local testing only.
server:
listen: 127.0.0.1:12843
base: /v1
data_dir: .
users_file: users.json
tls:
mode: auto
dir: tls
cache_dir: tls/cache
client:
endpoint: https://localhost:12843/v1
auth_file: auth.json
terminal:
term: xterm-256color
scrollback_lines: 5000Relative paths are resolved relative to the config file. ~ is not expanded in
config values; use absolute paths or relative paths instead.
lingon bootstrap- create config + TLS assetslingon serve- run relay serverlingon login- authenticate and cache tokenslingon- start an interactive host session (default command)lingon -x/lingonx- start a detached local headless PTY host sessionlingon attach- attach to a sessionlingon attach -x- attach to a local headless PTY sessionlingon send- send input to a sessionlingon send -x- send input to a local headless PTY sessionlingon users- manage userslingon sessions- list sessionslingon sessions -x- list local headless PTY sessionslingon detach <session-id>- force-stop a local headless PTY sessionlingon share- create/revoke share tokenslingon tls- manage TLS assetslingon completion- generate shell completion scriptslingon version- print version/build metadata
Run lingon <command> --help for details.
Top-level Makefile targets:
make build- buildbin/lingonmake test- run unit testsmake test-webui- run webui-tagged testsmake test-android- run Android integration testsmake lint- go vet + golint + golangci-lintmake release- build binary + Android APK + zip bundle
The SDK lives in the lingon Go package and mirrors CLI functionality.
See doc.go for examples.
Lingon is not Klingon. The name comes from the idea that the terminal is a place where humans talk to machines, using a shared lingo the machine understands. Lingon makes that lingo usable across n platforms and n simultaneous clients—whether they act as viewers or controllers.


