A draggable, resizable desktop terminal that runs your real shell — vim, htop, ssh, tmux, fzf all work — by embedding ttyd inside an Übersicht widget.
Übersicht widgets render a flat HTML/JS page on the desktop — they have no built-in terminal emulator. Naïve PTY-relay implementations (the original design of this widget) garble interactive programs because there is no one to interpret ANSI cursor sequences, alt-screen switches, or per-keystroke echo.
This widget delegates that work to xterm.js, packaged by ttyd as a tiny localhost web terminal, and wraps it in a movable Übersicht container. The widget itself is ~150 lines of JSX — all the terminal heavy-lifting is done by mature, battle-tested code.
┌─────────────────────────────────────────┐
│ Übersicht widget (index.jsx) │
│ ┌──────────────────────────────────┐ │
│ │ <iframe src=localhost:7681> │ │ ← xterm.js front end
│ │ ▲ │ │
│ └───┼──────────────────────────────┘ │
└──────┼──────────────────────────────────┘
│ WebSocket
▼
ttyd ──spawns──► /bin/zsh -l (your real shell)
- ✅ Full interactive shell — vim, htop, ssh, tmux, fzf, less, watch
- ✅ Powerlevel10k / oh-my-zsh / Nerd Font glyphs render correctly
- ✅ Drag to move (top 8 px strip), drag bottom-right corner to resize
- ✅ Multiple independent instances (each with its own port +
.zshrc) - ✅ Hidden scrollbar for a clean desktop look
- ✅ No login screen, localhost-only — never exposed to the network
| Dependency | Install |
|---|---|
| macOS | already there |
| Übersicht | App Store / brew install --cask ubersicht |
| ttyd ≥ 1.7 | brew install ttyd |
| zsh (or any shell of your choice) | macOS ships with zsh |
| A Nerd Font (optional, for prompt glyphs) | brew install font-jetbrains-mono-nerd-font |
cd "/Users/<you>/Library/Application Support/Übersicht/widgets/terminal-widget.widget"
python3 install.pyThe installer walks you through:
- Checking ttyd / Übersicht
- Choosing how many independent widget instances to create
- Picking a port and (optionally) a custom
ZDOTDIRfor each instance - Cloning the widget folder, patching constants, and refreshing Übersicht
- Drop this folder into
~/Library/Application Support/Übersicht/widgets/ - Übersicht menu → Refresh All Widgets
That's it. The widget will spawn ttyd on first tick and connect to it.
Each instance needs (a) its own widget folder, (b) a unique port, and
optionally (c) its own .zshrc.
The installer automates this. Manually:
# 1. Clone the widget folder
cp -R terminal-widget.widget terminal-widget-work.widget
# 2. (optional) Prepare a separate zsh config dir
mkdir -p ~/.config/widget-zsh-work
cp ~/.zshrc ~/.config/widget-zsh-work/.zshrc
# … edit to taste — different aliases, prompt, PATH, etc.
# 3. Edit the new widget's index.jsx — change the two constants at the top:
# const TTYD_PORT = 7682;
# const ZDOTDIR = "/Users/scottchen/.config/widget-zsh-work";Refresh Übersicht and you have a second, fully isolated zsh session living on the desktop.
terminal-widget.widget/
├── index.jsx ← Übersicht widget: iframe + drag/resize + state
├── bridge-tick.sh ← per-poll launcher: ensures ttyd is running
├── install.py ← interactive installer / instance cloner
├── widget.json ← Übersicht metadata
└── README.md
Logs:
/tmp/terminal-widget-ttyd-<port>.log— ttyd's stderr per instance/tmp/terminal-widget-ttyd-<port>.lock— race-prevention lock dir
Top of index.jsx:
const TTYD_PORT = 7681; // unique per instance
const ZDOTDIR = "/Users/scottchen/.config/widget-zsh-a"; // "" → use ~/.zshrcInside bridge-tick.sh:
SHELL_BIN="/bin/zsh" # change to bash / fish / nushell etc.ttyd theming (also in bridge-tick.sh) — passed via -t key=value:
-t 'fontFamily=JetBrainsMono Nerd Font, Menlo, monospace'
-t 'fontSize=13'
-t 'theme={"background":"#141418","foreground":"#d4d4d4","cursor":"#5fd7af"}'
-t 'cursorStyle=bar'Full xterm.js options: https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/
| Symptom | Fix |
|---|---|
| "starting ttyd…" never goes away | Check cat /tmp/terminal-widget-ttyd-7681.log — typically a port conflict or missing ttyd binary |
| Widget loads but iframe is blank | Refresh once more; ttyd needs ~0.5 s to bind on first launch |
| Glyphs show as boxes | Install a Nerd Font and update fontFamily in bridge-tick.sh |
| Two widgets fight over the same shell | They share a port — give each its own TTYD_PORT |
| Powerlevel10k complains about instant prompt | Add POWERLEVEL9K_INSTANT_PROMPT=quiet to your .zshrc (or .p10k.zsh) |
| Want a true logout instead of disconnect on close | Restart Übersicht or pkill -f 'ttyd -p <port>' |
pkill -f 'ttyd -p 768' # kills all widget ttyds in the 768x range
rm -f /tmp/terminal-widget-ttyd-* # logs + lock dirs- ttyd binds to 127.0.0.1 only (
-i 127.0.0.1). Nothing on your LAN can reach it. - No authentication is required for localhost connections — anyone able to
open
http://127.0.0.1:7681already has shell access via your user account anyway. - If you don't trust other local users on this Mac, add
--credential user:passwordto the ttyd invocation inbridge-tick.sh.
MIT. ttyd is also MIT. xterm.js is MIT. Author: Scott Chen.