Finalize Go-only migration, runtime hardening, and CI/container optimization
This commit consolidates the full repository transition to a Go-first codebase and captures the follow-up performance/reliability work completed in the same stream. Highlights: - Remove Python implementation and test suites (, , , ) and retire Python-specific docs/instructions. - Move and standardize static web assets under , updating Bun/TypeScript build paths and server static resolution logic. - Rewrite developer workflow to Makefile-first Go targets (vet/test/race/coverage/fuzz/build) and align repository guidance/docs accordingly. - Update Docker and CI/CD for leaner artifacts: - switch to Alpine-based multi-stage build with stripped Go binary - install only minimal runtime deps (, ) - tighten Docker build context via - ensure workflows build/publish the target. - Improve runtime correctness/latency and reduce duplication: - explicit WebSocket outbound frame typing (text vs binary) instead of payload-byte heuristics - SSE activity fan-out outside global lock and safer subscriber lifecycle - shared session output/snapshot helpers to reduce duplicated logic - restart-safe channel lifecycle for Docker watcher/stats start-stop-start flows - faster screenshot cold-start path (poll-until-ready within timeout vs fixed sleep). - Add/expand regression coverage for the above lifecycle and helper paths. Validation run: - bun run build [32mBundled 3 modules in 10ms[0m [34mterminal.js[33m 0.68 MB [2m(entry point)[0m (Bun typecheck + bundle) - cd go && go vet ./... cd go && go test ./... ok github.com/rcarmo/webterm-go-port/cmd/webterm (cached) ok github.com/rcarmo/webterm-go-port/internal/terminalstate (cached) ok github.com/rcarmo/webterm-go-port/webterm (cached) cd go && go test ./webterm -coverprofile=coverage.out && go tool cover -func=coverage.out ok github.com/rcarmo/webterm-go-port/webterm (cached) coverage: 81.0% of statements github.com/rcarmo/webterm-go-port/webterm/cli.go:14: RunCLI 51.6% github.com/rcarmo/webterm-go-port/webterm/config.go:25: DefaultConfig 100.0% github.com/rcarmo/webterm-go-port/webterm/config.go:29: LoadLandingYAML 82.6% github.com/rcarmo/webterm-go-port/webterm/config.go:70: LoadComposeManifest 76.9% github.com/rcarmo/webterm-go-port/webterm/config.go:114: extractLabel 92.3% github.com/rcarmo/webterm-go-port/webterm/config.go:138: asString 80.0% github.com/rcarmo/webterm-go-port/webterm/constants.go:27: EnvBool 50.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:30: Read 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:56: NewDockerExecSession 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:72: Open 90.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:99: Start 85.7% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:119: readLoop 83.3% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:143: handleOutput 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:153: createExec 75.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:183: startExecSocket 60.7% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:219: resizeExec 83.3% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:237: Close 90.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:250: Wait 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:257: SetTerminalSize 81.8% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:274: ForceRedraw 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:281: SendBytes 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:294: SendMeta 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:298: IsRunning 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:304: GetReplayBuffer 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:308: GetScreenSnapshot 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_exec_session.go:316: UpdateConnector 80.0% github.com/rcarmo/webterm-go-port/webterm/docker_http.go:23: DockerSocketPath 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_http.go:37: newUnixHTTPClient 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_http.go:47: sharedUnixClient 91.7% github.com/rcarmo/webterm-go-port/webterm/docker_http.go:64: unixJSONRequest 84.2% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:33: NewDockerStatsCollector 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:47: Available 72.7% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:64: Start 80.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:78: Stop 75.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:90: AddService 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:101: RemoveService 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:115: GetCPUHistory 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:124: pollLoop 95.2% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:160: discoverContainers 65.4% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:198: pollContainer 77.8% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:223: calculateCPUPercent 69.2% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:260: RenderSparklineSVG 89.3% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:299: max 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:306: toAnyMap 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:323: toStringMap 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:331: toAnySlice 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:340: toStringSlice 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:351: toUint 91.7% github.com/rcarmo/webterm-go-port/webterm/docker_stats.go:376: toInt 85.7% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:34: NewDockerWatcher 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:54: hasWebtermLabel 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:60: isAutoLabel 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:67: getContainerCommand 80.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:76: getContainerTheme 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:81: getContainerName 42.9% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:93: containerToSlug 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:98: addContainer 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:119: removeContainer 100.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:143: listLabeledContainers 94.1% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:168: handleEvent 80.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:202: watchEvents 85.7% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:239: ScanExisting 60.0% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:249: Start 83.3% github.com/rcarmo/webterm-go-port/webterm/docker_watcher.go:265: Stop 81.8% github.com/rcarmo/webterm-go-port/webterm/identity.go:12: GenerateID 94.1% github.com/rcarmo/webterm-go-port/webterm/normalize.go:13: FilterDASequences 83.3% github.com/rcarmo/webterm-go-port/webterm/replay.go:14: NewReplayBuffer 66.7% github.com/rcarmo/webterm-go-port/webterm/replay.go:21: Add 100.0% github.com/rcarmo/webterm-go-port/webterm/replay.go:43: Bytes 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:95: OnData 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go💯 OnBinary 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:105: OnMeta 0.0% github.com/rcarmo/webterm-go-port/webterm/server.go:107: OnClose 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:112: NewLocalServer 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:163: findStaticPath 62.5% github.com/rcarmo/webterm-go-port/webterm/server.go:184: markRouteActivity 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:207: enqueueWSFrame 77.8% github.com/rcarmo/webterm-go-port/webterm/server.go:233: stopWSClient 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:246: wsSender 80.0% github.com/rcarmo/webterm-go-port/webterm/server.go:256: createTerminalSession 66.7% github.com/rcarmo/webterm-go-port/webterm/server.go:278: clampInt 60.0% github.com/rcarmo/webterm-go-port/webterm/server.go:288: parseResizePayload 88.9% github.com/rcarmo/webterm-go-port/webterm/server.go:303: handleWebSocket 81.1% github.com/rcarmo/webterm-go-port/webterm/server.go:425: chooseRouteForScreenshot 50.0% github.com/rcarmo/webterm-go-port/webterm/server.go:440: screenshotTTL 66.7% github.com/rcarmo/webterm-go-port/webterm/server.go:457: handleScreenshot 55.7% github.com/rcarmo/webterm-go-port/webterm/server.go:541: handleCPUSparkline 94.4% github.com/rcarmo/webterm-go-port/webterm/server.go:566: handleEvents 76.0% github.com/rcarmo/webterm-go-port/webterm/server.go:602: toIntFromQuery 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:609: dashboardTiles 81.8% github.com/rcarmo/webterm-go-port/webterm/server.go:631: handleTiles 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:636: getWSURL 65.2% github.com/rcarmo/webterm-go-port/webterm/server.go:671: handleRoot 56.8% github.com/rcarmo/webterm-go-port/webterm/server.go:732: htmlEscape 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:736: htmlAttrEscape 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:740: handleHealth 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:744: setupDockerFeatures 40.0% github.com/rcarmo/webterm-go-port/webterm/server.go:791: shutdown 62.5% github.com/rcarmo/webterm-go-port/webterm/server.go:814: Handler 100.0% github.com/rcarmo/webterm-go-port/webterm/server.go:829: Run 77.8% github.com/rcarmo/webterm-go-port/webterm/session.go:31: OnData 0.0% github.com/rcarmo/webterm-go-port/webterm/session.go:32: OnBinary 0.0% github.com/rcarmo/webterm-go-port/webterm/session.go:33: OnMeta 0.0% github.com/rcarmo/webterm-go-port/webterm/session.go:34: OnClose 0.0% github.com/rcarmo/webterm-go-port/webterm/session.go:36: dispatchSessionOutput 100.0% github.com/rcarmo/webterm-go-port/webterm/session.go:47: snapshotFromTracker 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:20: NewSessionManager 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:34: SetSessionFactory 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:40: defaultSessionFactory 87.5% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:57: splitCommand 75.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:65: shlexSplit 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:69: AddApp 87.5% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:88: RemoveApp 87.5% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:101: Apps 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:107: AppBySlug 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:114: GetDefaultApp 80.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:123: NewSession 50.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:167: OnSessionEnd 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:176: CloseAll 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:189: CloseSession 87.5% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:201: GetSession 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:207: GetSessionByRouteKey 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:217: GetSessionIDByRouteKey 100.0% github.com/rcarmo/webterm-go-port/webterm/session_manager.go:223: GetFirstRunningSession 85.7% github.com/rcarmo/webterm-go-port/webterm/shellsplit.go:5: shlexSplitImpl 100.0% github.com/rcarmo/webterm-go-port/webterm/slugify.go:13: Slugify 100.0% github.com/rcarmo/webterm-go-port/webterm/svg_exporter.go:35: RenderTerminalSVG 92.6% github.com/rcarmo/webterm-go-port/webterm/svg_exporter.go:113: colorToHex 87.5% github.com/rcarmo/webterm-go-port/webterm/svg_exporter.go:139: isHex 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:37: NewTerminalSession 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:49: Open 86.7% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:90: Start 85.7% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:110: readLoop 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:132: handleOutput 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:142: Close 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:160: Wait 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:167: SetTerminalSize 80.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:190: ForceRedraw 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:198: SendBytes 88.9% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:211: SendMeta 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:215: IsRunning 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:221: GetReplayBuffer 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:225: GetScreenSnapshot 100.0% github.com/rcarmo/webterm-go-port/webterm/terminal_session.go:233: UpdateConnector 80.0% github.com/rcarmo/webterm-go-port/webterm/twoway.go:14: NewTwoWayMap 100.0% github.com/rcarmo/webterm-go-port/webterm/twoway.go:21: Set 88.9% github.com/rcarmo/webterm-go-port/webterm/twoway.go:36: DeleteKey 100.0% github.com/rcarmo/webterm-go-port/webterm/twoway.go:45: Get 100.0% github.com/rcarmo/webterm-go-port/webterm/twoway.go:52: GetKey 100.0% github.com/rcarmo/webterm-go-port/webterm/twoway.go:59: Keys 100.0% github.com/rcarmo/webterm-go-port/webterm/twoway.go:70: UnsafeForward 100.0% total: (statements) 81.0% - cd go && go test -race ./... ok github.com/rcarmo/webterm-go-port/cmd/webterm (cached) ok github.com/rcarmo/webterm-go-port/internal/terminalstate (cached) ok github.com/rcarmo/webterm-go-port/webterm (cached) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,289 +1,118 @@
|
||||
# webterm
|
||||
# webterm (Go)
|
||||
|
||||

|
||||
|
||||
Serve terminal sessions over the web with a simple CLI command. [Blog post](https://taoofmac.com/space/notes/2026/01/25/2030#seizing-the-means-of-production)
|
||||
|
||||
> **Credit and Inspiration:** This project was originally based on the genius [textual-web](https://github.com/Textualize/textual-web) package, which uses `xterm.js`. It has been rewritten to use a [ghostty-web](https://github.com/coder/ghostty-web)'s WebAssembly-based terminal emulator, which provides better performance and native theme support.
|
||||
|
||||
It is, for the moment, temporarily based on a [patched version of ghostty-web](https://github.com/rcarmo/ghostty-web), because the current version has bugs and feature gaps that I needed to fill.
|
||||
|
||||
Coupled with [`agentbox`](https://github.com/rcarmo/agentbox), you can use it to keep track of several containerized AI coding agents, since it provides an easy way to expose terminal sessions via HTTP/WebSocket with automatic reconnection support:
|
||||
`webterm` serves terminal sessions over HTTP/WebSocket, with a dashboard mode for multiple sessions and Docker-aware tiles.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Web-based terminal** - Access your terminal from any browser
|
||||
- **Mobile support** - Works on iOS Safari and Android with on-screen keyboard modifier (experimental) and touch selection
|
||||
- **Session reconnection** - Refresh the page and reconnect to the same session
|
||||
- **Full terminal emulation** - Colors, cursor, and ANSI codes work correctly
|
||||
- **Customizable themes** - 9 built-in themes (monokai, dracula, nord, etc.)
|
||||
- **Custom fonts** - Configure terminal font family and size
|
||||
- **Scrollback history** - Scroll back through terminal output (configurable)
|
||||
- **Auto-sizing** - Terminal automatically resizes to fit the browser window
|
||||
- **Live screenshots** - Dashboard shows real-time SVG screenshots of terminals
|
||||
- **CPU sparklines** - Dashboard displays 30-minute CPU history for Docker containers
|
||||
- **SSE updates** - Real-time screenshot updates via Server-Sent Events
|
||||
- **Simple CLI** - One command to start serving
|
||||
- Web terminal with reconnect support
|
||||
- Session dashboard with live SVG screenshots
|
||||
- Docker watch mode (`webterm-command` / `webterm-theme` labels)
|
||||
- Docker compose manifest ingestion
|
||||
- CPU sparkline tiles for compose services
|
||||
- SSE activity updates for fast dashboard refresh
|
||||
- Theme/font controls for terminal rendering
|
||||
|
||||
## Non-Features
|
||||
## Install
|
||||
|
||||
- **No Authentication** - this is meant to be used inside a dedicated container, and you should set up an authenticating reverse proxy like `authelia`
|
||||
- **No Encryption (TLS/HTTPS)** - again, this is meant to be fronted by something like `traefik` or `caddy`
|
||||
|
||||
## Known Issues
|
||||
|
||||
- `pyte` (the library used to capture the underlying terminal state for screenshots) does not implement some standard escape sequences, resulting in occasionally mis-rendered screenshots. We monkeypatch pyte at runtime to add missing support (CSI S/T scroll, alternate screen buffers, etc.) — see [docs/pyte-patches.md](docs/pyte-patches.md) for details.
|
||||
|
||||
## Installation
|
||||
|
||||
Install directly from GitHub:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/rcarmo/webterm.git
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Serve a Terminal
|
||||
|
||||
Serve your default shell:
|
||||
|
||||
```bash
|
||||
webterm
|
||||
```
|
||||
|
||||
Serve a specific command:
|
||||
|
||||
```bash
|
||||
webterm htop
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
Specify host and port:
|
||||
|
||||
```bash
|
||||
webterm --host 0.0.0.0 --port 8080 bash
|
||||
```
|
||||
|
||||
Customize theme and font:
|
||||
|
||||
```bash
|
||||
webterm --theme dracula --font-size 18
|
||||
webterm --theme nord --font-family "JetBrains Mono, monospace"
|
||||
```
|
||||
|
||||
Available themes: `xterm` (default), `monokai`, `dark`, `light`, `dracula`, `catppuccin`, `nord`, `gruvbox`, `solarized`, `tokyo`.
|
||||
|
||||
Then open http://localhost:8080 in your browser.
|
||||
|
||||
## Session Dashboard
|
||||
|
||||
You can serve a dashboard with multiple terminal tiles driven by a YAML manifest:
|
||||
|
||||
```yaml
|
||||
- name: My Service
|
||||
slug: my-service
|
||||
command: docker logs -f my-service
|
||||
```
|
||||
|
||||
Run with:
|
||||
|
||||
```bash
|
||||
webterm --landing-manifest landing.yaml
|
||||
```
|
||||
|
||||
### Docker Watch Mode
|
||||
|
||||
Watch for Docker containers with `webterm-command` **or** `webterm-theme` labels and dynamically add/remove terminal sessions:
|
||||
|
||||
```bash
|
||||
webterm --docker-watch
|
||||
```
|
||||
|
||||
When a container starts with either label, it automatically appears in the dashboard. When it stops, it's removed. Label values:
|
||||
|
||||
- `webterm-command: auto` (or empty) - Opens a PTY via Docker exec API (override with `WEBTERM_DOCKER_AUTO_COMMAND`)
|
||||
- `webterm-command: <command>` - Runs the specified command
|
||||
- `webterm-theme: <theme>` - Sets the terminal theme for that container (xterm, monokai, dark, light, dracula, catppuccin, nord, gruvbox, solarized, tokyo). Invalid themes fall back to `tango` and the page background defaults to black.
|
||||
|
||||
Containers that only specify `webterm-theme` are still included and use the default auto command.
|
||||
|
||||
**Environment Variables:**
|
||||
- `WEBTERM_DOCKER_USERNAME` - Set to run Docker exec sessions as a specific user (default: root)
|
||||
- `WEBTERM_DOCKER_AUTO_COMMAND` - Override the default `auto` command (default: `/bin/bash`). Supports `{container}` placeholder for the container name.
|
||||
- `WEBTERM_SCREENSHOT_FORCE_REDRAW` - When set to `true`, send a SIGWINCH-style redraw before generating screenshots (default: false).
|
||||
|
||||
Example: Start containers and exec into them as `developer` user:
|
||||
```bash
|
||||
WEBTERM_DOCKER_USERNAME=developer webterm --docker-watch
|
||||
```
|
||||
|
||||
Example: Use tmux with per-container session names:
|
||||
```bash
|
||||
WEBTERM_DOCKER_AUTO_COMMAND="tmux new-session -ADs {container}" webterm --docker-watch
|
||||
```
|
||||
This creates a tmux session named after each container (e.g., `my-webapp`, `redis`, etc.) instead of a shared session name.
|
||||
|
||||
Example docker-compose.yaml:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myapp:
|
||||
image: myapp:latest
|
||||
labels:
|
||||
webterm-command: auto # Opens bash in container
|
||||
webterm-theme: monokai
|
||||
|
||||
logs:
|
||||
image: myapp:latest
|
||||
labels:
|
||||
webterm-command: docker logs -f myapp # Shows logs
|
||||
webterm-theme: nord
|
||||
```
|
||||
|
||||
**Requires**: Docker socket access (`-v /var/run/docker.sock:/var/run/docker.sock`)
|
||||
|
||||
### Docker Compose Integration
|
||||
|
||||
Point to a docker-compose file; services with the label `webterm-command` become tiles (and `webterm-theme` applies there too):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
image: postgres
|
||||
labels:
|
||||
webterm-command: docker exec -it db psql
|
||||
webterm-theme: gruvbox
|
||||
```
|
||||
|
||||
Start with:
|
||||
|
||||
```bash
|
||||
webterm --compose-manifest compose.yaml
|
||||
```
|
||||
|
||||
In compose mode, the dashboard displays **CPU sparklines** showing 30 minutes of container CPU usage history (requires access to Docker socket at `/var/run/docker.sock`).
|
||||
|
||||
### Dashboard Features
|
||||
|
||||
- **Live screenshots** - Terminal thumbnails update in real-time via SSE when activity occurs
|
||||
- **Dynamic updates** - In docker-watch mode, tiles appear/disappear as containers start/stop
|
||||
- **CPU sparklines** - Mini charts showing container CPU usage (compose mode only)
|
||||
- **Tab reuse** - Clicking the same tile reopens the existing browser tab
|
||||
- **Auto-focus** - Terminals automatically receive keyboard focus on load
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
Usage: webterm [OPTIONS] [COMMAND]
|
||||
|
||||
Serve a terminal over HTTP/WebSocket.
|
||||
|
||||
COMMAND: Shell command to run in terminal (default: $SHELL)
|
||||
|
||||
Options:
|
||||
-H, --host TEXT Host to bind to [default: 0.0.0.0]
|
||||
-p, --port INTEGER Port to bind to [default: 8080]
|
||||
-L, --landing-manifest PATH YAML manifest describing landing page tiles
|
||||
(slug/name/command).
|
||||
-C, --compose-manifest PATH Docker compose YAML; services with label
|
||||
"webterm-command" become landing tiles.
|
||||
-D, --docker-watch Watch Docker for containers with
|
||||
"webterm-command" label (dynamic mode).
|
||||
-t, --theme TEXT Terminal color theme [default: xterm]
|
||||
Options: xterm, monokai, dark, light, dracula,
|
||||
catppuccin, nord, gruvbox, solarized, tokyo
|
||||
-f, --font-family TEXT Terminal font family (CSS font stack)
|
||||
-s, --font-size INTEGER Terminal font size in pixels [default: 16]
|
||||
--version Show the version and exit.
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `/` | Dashboard (with manifest/docker-watch) or terminal view |
|
||||
| `/ws/{route_key}` | WebSocket for terminal I/O |
|
||||
| `/screenshot.svg?route_key=...` | SVG screenshot of terminal |
|
||||
| `/cpu-sparkline.svg?container=...` | CPU sparkline SVG (compose mode) |
|
||||
| `/tiles` | JSON list of current tiles (for dynamic dashboards) |
|
||||
| `/events` | SSE stream for activity notifications |
|
||||
| `/health` | Health check endpoint |
|
||||
|
||||
## Development
|
||||
|
||||
### Setup (Makefile-first)
|
||||
### Build from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rcarmo/webterm.git
|
||||
cd webterm
|
||||
cd webterm/go
|
||||
mkdir -p bin
|
||||
go build -o ./bin/webterm ./cmd/webterm
|
||||
```
|
||||
|
||||
# Install with dev dependencies via Makefile
|
||||
The command above produces `go/bin/webterm`; you can also build it from repo root with `make build-go`.
|
||||
|
||||
## Quick start
|
||||
|
||||
Run a default shell session:
|
||||
|
||||
```bash
|
||||
cd go
|
||||
go run ./cmd/webterm
|
||||
```
|
||||
|
||||
Run a specific command:
|
||||
|
||||
```bash
|
||||
cd go
|
||||
go run ./cmd/webterm -- htop
|
||||
```
|
||||
|
||||
Then open <http://localhost:8080>.
|
||||
|
||||
## Dashboard modes
|
||||
|
||||
### Landing manifest
|
||||
|
||||
```yaml
|
||||
- name: Logs
|
||||
slug: logs
|
||||
command: docker logs -f my-service
|
||||
theme: nord
|
||||
```
|
||||
|
||||
```bash
|
||||
cd go
|
||||
go run ./cmd/webterm -- --landing-manifest ../landing.yaml
|
||||
```
|
||||
|
||||
### Docker watch
|
||||
|
||||
```bash
|
||||
cd go
|
||||
go run ./cmd/webterm -- --docker-watch
|
||||
```
|
||||
|
||||
Containers with these labels become tiles:
|
||||
|
||||
- `webterm-command`: command string, or `auto` for Docker exec
|
||||
- `webterm-theme`: theme name (fallback is `xterm` palette)
|
||||
|
||||
### Compose manifest
|
||||
|
||||
```bash
|
||||
cd go
|
||||
go run ./cmd/webterm -- --compose-manifest ../docker-compose.yaml
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
- `WEBTERM_STATIC_PATH`: override static asset directory
|
||||
- `WEBTERM_DOCKER_USERNAME`: user for Docker exec sessions
|
||||
- `WEBTERM_DOCKER_AUTO_COMMAND`: override auto command (`/bin/bash` default)
|
||||
- `WEBTERM_SCREENSHOT_FORCE_REDRAW`: force redraw before screenshots (`true/1/yes/on`)
|
||||
- `DOCKER_HOST`: Docker daemon endpoint override
|
||||
|
||||
## Development (Makefile-first)
|
||||
|
||||
```bash
|
||||
make install-dev
|
||||
make check
|
||||
make race
|
||||
make test
|
||||
```
|
||||
|
||||
### Common tasks (use Makefile)
|
||||
|
||||
- Lint: `make lint`
|
||||
- Format: `make format`
|
||||
- Tests: `make test`
|
||||
- Coverage (fail_under=78): `make coverage`
|
||||
- Full check (lint + coverage): `make check`
|
||||
- Bump patch version: `make bump-patch`
|
||||
|
||||
### Frontend Development
|
||||
|
||||
The terminal UI is built with a [patched version of ghostty-web](https://github.com/rcarmo/ghostty-web), which provides Ghostty's VT100 parser via WebAssembly with native theme/palette support. This replaces the original xterm.js dependency used in earlier versions.
|
||||
|
||||
Key improvements over xterm.js:
|
||||
- Native theme colors passed directly to WASM (no runtime color remapping)
|
||||
- Smaller bundle size (~0.67 MB vs ~1.16 MB)
|
||||
- IME input support for CJK languages
|
||||
- Better Unicode and complex script rendering
|
||||
|
||||
The pre-built bundle is committed to the repo, so users can `pip install` without needing Node.js.
|
||||
|
||||
To rebuild the frontend after modifying `terminal.ts`:
|
||||
|
||||
```bash
|
||||
# Requires Bun (https://bun.sh)
|
||||
bun install
|
||||
bun run build
|
||||
# Or simply:
|
||||
make bundle
|
||||
```
|
||||
|
||||
For development with auto-rebuild:
|
||||
Frontend bundle tasks:
|
||||
|
||||
```bash
|
||||
make build
|
||||
make build-fast
|
||||
make bundle-watch
|
||||
```
|
||||
|
||||
### Notes
|
||||
## Docker
|
||||
|
||||
- WebSocket protocol (browser ↔ server) is JSON: `["stdin", data]`, `["resize", {"width": w, "height": h}]`, `["ping", data]`.
|
||||
- Frontend source is in `src/webterm/static/js/terminal.ts`.
|
||||
- Screenshots use [pyte](https://github.com/selectel/pyte) for ANSI interpretation and custom SVG rendering. `AltScreen` adds alternate screen buffer support, [CSI S/T scroll handling, and Ink partial clear expansion](docs/pyte-patches.md).
|
||||
- Go runtime port is in `go/webterm` (entrypoint: `go/cmd/webterm`), using [go-te](https://github.com/rcarmo/go-te) for terminal state and screenshots.
|
||||
- CPU stats are read directly from Docker socket using asyncio (no additional dependencies).
|
||||
```bash
|
||||
docker build -t webterm .
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 webterm --docker-watch
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.9+
|
||||
- Bun
|
||||
- Linux or macOS
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [ghostty-web](https://github.com/rcarmo/ghostty-web) - Patched Ghostty terminal for the web (vendored fork with theme support)
|
||||
- [ghostty-web upstream](https://github.com/coder/ghostty-web) - Original Ghostty terminal for the web
|
||||
- [pyte](https://github.com/selectel/pyte) - PYTE terminal emulator (used for SVG screenshots)
|
||||
- [go-te](https://github.com/rcarmo/go-te) - Go VT terminal emulator used for the in-progress port
|
||||
The image sets `WEBTERM_STATIC_PATH=/app/static` and serves assets from `go/webterm/static`.
|
||||
The Dockerfile uses a minimal Alpine runtime stage and only installs `ca-certificates` plus `docker-cli`.
|
||||
|
||||
Reference in New Issue
Block a user