Skip to content

Kynde/swyd

Repository files navigation

Show What You Doing

Swyd - Show What You Doing

Deprecated

The features here were all merged to airc and extended further.

Overview

Swyd mirrors a tmux session, read-only, into a web page. The target viewer is the Tesla browser; the terminal session runs on the laptop sitting in the car (driven by voice via dais). There is deliberately no browser-to-terminal input path.

Tesla browser -> ngrok HTTPS dev domain -> laptop localhost server -> tmux

The server follows the active pane of one tmux session (a specific pane can be pinned from the viewer), converts tmux capture-pane -e output to HTML server-side, and the page polls it with ETag/304 suppression and an adaptive interval. The server also spawns and supervises the ngrok agent as a child process, so one systemd unit is the whole system.

Usage

tools/swyd on        # start unit, wait for tunnel, print the bookmark URL
tools/swyd off       # stop everything (server + ngrok child); nothing stays running
tools/swyd status    # one-line status; --json for machines (dais)
tools/swyd url       # print the bookmark URL without starting anything

One-time install of the user unit:

systemctl --user link /home/k/swyd/tools/swyd.service
systemctl --user daemon-reload

Logs: journalctl --user -u swyd -f.

The Tesla bookmark is the URL printed by swyd on/swyd url: https://<domain>/?k=<token>. The token persists as a cookie after the first visit; the bookmark re-authenticates with zero typing even when the cookie is gone. Everything except fonts/static assets answers plain 404 without the token. /healthz is open to local loopback only (for dais-top), and requires the token through the tunnel.

Configuration

config.json (gitignored; see config.example.json for all keys with their defaults). The auth token is generated and written there on first run.

{
  "session": "swyd-test",
  "port": 8080,
  "resizeToViewport": false,
  "ngrok": { "enabled": true, "domain": "carrousel-value-recipient.ngrok-free.dev" }
}

CLI flags override the config per-invocation:

node src/server.js --session app --no-ngrok --port 8090   # local dev
node src/server.js --print-url                            # bookmark URL, then exit
yarn dev                                                  # server without tunnel

resizeToViewport (opt-in): the viewer reports its cols/rows and the server runs tmux resize-window to match the car screen exactly. Off by default because it reshapes the same session shown on the laptop. The default sizing is client-side: the page scales the font so the captured pane fits the viewport (Fit), with manual A−/A+ override.

Pages and endpoints

  • / viewer: terminal + status strip (live/idle/stale dot, pane label → pane picker overlay, font controls, day/night theme, pause).
  • /probe browser capability probe (kept for in-car diagnostics).
  • /api/tmux/frame one frame as JSON (?pane=%5 pins a pane); supports If-None-Match/304. /api/tmux/panes lists panes. /api/config viewer settings. /healthz rich status JSON (tunnel URL, last-capture age, viewers in the last minute).

Note lastCaptureAgeMs in /healthz grows when no viewer is polling - capture is request-driven, so "old but ngrok up" means no viewer, not broken.

Development

Node >= 20, dependency-free (built-in modules only). tmux >= 3.x, ngrok agent on PATH (~/bin/ngrok).

yarn check    # node --check over all server/client JS + inline scripts in public/*.html

Layout: src/server.js (routes, lifecycle), src/tmux.js (capture, pane resolution - note tmux exits 0 with empty fields for unknown targets, the empty pane id is the "not found" signal), src/ansi.js (SGR -> HTML: 16-color palette as themable CSS classes, 256/truecolor inline, bold/dim/ italic/underline/reverse), src/auth.js, src/ngrok.js (child supervision, URL discovery via the agent API on :4040), src/config.js. Viewer in public/ (index.html, app.js, app.css).

More complete context for humans and fresh-context coding agents lives in docs/: project brief, architecture, operations, Tesla browser findings, implementation notes, and future work.

A color-cycling test session for poking at the viewer:

tmux new -d -s swyd-test -x 100 -y 30 \
  'while true; do printf "\033[32mgreen\033[0m \033[38;5;208m256\033[0m \033[38;2;255;105;180mrgb\033[0m\n"; date; sleep 2; done'

Tesla findings (POC, 2026-06)

Network:

  • Direct hotspot LAN access did not work from Tesla (10.204.192.28:8080); the same URL worked from laptop and phone. Tesla appears to block hotspot-local/private addresses, at least 10.0.0.0/8.
  • Cloudflare TryCloudflare worked; ngrok preferred because the assigned free dev domain is a stable bookmark.
  • ngrok free tier: interstitial on first visit (cookie expires after ~1 week in Firefox), and only one agent session - a stray manually started ngrok blocks the supervised child (its error is logged verbatim).

Browser (Chrome 140-based, X11 UA):

  • Viewport (fixed, confirmed in car): 1180x919 css px, devicePixelRatio: 1 (physical == css, no scaling). Fullscreen-stretched in Park is 1920x1089 (available 1920x1200) but still shows a location bar — not true fullscreen.
  • fetch, WebSocket-through-ngrok, EventSource, cookies, and localStorage all work. navigator.language = fi.

Rendering:

  • Tesla's generic monospace and Courier New stacks do NOT render fixed-width; the bundled Fira Code (public/fonts/) renders aligned. The terminal must use the web font.

Confirmed in car (2026-06-12):

  • Stays live while driving, not just in Park — minor latency, works well.
  • Blinking cursor is visible and correctly positioned.
  • Pane switching from the top pane-picker works well.

Still open — text sizing / pane fit:

  • A quarter-size Konsole window fit the car screen well; a full half window was too much (too many cols/rows → client auto-fit shrinks the font too small). Raising the Konsole font (fewer cols/rows) helped.
  • The lever is the source pane's cols/rows, not just font scaling. Likely direction: the opt-in resizeToViewport or a target cols/rows cap. Being tested before committing to an approach.

Transport notes

Polling at 1 Hz (stretching to pollIdleMaxMs after unchanged frames, 304s in between) is simple and tunnel-friendly for a passive viewer. WebSocket verified working through ngrok and can replace polling later as an optimization, keeping polling as the fallback.

About

Show what you do

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors