
Remobi Setup
- 21 installs
- 52 repo stars
- Updated July 16, 2026
- connorads/remobi
Onboards remobi, the mobile tmux terminal overlay, by inspecting tmux config, generating a validated remobi.config.ts, and guiding Tailscale deployment.
About
Runs full interactive onboarding for remobi, the mobile terminal overlay for tmux, checking prerequisites, inspecting tmux config, generating a validated remobi.config.ts, and walking through deployment. A developer uses it to set up remobi and control tmux from their phone.
- Interactive onboarding that detects tmux config and generates a validated remobi.config.ts
- Covers Tailscale/Cloudflare deployment and local-only security hardening
Remobi Setup by the numbers
- 21 all-time installs (skills.sh)
- Ranked #361 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/connorads/remobi --skill remobi-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 52 |
| Last updated | July 16, 2026 |
| Repository | connorads/remobi ↗ |
What it does
Onboards remobi, the mobile tmux terminal overlay, by inspecting tmux config, generating a validated remobi.config.ts, and guiding Tailscale deployment.
Files
remobi-setup
Interactive onboarding skill for remobi — monitor and control tmux from your phone.
This skill walks the user through setup in one conversation. The guiding principle: detect everything possible, default everything sensible, ask only what requires human intent. Most users answer 1-3 questions total.
Workflow
Phase 1: Welcome and understand (1 question)
Open with a one-liner confirming what they're getting, then ask what brings them here:
"remobi puts your tmux session on your phone — same panes, same windows, touch controls on top. Everything we set up here you can change later."
>
"What brings you to remobi? For example: monitoring coding agents from your phone, getting phone access to your dev sessions, or just curious to try it out."
Map the answer to a persona internally (don't tell the user their "persona"):
| Persona | Signals | Downstream effect |
|---|---|---|
| Agent Watcher | Mentions coding agents, Claude Code, Codex, AI, monitoring | Auto-zoom on, floating zoom button, double-tap zoom enabled, lean config, minimal questions |
| Remote Dev | Mentions tmux, SSH, dev workflow, existing setup | Inspect config thoroughly, offer popup drawer buttons, ask about auto-zoom |
| Newcomer | Says curious, trying it out, heard about it, no specific use case | Offer tmux setup, explain concepts, auto-zoom on, sensible defaults |
If the answer is ambiguous, lean towards Agent Watcher — it's the most common path and the defaults work well for everyone.
Phase 2: Environment and tmux setup
Check prerequisites
Run silently, then report what's present vs missing:
node --version # need >= 22
tmux -V # target multiplexer
which remobi # npm install -g remobiIf anything is missing, help install it:
- Node: suggest mise, nvm, or direct install
- tmux:
brew install tmuxor distro package - remobi:
npm install -g remobi
Inspect tmux
Gather the user's tmux configuration to inform config generation.
tmux show-options -g prefix # prefix key
tmux list-keys # all bindings
tmux show-options -g mouse # mouse mode
tmux show-options -g status-left # status bar
tmux show-options -g status-position # top or bottom
tmux list-keys | grep display-popup # popup bindingsIf tmux isn't running, fall back to reading the config file directly:
cat ~/.config/tmux/tmux.conf 2>/dev/null || cat ~/.tmux.conf 2>/dev/nullAuto-detect and note:
- Prefix key and byte (Ctrl-B =
\x02, Ctrl-A =\x01, etc.) - Custom popup bindings (lazygit, yazi, scratch shell, system monitor, etc.)
- Whether mouse mode is on
- Split bindings (stock
%/"or remapped|/-) - Status bar complexity and position
- Plugin manager (tpm, etc.)
- Double-tap zoom gesture (see
references/mobile-panes.mdfor pane workflows)
Detect installed tools — check for popular tools that work well as tmux popup bindings:
which lazygit # Git TUI
which yazi # File manager
which btm || which htop # System monitor
which nvim || which vim # EditorOffer tmux setup (Agent Watcher and Newcomer only)
If no tmux config exists, read references/tmux-basics.md and offer to create one. Frame it as a proposal, not a gap:
Agent Watcher framing:
"I'll create a tmux config tuned for monitoring agents — mouse support, status bar at top, and double-tap zoom so you can zoom into any agent pane on your phone. Go ahead?"
Newcomer framing:
"tmux is the terminal multiplexer that remobi sits on top of — it keeps your sessions running even when you disconnect. I'll set up a config with mouse support, sensible defaults, and a help popup to learn the keybindings. Want me to explain what each setting does as I go?"
Remote Dev: Skip — they already have a config.
The starter config comes from references/tmux-basics.md. For Agent Watchers, include the "Agent watcher starter config" section (zoom indicator, auto-rename, double-tap zoom via remobi config).
For Newcomers with detected tools, also offer popup bindings:
"I found lazygit and yazi on your system. These work great as tmux popups — one keypress to open a floating window. Want me to add popup bindings for them?"
Only proceed to Phase 3 once the user has a working tmux session.
Phase 3: Confirm detections and ask what's needed (0-3 questions)
Present a summary of what you found and what you plan to configure. The style is "here's what I'll do" with checkpoints, not an interview.
Summary format:
"Based on your setup, here's what I'll configure:
- Prefix: Ctrl-B (detected from your tmux config)
- Auto-zoom on mobile load (pane fills the phone screen)
- Floating zoom button (one-tap zoom toggle)
- Default toolbar and drawer buttons
- [If applicable:] Drawer buttons for lazygit and yazi (matching your popup bindings)"
Then ask only questions that can't be detected or defaulted:
Questions by persona
Agent Watcher (0-1 questions):
If popup bindings or tools were detected:
"I found [lazygit/yazi/btm] on your system and matching popup bindings. Want drawer buttons for these in remobi so you can trigger them from your phone?"
If nothing special detected: zero questions — proceed straight to config generation.
Remote Dev (1-3 questions):
Question 1 (if popup bindings or tools detected):
"I found popup bindings for [list]. Want matching drawer buttons in remobi?"
Question 2 (if multi-pane layout likely):
"Do you want auto-zoom when you open remobi on your phone? This zooms the current pane to full screen — works well with multi-pane layouts on a small screen."
Question 3 (catch-all):
"Anything else you want accessible from your phone? Custom tmux bindings, specific tools, anything I missed?"
Newcomer (0-1 questions):
If tools were detected and popup bindings were set up in Phase 2:
"I set up popup bindings for [lazygit/yazi]. Want matching buttons in remobi's command drawer?"
Otherwise: zero questions — defaults are great to start with.
Summarise what you've gathered before moving to config generation.
Phase 4: Generate config and suggest tmux tweaks
Generate remobi.config.ts
Export a plain config object — only include keys that differ from defaults, omit everything else. Do not import { defineConfig } from 'remobi' — the CLI calls defineConfig() internally so the config just needs a plain object export.
export default {
// Only non-default overrides here
}Place at ~/.config/remobi/remobi.config.ts (XDG location) unless the user prefers elsewhere.
After writing, validate by starting remobi. remobi auto-discovers config from the current directory first, then ~/.config/remobi/, so --config is only needed when you want to force a specific file:
remobi serve --port 18765 -- /bin/trueA zero exit means the config loaded and the command started cleanly. If the user stored config somewhere custom, validate that path explicitly instead:
remobi serve --config /path/to/remobi.config.ts --port 18765 -- /bin/trueFix any errors and re-validate until clean.
See Config reference below for the full schema, allowed keys, action types, and escape codes.
Suggest tmux mobile optimisations (Remote Dev only)
For Remote Dev users who already had a tmux config, offer mobile tweaks as a single confirmation. Read references/mobile-tmux.md and references/mobile-panes.md for full context.
"I have a few suggestions to make your tmux more mobile-friendly: [list 2-3 most impactful items]. Want me to add these to your tmux.conf?"
Prioritise by impact, suggest maximum 3:
1. Double-tap zoom (if multi-pane user — enable via remobi gestures.doubleTap) 2. Responsive status bar (if status bar would overflow on phone — see references/mobile-tmux.md) 3. Zoom indicator (if #{window_zoomed_flag} missing from status)
Also check and mention (but don't push):
| Check | Command | Good sign | Suggestion if missing |
|---|---|---|---|
| Mouse mode | tmux show -g mouse | on | set -g mouse on |
| Status position | tmux show -g status-position | top | set -g status-position top (keeps status away from remobi toolbar) |
| Popup sizing | `tmux list-keys \ | grep display-popup` | Uses % dimensions |
| Window renumbering | tmux show -g renumber-windows | on | set -g renumber-windows on |
Suggest snippets only — never modify tmux.conf without explicit permission.
Skip this for Newcomers — their starter config from Phase 2 already includes the essentials.
Phase 5: Deploy and wrap up
Deployment
Detect what's available and recommend accordingly:
which tailscale # check for TailscaleIf Tailscale installed: recommend Tailscale Serve directly:
"I see Tailscale on your system. Tailscale Serve is the simplest way to access remobi from your phone — HTTPS over your private network, no extra setup."
Read references/tailscale-serve.md for the full guide.
If no Tailscale: offer options:
"To access remobi from your phone, you need to put it behind a trusted network layer. Options:
- Tailscale Serve (recommended) — private VPN, HTTPS, easiest setup
- Cloudflare Tunnel + Access — private tunnel with access policies
- Local network — if your phone is on the same WiFi/VPN"
remobi is a remote-control surface for your terminal — never expose it to the public internet. All deployment options keep access private.
Security hardening
remobi hardens the connection even on private networks. Mention these if the user has security concerns:
- Binds `127.0.0.1` only — never exposed to network without explicit
--hostflag - Content-Security-Policy — strict default-src, script-src, connect-src scoped to same host
- WebSocket origin validation — rejects cross-origin upgrade requests
- Relay buffer limit — 1 MB per connection; drops oversized payloads
- Local-only default — remobi binds to
127.0.0.1unless the user explicitly changes--host - X-Frame-Options DENY — prevents clickjacking via iframes
- Referrer-Policy: no-referrer — no URL leaking to external sites
For macOS users, mention --no-sleep and point to references/keep-awake.md for persistent options.
For users migrating from old ttyd-based setups, point to references/ttyd-flags.md as legacy guidance only.
Summary
Tell the user: 1. What was configured and why (prefix byte, custom bindings, gestures, auto-zoom) 2. How to start: remobi serve 3. How to access from their phone (URL from deployment choice) 4. PWA install: on mobile, tap "Add to Home Screen" for a standalone app experience 5. Built-in mobile controls (these work out of the box, no config needed):
- Font size:
+/-buttons in top-right. Config:font.mobileSizeDefault(default 16px),font.sizeRange(default [8, 32]), steps by 2 - Scroll buttons: Floating arrow buttons on the sides. Long-press for rapid repeat (300ms delay, 100ms interval). Auto-fade after 2s. Strategy follows
gestures.scroll.strategy(wheelsends mouse events,keyssends PageUp/PageDown) - Combo picker: Modal for arbitrary key combos — type
C-s,M-Enter,Alt-x,C-[. Supports Ctrl, Alt, Shift modifiers + named keys (PageUp, Escape, etc.). Opened via drawer "Combo" button - Help overlay:
?button in top-right. Shows all configured buttons, gestures, and floating buttons in tables. Config-driven, updates when you change buttons - Landscape + keyboard: When on-screen keyboard opens in landscape, row 2 auto-hides and buttons shrink. No config needed
6. PWA: enabled by default. On mobile Safari/Chrome, tap Share then "Add to Home Screen" for standalone app experience. Config options:
pwa.enabled(defaulttrue) — setfalseto disable manifest + iconspwa.themeColor(default'#1e1e2e') — status bar colour on mobilepwa.shortName(optional) — short name for home screen icon (falls back toname)
7. This is a starting point — not a locked-in config. Run this skill again any time to tweak buttons, add drawer commands, or change gestures.
---
Config reference
Allowed root keys
Exactly these — validation rejects anything else:
name theme font toolbar drawer gestures mobile floatingButtons pwa reconnectButtonAction union
type | Required fields | Notes |
|---|---|---|
send | data: string | Optional keyLabel?: string for help overlay |
prefix | data: string | Sends prefix byte then opens combo picker for follow-up key. Use { type: 'send', data: '\x02' } for raw prefix-only behaviour |
ctrl-modifier | (none) | Opens Ctrl+key combo UI |
paste | (none) | Paste from clipboard |
combo-picker | (none) | Opens Ctrl/Alt + key modal |
drawer-toggle | (none) | Opens/closes command drawer |
Non-send/prefix actions must NOT have data or keyLabel — the validator rejects them.
ControlButton shape
Every button in toolbar rows, drawer, and floatingButtons uses this schema:
{
id: string // unique within its array
label: string // text shown on the button
description: string // shown in help overlay — keep user-facing and clear
action: ButtonAction
}Button array forms (toolbar.row1, toolbar.row2, drawer.buttons)
Two forms — pick the least invasive:
// 1. Replace entirely (plain array)
toolbar: { row1: [{ id, label, description, action }, ...] }
// 2. Transform (function receives defaults, returns new array)
toolbar: { row2: (defaults) => defaults.filter(b => b.id !== 'q') }
// Function form covers all operations via standard JS:
// - Append: (d) => [...d, newBtn]
// - Prepend: (d) => [newBtn, ...d]
// - Remove: (d) => d.filter(b => b.id !== 'q')
// - Replace: (d) => d.map(b => b.id === 'tmux-prefix' ? newBtn : b)
// - Insert: (d) => { const i = d.findIndex(b => b.id === 'tab'); return [...d.slice(0,i), newBtn, ...d.slice(i)] }Floating buttons
Must use the grouped shape — a flat ControlButton[] is rejected:
floatingButtons: [
{
position: 'top-left', // required
direction: 'row', // optional: 'row' | 'column' (default 'row')
buttons: [{ id, label, description, action }],
},
]Valid positions: top-left | top-right | top-centre | bottom-left | bottom-right | bottom-centre | centre-left | centre-right
Default button IDs
Toolbar row 1 (10 buttons):
id | label | action |
|---|---|---|
esc | Esc | send \x1b |
tmux-prefix | Prefix | prefix \x02 (sends prefix then opens combo picker for follow-up key) |
tab | Tab | send \t |
shift-tab | S-Tab | send \x1b[Z |
left | <- | send \x1b[D |
up | up arrow | send \x1b[A |
down | down arrow | send \x1b[B |
right | -> | send \x1b[C |
ctrl-c | C-c | send \x03 |
enter | enter | send \r |
Toolbar row 2 (7 buttons):
id | label | action |
|---|---|---|
q | q | send q |
alt-enter | M-enter | send \x1b\r |
ctrl-d | C-d | send \x04 |
drawer-toggle | hamburger More | drawer-toggle |
paste | Paste | paste |
backspace | backspace | send \x7f |
space | Space | send ' ' |
Drawer (12 buttons):
id | label | action |
|---|---|---|
tmux-new-window | + Win | send \x02c |
tmux-split-vertical | Split \ | |
tmux-split-horizontal | Split -- | send \x02" |
tmux-zoom | Zoom | send \x02z |
tmux-sessions | Sessions | send \x02s |
tmux-windows | Windows | send \x02w |
page-up | PgUp | send \x1b[5~ |
page-down | PgDn | send \x1b[6~ |
tmux-copy | Copy | send \x02[ |
tmux-help | Help | send \x02? |
tmux-kill-pane | Kill | send \x02x |
combo-picker | Combo | combo-picker |
Gestures
| Field | Default | Notes |
|---|---|---|
gestures.swipe.enabled | true | |
gestures.swipe.left | '\x02n' | Next tmux window |
gestures.swipe.right | '\x02p' | Previous tmux window |
gestures.swipe.threshold | 80 | Pixels |
gestures.swipe.maxDuration | 400 | Milliseconds |
gestures.pinch.enabled | false | |
gestures.scroll.enabled | true | |
gestures.scroll.strategy | 'wheel' | 'wheel' (recommended) sends SGR mouse wheel sequences — works in vim, less, htop. 'keys' sends PageUp/PageDown — simpler, works everywhere |
gestures.scroll.sensitivity | 40 | |
gestures.scroll.wheelIntervalMs | 24 | |
gestures.doubleTap.enabled | false | Opt-in double-tap gesture on terminal screen |
gestures.doubleTap.data | '\x02z' | Data to send on double-tap (default: tmux zoom toggle) |
gestures.doubleTap.maxInterval | 300 | Max milliseconds between taps |
Font
| Field | Default | Notes |
|---|---|---|
font.family | 'JetBrainsMono NFM, monospace' | CSS font-family |
font.cdnUrl | jsdelivr nerdfont URL | CSS file for web font |
font.mobileSizeDefault | 16 | px, applied on mobile |
font.sizeRange | [8, 32] | Min/max for +/- buttons |
PWA
| Field | Default | Notes |
|---|---|---|
pwa.enabled | true | Set false to disable manifest + icons |
pwa.themeColor | '#1e1e2e' | Status bar colour on mobile |
pwa.shortName | (none) | Short name for home screen icon, falls back to name |
Hooks (advanced)
Hooks are programmatic, not via defineConfig(). See references/hooks.md if the user asks about analytics, action filtering, or custom DOM. Do not proactively suggest hooks during setup.
Escape-code cheat sheet
Use these in action.data and gesture left/right fields:
| Key | Escape sequence | Notes |
|---|---|---|
| Ctrl-B (prefix) | \x02 | Default tmux prefix |
| Ctrl-A (prefix) | \x01 | screen/byobu/custom prefix |
| Ctrl-C | \x03 | Interrupt |
| Ctrl-D | \x04 | EOF / exit shell |
| Escape | \x1b | |
| Tab | \t | |
| Shift+Tab | \x1b[Z | |
| Enter | \r | |
| Alt+Enter | \x1b\r | |
| Backspace | \x7f | DEL character |
| Up arrow | \x1b[A | |
| Down arrow | \x1b[B | |
| Right arrow | \x1b[C | |
| Left arrow | \x1b[D | |
| Page Up | \x1b[5~ | |
| Page Down | \x1b[6~ | |
| Space | ' ' | literal space |
Composing tmux key sequences
tmux bindings are prefix + key. Concatenate the bytes:
Ctrl-B + c -> '\x02c' (new window)
Ctrl-B + n -> '\x02n' (next window)
Ctrl-B + p -> '\x02p' (previous window)
Ctrl-B + z -> '\x02z' (zoom pane)
Ctrl-B + % -> '\x02%' (split vertical -- stock tmux)
Ctrl-B + " -> '\x02"' (split horizontal -- stock tmux)
Ctrl-B + [ -> '\x02[' (copy mode)
Ctrl-B + d -> '\x02d' (detach)For a custom prefix (e.g. Ctrl-A): replace \x02 with \x01.
Example configs
Minimal — default Ctrl-B prefix, custom name only
export default {
name: 'dev',
}Custom prefix — Ctrl-A (screen/byobu style)
Replace the default tmux-prefix button and update swipe gestures:
export default {
name: 'dev',
toolbar: {
row1: (defaults) => defaults.map(b =>
b.id === 'tmux-prefix'
? { ...b, description: 'Send tmux prefix key (Ctrl-A)', action: { type: 'prefix', data: '\x01' } }
: b
),
},
gestures: {
swipe: {
left: '\x01n',
right: '\x01p',
leftLabel: 'Next tmux window',
rightLabel: 'Previous tmux window',
},
},
drawer: {
buttons: (defaults) => defaults.map(b => {
// Remap tmux-prefixed buttons from Ctrl-B (\x02) to Ctrl-A (\x01)
if (b.action.type === 'send' && b.action.data.startsWith('\x02')) {
return { ...b, action: { ...b.action, data: '\x01' + b.action.data.slice(1) } }
}
return b
}),
},
}Agent watcher — auto-zoom + floating button
export default {
name: 'agents',
mobile: {
initData: '\x02z', // zoom focused pane on mobile load
},
floatingButtons: [
{
position: 'top-left',
buttons: [
{
id: 'zoom',
label: 'Zoom',
description: 'Toggle pane zoom',
action: { type: 'send', data: '\x02z' },
},
],
},
],
}Scroll strategy — keys instead of wheel
export default {
gestures: {
scroll: { strategy: 'keys' },
},
}Popup-heavy workflow — lazygit, yazi, scratch shell
Uses function form to keep default drawer buttons and append popup triggers:
export default {
name: 'dev',
drawer: {
buttons: (defaults) => [
...defaults,
{
id: 'lazygit',
label: 'Git',
description: 'Open lazygit popup (prefix + g)',
action: { type: 'send', data: '\x02g' },
},
{
id: 'yazi',
label: 'Files',
description: 'Open yazi file manager popup (prefix + y)',
action: { type: 'send', data: '\x02y' },
},
{
id: 'scratch',
label: 'Scratch',
description: 'Open scratch shell popup (prefix + `)',
action: { type: 'send', data: '\x02`' },
},
],
},
}Requires matching tmux bindings (see references/tmux-basics.md popup section).
Guardrails
- Do not `import` from `'remobi'` — the CLI calls
defineConfig()internally, so configs just export a plain object. Usingimport { defineConfig } from 'remobi'fails when the config lives outside a project with remobi installed. - Never invent root keys. The validator rejects unknown keys with a path-based error.
- Use `drawer.buttons`, never `drawer.commands` — the latter was renamed and no longer works.
- `send` actions require `data` — omitting it fails validation.
- Non-`send` actions must not have `data` or `keyLabel` — validator rejects them.
- `floatingButtons` is an array of groups — wrap buttons in
{ position, buttons }. - `toolbar` has `row1` and `row2` — there is no
row3or flatbuttonskey on toolbar. - `mobile.initData` is
string | null— set tonullto disable, notfalseor''. - `reconnect` has only
enabled: boolean— defaults totrue. Set{ enabled: false }to disable. - `gestures.scroll` is an object, not a string — use
{ strategy: 'wheel' }or{ strategy: 'keys' }, never a bare'wheel'/'keys'string.
Validation
remobi serve --port 18765 -- /bin/trueA zero exit means the config is valid when the file is in the normal search path (current directory or ~/.config/remobi/).
For a custom location, validate explicitly:
remobi serve --config /path/to/remobi.config.ts --port 18765 -- /bin/trueAny error output means fix the reported paths before proceeding.
Common validation errors
| Error | Cause | Fix |
|---|---|---|
config.<unknown-key> | Invented or legacy root key | Remove it; only allowed root keys are valid |
config.drawer.commands | Old key name | Rename to drawer.buttons |
config.toolbar.buttons | Wrong toolbar shape | Use toolbar.row1 and/or toolbar.row2 |
| `action.type: expected 'send' \ | ...` | Wrong type string |
action.data: expected string, received undefined | send action missing data | Add data: '\x...' |
action.data: expected undefined | data on non-send action | Remove data from non-send actions |
floatingButtons[0]: expected object | Flat ControlButton[] | Wrap in group: { position: 'top-left', buttons: [...] } |
mobile.initData: expected string or null | false or 0 passed | Use null to disable, or a string to send |
Cannot find package 'remobi' | Config uses import ... from 'remobi' | Remove the import — export a plain object instead. The CLI calls defineConfig() internally |
gestures.scroll: expected Object, received string | Bare 'wheel' / 'keys' string | Use { strategy: 'wheel' } or { strategy: 'keys' } |
Hooks
Lifecycle hooks for remobi. Only reference this if the user asks about analytics, action filtering, custom DOM, or conditional behaviour.
Overview
Hooks are registered programmatically — they are not part of defineConfig(). Import createHookRegistry from 'remobi' and register handlers. Each hook receives typed context and runs asynchronously. Errors are caught and logged without stopping other hooks.
SendSource type
type SendSource = 'toolbar' | 'drawer' | 'floating-buttons' | 'mobile-init'Hook reference
| Hook | Fires when | Can modify? |
|---|---|---|
beforeSendData | Before terminal input is sent | Yes — return { block: true } to prevent, or { data: '...' } to rewrite |
afterSendData | After data is sent | No (observation only) |
overlayInitStart | Overlay initialisation begins | No |
overlayReady | Overlay fully initialised and wired | No |
toolbarCreated | Toolbar DOM mounted | No (but can modify DOM) |
drawerCreated | Drawer DOM mounted | No (but can modify DOM) |
Context interfaces
BeforeSendDataContext / AfterSendDataContext:
{
term: XTerminal
config: RemobiConfig
source: SendSource
actionType: ButtonAction['type']
kbWasOpen: boolean
data: string
}OverlayInitContext (overlayInitStart, overlayReady):
{
term: XTerminal
config: RemobiConfig
mobile: boolean
}ToolbarCreatedContext:
{ term: XTerminal, config: RemobiConfig, toolbar: HTMLDivElement }DrawerCreatedContext:
{ term: XTerminal, config: RemobiConfig, drawer: HTMLDivElement, backdrop: HTMLDivElement }Registration API
const hooks = createHookRegistry()
const { dispose } = hooks.on('beforeSendData', async (ctx) => {
console.log(`Sending ${ctx.data} from ${ctx.source}`)
return {}
})dispose() removes the handler.
Examples
Log all sent data:
hooks.on('afterSendData', async (ctx) => {
console.log(`[${ctx.source}] sent: ${JSON.stringify(ctx.data)}`)
})Block dangerous commands:
hooks.on('beforeSendData', async (ctx) => {
if (ctx.data.includes('rm -rf')) return { block: true }
return {}
})Add custom DOM to toolbar:
hooks.on('toolbarCreated', async (ctx) => {
const indicator = document.createElement('span')
indicator.textContent = 'LIVE'
indicator.style.color = '#a6e3a1'
ctx.toolbar.prepend(indicator)
})Keeping your Mac awake for remote access
When running remobi serve on a Mac to expose a terminal over the network (e.g. via Tailscale), the host going to sleep makes the terminal unreachable. This guide covers your options from quick to permanent.
Quick: --no-sleep flag (recommended)
remobi serve --no-sleepUnder the hood this runs caffeinate -s -w <pid> alongside ttyd. The sleep assertion is held exactly as long as the server runs and dropped automatically on shutdown — no stale state, no manual cleanup.
Combine with other flags as usual:
remobi serve --no-sleep --port 8080 -- tmux new -As devCaveats:
- Works on AC power only — battery Macs will still sleep when unplugged
- Does not prevent lid-close sleep — closing the lid still sleeps the Mac
- No system config changes required
- Non-macOS: the flag is silently ignored (caffeinate is macOS-only)
Persistent: system settings
For a Mac that should always be accessible (Mac mini, Mac Studio, headless Mac Pro), configure the OS to never sleep permanently.
System Settings GUI
System Settings → Energy → Prevent automatic sleeping when the display is off
Check this box and the Mac will stay awake indefinitely on AC power.
pmset (command line)
# Never sleep on AC power
sudo pmset -c sleep 0
# Display off after 10 min (saves energy, doesn't affect remote access)
sudo pmset -c displaysleep 10
# Wake on LAN (useful if you ever let it sleep)
sudo pmset -c womp 1
# Stay awake during remote SSH/tty sessions
sudo pmset -c ttyskeepawake 1
# Auto restart after power loss
sudo pmset -c autorestart 1Check current settings:
pmset -gnix-darwin (declarative)
power.sleep = {
computer = "never";
display = 10;
};This is equivalent to the System Settings checkbox above. For the extra pmset settings (WoL, ttyskeepawake, autorestart) add an activation script:
system.activationScripts.powerManagement.text = ''
/usr/bin/pmset -c womp 1
/usr/bin/pmset -c ttyskeepawake 1
/usr/bin/pmset -c autorestart 1
'';Caffeinate flags reference
caffeinate is the macOS command-line tool for holding power management assertions. --no-sleep uses -s -w <pid>.
| Flag | Prevents | Notes |
|---|---|---|
-i | Idle sleep | Weakest — system can still sleep from other triggers |
-s | System sleep | On AC power only. What --no-sleep uses |
-d | Display sleep | Keeps screen on — rarely useful for a headless server |
-w <pid> | (modifier) | Drop all assertions when the given PID exits |
You can also use caffeinate directly for ad-hoc situations:
caffeinate -s # stay awake until Ctrl-C
caffeinate -s -t 3600 # stay awake for 1 hour
caffeinate -s -- long-task # stay awake while long-task runsLid-close behaviour
None of the above options prevent sleep when the lid is closed. macOS sleeps on lid-close by design.
For a MacBook acting as a server, the options are:
- Clamshell mode — connect an external display, plug in power, close
the lid. macOS stays awake in this configuration.
- Amphetamine (free, Mac App Store) — can override lid-close sleep
independently of a display being connected.
For a desktop Mac (mini, Studio, Pro) lid-close is irrelevant.
Mobile pane navigation
Desktop tmux dashboards with 8-16 panes are effectively unusable on mobile — panes are squished to unreadable sizes. This guide shows how to navigate panes comfortably from your phone using remobi's built-in features.
The problem
When you swipe left/right in remobi, it sends \x02n / \x02p by default — next/previous window. That's fine for window switching, but it doesn't help when you have many panes in one window. On mobile you want to zoom a pane to full screen and cycle through them.
Double-tap zoom (recommended)
remobi has a built-in double-tap gesture that sends any escape sequence when you double-tap the terminal screen. Combined with auto-zoom on load and a floating zoom button, this gives you full pane control without any tmux config changes:
remobi config (~/.config/remobi/remobi.config.ts):
export default {
mobile: {
initData: '\x02z', // auto-zoom current pane on mobile load
},
gestures: {
doubleTap: {
enabled: true, // double-tap terminal to toggle zoom
},
},
floatingButtons: [
{
position: 'top-left',
buttons: [
{
id: 'zoom',
label: 'Zoom',
description: 'Toggle pane zoom',
action: { type: 'send', data: '\x02z' },
},
],
},
],
}Result:
- Phone loads -> current pane auto-zooms to full screen
- Double-tap terminal -> toggle zoom on/off
- Tap floating Zoom button -> toggle zoom
- Swipe left/right -> navigate windows (always, even when zoomed)
This workflow keeps swipe for window navigation and uses double-tap/button for zoom control. No tmux config changes needed.
Mobile init data
Use mobile.initData to send an arbitrary string to the terminal when remobi loads on a narrow viewport (below mobile.widthThreshold, default 768px). This runs once on page load.
Auto-zoom on load:
// remobi.config.ts
export default {
mobile: {
initData: '\x02z', // send prefix-z to zoom current pane
},
}The widthThreshold (default 768px) controls when initData is sent. Adjust if needed:
mobile: {
initData: '\x02z',
widthThreshold: 1024, // treat tablets in portrait as mobile too
},Floating buttons
Add floatingButtons to put always-visible quick-action buttons at a chosen position, visible on touch devices only. Each group specifies a position and a buttons array. Useful for a one-tap zoom button without opening the drawer:
export default {
floatingButtons: [
{
position: 'top-left',
buttons: [
{
id: 'zoom',
label: 'Zoom',
description: 'Toggle pane zoom',
action: { type: 'send', data: '\x02z' },
},
],
},
],
}Configurable swipe commands
Override what data swipe gestures send — useful if you prefer pane cycling over window switching:
export default {
gestures: {
swipe: {
left: '\x02]', // next pane (instead of next window)
right: '\x02[', // previous pane
leftLabel: 'Next pane',
rightLabel: 'Previous pane',
},
},
}The leftLabel/rightLabel values appear in the help overlay (? button -> Gestures section).
Advanced: extending gestures via tmux
remobi gestures send escape sequences — tmux interprets them. This means you can create "smart" gesture behaviour purely via tmux config, with no remobi changes needed. The binding decides what happens; remobi just sends the key.
Example: zoom-aware pane cycling
Override prefix n/prefix p in tmux so they behave differently depending on whether a pane is zoomed:
# ~/.config/tmux/tmux.conf
# When zoomed: cycle to next/prev pane and re-zoom
# When not zoomed: behave as normal window switching
bind -N "Next window (zoom-aware)" n \
if -F '#{window_zoomed_flag}' \
'select-pane -t :.+ ; resize-pane -Z' \
'next-window'
bind -N "Previous window (zoom-aware)" p \
if -F '#{window_zoomed_flag}' \
'select-pane -t :.- ; resize-pane -Z' \
'previous-window'With this binding:
- Not zoomed:
prefix n/prefix pswitch windows (default behaviour) - Zoomed:
prefix n/prefix pcycle to the next/previous pane and keep it zoomed
Swipe gestures in remobi still send \x02n/\x02p, so no remobi config change is needed — the tmux binding does all the work.
Trade-off: this prevents swiping between windows when zoomed. If you use double-tap zoom (recommended above), you probably want simple window navigation on swipe instead.
This pattern generalises — any tmux binding can add conditional logic to change what a remobi gesture does. For example, you could make swipe send different commands based on the current program (if -F '#{pane_current_command}').
General mobile tmux tips
See Mobile-friendly tmux config for a full guide on responsive status bars, popup sizing, zoom indicators, mouse mode, and binding ergonomics.
Mobile-friendly tmux config
remobi makes your browser the mobile client, but tmux itself can also adapt to narrow viewports. This guide covers making your tmux.conf respond to terminal width — so the same session works on a phone, a tablet, and a desktop without manual adjustment.
This is separate from pane navigation ergonomics, which are covered in Mobile pane navigation.
Responsive status bar
The biggest win. tmux format strings support conditionals via #{?condition,true,false}, and #{client_width} gives you the current terminal width. Nest these to build breakpoints.
status-left tiers
# ~/.config/tmux/tmux.conf
# status-left: three tiers
# >= 80 cols: session name + zoom flag
# >= 50 cols: abbreviated session name + zoom flag
# < 50 cols: zoom flag only
set -g status-left "#{?#{>=:#{client_width},80},#[bold] #{session_name} #{?window_zoomed_flag,[Z] ,},#{?#{>=:#{client_width},50}, #{=8:session_name} #{?window_zoomed_flag,[Z] ,}, #{?window_zoomed_flag,[Z] ,}}}"Reading nested conditionals is easier from the inside out:
#{?condition, true-branch, false-branch}
#{>=:A,B} — true when A >= B (numeric comparison)
#{=N:var} — truncate var to N charactersFor readability, split across lines with line continuation (tmux ignores trailing \ in set values):
set -g status-left \
"#{?#{>=:#{client_width},80},\
#[bold]#{session_name} #{?window_zoomed_flag,[Z] ,},\
#{?#{>=:#{client_width},50},\
#{=8:session_name} #{?window_zoomed_flag,[Z] ,},\
#{?window_zoomed_flag,[Z] ,}}}"Note: Examples here use plain text formatting. If you use Powerline glyphs or Catppuccin status modules, wrap the same #{?#{>=:...},...} logic around your existing strings.status-right tiers
status-right typically carries date, hostname, or load — content worth stripping on mobile. A shell script is easier to read than deeply nested conditionals for four tiers:
set -g status-right-length 100
set -g status-right "#(~/.config/tmux/status-right.sh #{client_width})"~/.config/tmux/status-right.sh:
#!/bin/sh
# Print a status-right string appropriate for the given terminal width.
# Usage: status-right.sh <width>
width=${1:-80}
if [ "$width" -ge 120 ]; then
# Full: date + time + hostname
printf ' %s %s ' "$(date '+%a %d %b')" "$(hostname -s)"
elif [ "$width" -ge 90 ]; then
# Medium: date + hostname
printf ' %s %s ' "$(date '+%d %b')" "$(hostname -s)"
elif [ "$width" -ge 60 ]; then
# Compact: hostname only
printf ' %s ' "$(hostname -s)"
else
# Mobile: nothing (preserve horizontal space)
printf ''
fiMake it executable: chmod +x ~/.config/tmux/status-right.sh
The shell script approach is easier to test independently and extend without counting brace depths.
Window tab truncation
Long window names — especially those that include a working directory — eat horizontal space. Use window-status-format to hide the path on narrow terminals:
# Wide: show name + path indicator
# Narrow (< 80): name only
set -g window-status-format \
"#{?#{>=:#{client_width},80}, #I:#W#F , #I:#{=10:window_name}#F }"
set -g window-status-current-format \
"#{?#{>=:#{client_width},80}, #[bold]#I:#W#F , #[bold]#I:#{=10:window_name}#F }"#W is the full window name. #{=10:window_name} truncates to 10 characters. Adjust the threshold and length to suit your names.
If your window names include directory paths (e.g. via automatic renaming), keep them short — or use set -g automatic-rename-format '#{b:pane_current_path}' to show only the basename.
Popup sizing
Fixed character sizes overflow narrow terminals. Use percentages instead:
# Good — scales to whatever the terminal is
bind f display-popup -h 95% -w 100% -E "fzf --some-flags"
# Fragile — overflows if terminal is narrower than 120 cols
bind f display-popup -h 40 -w 120 -E "fzf --some-flags"display-popup + fzf vs display-menu
display-menu renders a tmux-native menu at a fixed position. display-popup opens a full sub-terminal — you can run fzf, gum, or any interactive picker inside it.
On a phone soft keyboard, a popup that fills the screen is easier to tap accurately than a small overlaid menu. Prefer display-popup for session/window pickers, file browsers, and any command that benefits from a scrollable list.
Example — session picker:
bind s display-popup -h 50% -w 80% -E \
"tmux list-sessions -F '#{session_name}' | fzf --prompt='session: ' | xargs tmux switch-client -t"Session and window naming
Short, memorable names make the status bar readable at any width.
# Truncate long session names in the status bar
set -g status-left " #{=8:session_name} "
# Renumber windows when one is closed (avoids gaps like 1, 3, 4)
set -g renumber-windows on
# Use only the basename for auto-renamed windows
set -g automatic-rename-format '#{b:pane_current_path}'Aim for session names of 4–8 characters (e.g. dev, prod, infra, docs). You can always rename: prefix + $ for sessions, prefix + , for windows.
Zoom indicator
Always show #{window_zoomed_flag} in your status bar. On mobile you zoom frequently, and it is easy to forget whether you are in a zoomed single-pane view or a multi-pane layout.
# In status-left or status-right, add:
# #{?window_zoomed_flag,[Z] ,}
# e.g.
set -g status-left " #{session_name} #{?window_zoomed_flag,[Z] ,}"The zoom indicator should be visible at all width tiers — put it inside every branch of your #{?#{>=:...}} conditionals, as shown in the responsive status-left example above.
Binding ergonomics
Mobile soft keyboards typically cannot produce Alt+Shift+key or other multi-modifier combos. Prefer:
- `prefix + single-key` — works reliably from remobi via the Prefix button or a configured toolbar/drawer button.
- Single-key in copy mode — no modifier needed.
- remobi toolbar/drawer buttons — the right place for actions you want one-tap access to on mobile.
Move modifier-heavy bindings to remobi buttons rather than trying to send them from a soft keyboard. For example, if you have a binding like bind -n M-S-f ..., surface it as a remobi drawer button with action: { type: 'send', data: '\x02f' } (mapped to a simpler prefix binding instead).
Mouse mode
set -g mouse onMouse mode enables:
- Tap to focus — tap a pane to make it active
- Touch scroll — scroll terminal history with a swipe (works alongside remobi's own scroll handling)
- Drag to resize — drag pane borders to resize
This is the single highest-value setting for mobile use. Enable it unconditionally unless a specific workflow requires it off.
Summary checklist
| Setting | Command | What it does |
|---|---|---|
| Responsive status-left | set -g status-left "#{?#{>=:...},...}" | Strip content on narrow terminals |
| Responsive status-right | script or nested #{?} | Hide date/host on very narrow terminals |
| Zoom indicator | #{?window_zoomed_flag,[Z] ,} in status | Always shows zoom state |
| Window tab truncation | #{=N:window_name} in window-status-format | Prevents tab overflow |
| Popup sizing | -h 95% -w 100% in display-popup | Fills screen regardless of terminal width |
| Mouse mode | set -g mouse on | Tap-to-focus, touch scroll, drag resize |
| Renumber windows | set -g renumber-windows on | Keeps window list tidy |
| Short names | prefix + $ / prefix + , | Readable at any width |
Further reading
- Mobile pane navigation — zoom-aware swipe, auto-zoom on load, floating buttons
Deploying remobi with Tailscale Serve
Expose a tmux session as a mobile-friendly web terminal over your Tailscale network with full PWA support.
Prerequisites
- tmux installed
- Tailscale configured with HTTPS enabled (
tailscale cert) - remobi installed (
npm install -g remobi)
Quick setup (recommended)
1. Start remobi serve
remobi serveThis bundles the browser client in memory, starts remobi's built-in HTTP and WebSocket server, and spawns a local PTY-backed terminal session on :7681 with full PWA support.
By default remobi serve binds to 127.0.0.1, so it is not exposed on your LAN. Tailscale Serve is the thing that publishes it.
2. Expose via Tailscale Serve
tailscale serve --bg 7681Your terminal is now available at https://<your-machine>.<tailnet>.ts.net. If you publish remobi behind a path prefix instead of the root, start remobi with --base-path /that-prefix so the WebSocket and PWA URLs stay aligned with the external URL.
On mobile, tap Add to Home Screen for a standalone app experience with the remobi icon.
3. Stop
pkill -f "remobi serve"
tailscale serve --https=443 offTip — keep your Mac awake: Add --no-sleep so the Mac doesn't go tosleep while you're away:
>
```bash
remobi serve --no-sleep
```
>
See Keeping your Mac awake for persistent options (pmset,
nix-darwin) and lid-close caveats.
Shell function
# webtermup: expose tmux session via remobi serve + Tailscale serve
function webtermup() {
local session=${1:-main}
local port=${2:-7681}
pkill -f "remobi serve.*--port $port" 2>/dev/null
remobi serve --no-sleep --port $port -- tmux new-session -A -s "$session" &!
tailscale serve --bg $port
echo "Terminal ($session): https://$(tailscale status --self --json | jq -r '.Self.DNSName' | sed 's/\.\$//')"
}
# webtermdown: stop remobi serve and Tailscale serve
function webtermdown() {
local port=${1:-7681}
pkill -f "remobi serve.*--port $port" 2>/dev/null
tailscale serve --https=443 off 2>/dev/null
echo "Web terminal stopped"
}Legacy ttyd notes
Current remobi releases do not depend on ttyd. If you are migrating an older setup, move it to remobi serve rather than trying to preserve the old ttyd path.
The old ttyd docs in this repo are historical only and are not a supported setup path on current releases.
If you are tempted to run remobi serve --host 0.0.0.0, be explicit about the trade-off: that bypasses the localhost-only default and exposes terminal control directly on the bound network interface. Prefer keeping remobi on loopback and letting Tailscale handle reachability.
tmux basics
Starter guide for users with no existing tmux config. General audience, informed by best practices.
What is tmux?
tmux (terminal multiplexer) lets you run multiple terminal sessions inside one connection. Three key benefits:
1. Persistent sessions — your work survives disconnects. SSH in from your laptop, detach, pick up on your phone with remobi. The session keeps running. 2. Windows and panes — split your terminal into multiple views. Run your editor in one pane, tests in another, logs in a third — all visible at once. 3. Popup tools — launch tools like lazygit, file managers (yazi), or scratch shells in floating popups over your current work. One keypress to open, one to close.
Install tmux
- macOS:
brew install tmux - Debian/Ubuntu:
sudo apt install tmux - Fedora:
sudo dnf install tmux - Check:
tmux -V
Core concepts
Sessions — persistent workspaces that survive disconnects, network drops, and closing your terminal. Each session has one or more windows. Think of a session as a project workspace.
Windows — like browser tabs within a session. Each window has its own shell and can be split into panes. Switch between windows with prefix + number or next/previous.
Panes — splits within a window. View multiple terminals side by side. Zoom any pane to full screen with prefix + z (great on mobile).
Session workflow
tmux new -s dev # create named session
tmux ls # list sessions
tmux attach -t dev # reattach to session
tmux new-session -A -s dev # attach if exists, else create (what remobi serve uses)- Detach:
Ctrl-B d(keeps session running in background) - Kill session:
tmux kill-session -t dev
Essential keybindings (prefix = Ctrl-B)
| Keys | Action |
|---|---|
c | New window |
n / p | Next / previous window |
0-9 | Go to window N |
, | Rename window |
% | Split pane vertically |
" | Split pane horizontally |
z | Toggle pane zoom (full screen current pane) |
x | Kill pane (with confirm) |
[ | Enter scroll/copy mode (q to exit) |
d | Detach from session |
s | Session picker |
w | Window picker |
? | List all keybindings |
Recommended starter tmux.conf
Location: ~/.config/tmux/tmux.conf (XDG) or ~/.tmux.conf (legacy).
# -- Essentials ---------------------------------------------------------------
set -g mouse on # Click, scroll, drag pane borders
set -g renumber-windows on # No gaps after closing windows
set -g history-limit 50000 # Generous scrollback
set -g escape-time 10 # Near-instant Escape (0 can cause issues)
set -g focus-events on # Pass focus events to apps (neovim etc.)
set -g set-clipboard on # OSC 52 clipboard (works over SSH)
# -- True colour --------------------------------------------------------------
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",*256col*:Tc" # Enable true colour
# -- Status bar ----------------------------------------------------------------
set -g status-position top # Status at top (phone-friendly: thumb zone at bottom)
set -g status-interval 15 # Refresh every 15s
# -- Better splits (in current working directory) -----------------------------
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"
# -- Vi mode for copy (optional) ----------------------------------------------
setw -g mode-keys viYour first popup — keybinding help
tmux's display-popup lets you launch anything in a floating window. Start with a help popup that shows all your keybindings — no extra tools needed, works out of the box:
# Help popup — shows all keybindings in a floating window (press q to close)
bind ? display-popup -E -w 80% -h 80% "tmux list-keys | less"This overrides the default prefix + ? (which uses tmux's built-in pager) with a cleaner floating popup. Press q to close.
Once you see how popups work, you'll want more. Here are common tools that work great as popups:
# lazygit — git TUI in a floating popup
bind g display-popup -E -w 95% -h 95% -d "#{pane_current_path}" "lazygit"
# yazi — file manager popup
bind y display-popup -E -w 95% -h 95% -d "#{pane_current_path}" "yazi"
# scratch shell — ephemeral shell for quick commands
bind ` display-popup -E -w 80% -h 80% -d "#{pane_current_path}"
# system monitor (btm or htop)
bind b display-popup -E -w 95% -h 95% "btm"Key flags:
-E— close popup when command exits-w 95% -h 95%— use percentage dimensions (scales to phone screens)-d "#{pane_current_path}"— open in same directory as current pane
After adding popup bindings to tmux, add matching drawer buttons in your remobi config so you can trigger them from your phone.
Custom prefix (optional)
Some users prefer Ctrl-A (screen-style). If changing prefix, update remobi config too:
unbind C-b
set -g prefix C-a
bind C-a send-prefixPlugin management with tpm (optional)
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpmAdd to end of tmux.conf:
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible' # Sensible defaults
set -g @plugin 'tmux-plugins/tmux-resurrect' # Save/restore sessions across restarts
run '~/.tmux/plugins/tpm/tpm'Install plugins: prefix + I (capital i).
Agent watcher starter config
For users who primarily monitor coding agents (Claude Code, Codex, etc.) from their phone. Extends the base starter config with a zoom indicator and auto-rename. Pane zoom is handled by remobi's double-tap gesture — no special tmux bindings needed.
Add these to the starter config above:
# -- Zoom indicator in status bar ---------------------------------------------
set -g status-left " #{session_name} #{?window_zoomed_flag,[Z] ,}"
# -- Auto-rename windows to current directory ---------------------------------
set -g automatic-rename-format '#{b:pane_current_path}'Combined with remobi's double-tap zoom and mobile.initData: '\x02z' (auto-zoom on phone load), this gives agent watchers:
- Phone loads -> current pane auto-zooms to full screen
- Double-tap terminal -> toggle zoom on/off to inspect any pane
- Swipe left/right -> navigate between windows
[Z]in status bar always shows you're zoomed
See mobile-panes.md for advanced tmux patterns that extend gesture behaviour.
Tips for remobi users
set -g status-position topkeeps the status bar away from remobi's toolbar at the bottom- Mouse mode (
set -g mouse on) lets you click panes and scroll on your phone - Use
%dimensions indisplay-popup(e.g.95%x95%) not fixed char widths — they scale to phone screens - Zoom pane (
prefix + z) is your best friend on mobile — remobi can auto-zoom on connect viamobile.initData - See
mobile-tmux.mdfor responsive status bar breakpoints and mobile-specific optimisations - See
mobile-panes.mdfor pane workflows and advanced tmux patterns that extend gesture behaviour
Recommended ttyd flags
Historical reference for old ttyd-based remobi setups.
Legacy only: current remobi releases no longer usettyd,remobi servedoes not generate ttyd flags, andremobi buildis deprecated. Useremobi servefor supported setups. Keep this page only for understanding or migrating old pre-runtime installs.
Essential flags
| Flag | Purpose |
|---|---|
--writable | Allow input (without this, the terminal is read-only) |
--index <path> | Use the remobi-patched HTML |
-i 127.0.0.1 | Bind to localhost only (use a reverse proxy for external access) |
--port <n> | Port to listen on (default: 7681) |
Theme flags (-t)
Pass theme settings as -t key=value to avoid flash of unstyled terminal on load.
ttyd \
-t 'theme={"background":"#1e1e2e","foreground":"#cdd6f4","cursor":"#f5e0dc","cursorAccent":"#1e1e2e","selectionBackground":"#45475a","black":"#45475a","red":"#f38ba8","green":"#a6e3a1","yellow":"#f9e2af","blue":"#89b4fa","magenta":"#cba6f7","cyan":"#94e2d5","white":"#bac2de","brightBlack":"#585b70","brightRed":"#f38ba8","brightGreen":"#a6e3a1","brightYellow":"#f9e2af","brightBlue":"#89b4fa","brightMagenta":"#cba6f7","brightCyan":"#94e2d5","brightWhite":"#a6adc8"}' \
-t 'fontFamily="JetBrainsMono NFM, monospace"' \
-t 'scrollSensitivity=3' \
-t 'disableLeaveAlert=true' \
...Flag reference
| Flag | Default | Notes |
|---|---|---|
theme={...} | — | JSON object matching xterm.js theme properties |
fontFamily="..." | "courier-new" | Must match the font loaded by remobi config |
scrollSensitivity=N | 1 | 3 works well for mobile touch scrolling |
disableLeaveAlert=true | false | Prevents "Leave site?" prompts when navigating away |
Generating theme JSON
Use remobi's serialiseThemeForTtyd() to generate the theme string from your config:
node -e "import { defineConfig, serialiseThemeForTtyd } from 'remobi/config'; console.log(serialiseThemeForTtyd(defineConfig()))"Full example
ttyd -i 127.0.0.1 --port 7681 --writable \
--index dist/index.html \
-t 'theme={"background":"#1e1e2e","foreground":"#cdd6f4"}' \
-t 'fontFamily="JetBrainsMono NFM, monospace"' \
-t 'scrollSensitivity=3' \
-t 'disableLeaveAlert=true' \
tmux new-session -A -s main