mobux
tmux on your phone. Access your tmux sessions from a mobile browser over Tailscale with HTTPS.
Features
- Full terminal — xterm.js v6 with scrollback, colors, and links
- Touch-native input — bottom input bar with control key ribbon (^C, arrows, backspace, etc.) and native text field with autocomplete/voice support
- Two send modes — keyboard Enter executes; ▶ button injects text without Enter for readline editing
- Attach any file — 📎 button uploads anything (gallery photo, video, audio, PDF, logs…) and injects the saved path into the terminal
- Audio record — 🎤 button records mic audio in-browser and uploads it; transcribe it locally with
make transcribe FILE=<path>(offline whisper.cpp —make setup-transcribeto install). No on-device STT bundled in the binary; nothing leaves the host - Swipe gestures — swipe sessions to rename/kill, swipe terminal to switch tmux windows, pinch to zoom, long-press for tmux commands
- Session management — create, rename, kill sessions from a mobile-native home screen
- Secure — self-signed TLS by default, HTTP Basic auth with PIN
- PWA — installable as a home screen app
- TWA —
make twabuilds a signed Android APK that mobux serves at/install; install once, get a real launcher icon and OS-level push notifications - Push on bell — terminal
\x07(BEL) fires a Web Push notification to every subscribed device, deep-linked back to the source session
Quick Start
# Prerequisites: Rust, Node.js, tmux
Setup scripts
Two idempotent setup scripts provision everything needed to build mobux and
(optionally) the Trusted Web Activity APK. Both prefer user-local installs and
never use sudo. Run them as many times as you like — they detect existing
tools and skip them.
| Script | Make target | What it installs |
|---|---|---|
bin/setup |
make setup |
Rust toolchain (rustup, cargo, rustc), clippy, rustfmt, and npm install for the web build. After this, cargo build and node web/build.js work. |
bin/setup-twa |
make setup-twa |
TWA build toolchain: JDK 17 via SDKMAN (~/.sdkman), Node LTS via nvm (~/.nvm), Android command-line tools (~/.android/cmdline-tools/latest), platform-tools / build-tools / android-34, and @bubblewrap/cli (npm prefix ~/.local). Accepts SDK licenses non-interactively. |
bin/setup-twa prints PATH/env hints at the end — paste them into your
shell rc so the user-local tools are visible in new shells.
Set auth credentials via environment:
Architecture
┌──────────────┐ WebSocket ┌──────────────┐
│ Phone │◄──────────────────►│ mobux (Rust) │
│ xterm.js │ /ws/:session │ axum + PTY │
│ input-bar │ │ tmux attach │
│ touch.js │ REST API │ │
│ │◄──────────────────►│ /api/* │
└──────────────┘ └──────────────┘
- Server: Rust (axum) — serves the frontend (embedded in the binary), WebSocket terminal proxy, REST API for session/pane management, file upload
- Client: vanilla JS modules — xterm.js (patched, bundled with esbuild), touch gesture recognizer, mobile input bar
- Build:
node web/build.jsapplies a diff patch to xterm.js'sCompositionHelperand bundles with esbuild
Embedded frontend / cargo install
The entire web/static tree is compiled into the binary with rust-embed and served from memory at /static/* — there is no runtime dependency on a web/ directory next to the executable. This makes cargo install mobux produce a self-contained binary that runs from anywhere.
For this to work, the generated vendor bundles (web/static/vendor/*.bundle.js, xterm.css, fonts/*.woff2) are committed to git so the published crate embeds the real frontend. They are build artifacts — regenerate them with make build (which runs node web/build.js) whenever the frontend or its dependencies change, then commit the result. Only the dev-only source maps (*.map) stay gitignored and are excluded from the embed.
Patched xterm.js
xterm.js has known issues with mobile keyboard input. We apply a source-level patch (patches/xterm-composition-helper.patch) that fixes:
- Composition-based autocomplete — mobile keyboards replace text via composition, not
insertReplacementText. The original code computes wrong substring offsets. - Broken diff algorithm —
_handleAnyTextareaChangesusedString.replace()which fails when autocorrect changes characters. - Backspace flood — textarea clearing on Enter caused massive backspace sequences.
See issue #9 for the full investigation.
Mobile Input
On mobile, the xterm.js hidden textarea is bypassed. Instead:
- Input bar appears on double-tap with a scrollable control key ribbon and a native text input
- Keyboard Enter sends text + carriage return (executes)
- ▶ button sends text without Enter (injects into readline for further editing)
- Ribbon keys send control sequences directly (^C, arrows, Tab, Esc, etc.)
- 📷 button uploads images to
/tmp/mobux-uploads/and sends the path to the terminal
API
| Endpoint | Method | Description |
|---|---|---|
/api/sessions |
GET | List tmux sessions |
/api/sessions |
POST | Create session {"name": "..."} |
/api/sessions/:name/kill |
POST | Kill session |
/api/sessions/:name/rename |
POST | Rename session {"name": "..."} |
/api/sessions/:name/panes |
GET | List panes/windows |
/api/sessions/:name/command |
POST | Run tmux command |
/api/sessions/:name/history |
GET | Capture scrollback |
/api/upload |
POST | Upload file (multipart) |
/api/push/vapid-public-key |
GET | VAPID public key (base64url) for client pushManager.subscribe |
/api/push/subscribe |
POST / DELETE | Register / unregister a Web Push subscription |
/api/push/devices |
GET | List subscribed devices |
/ws/:name |
WS | Terminal WebSocket |
/install |
GET | Self-service install page (CA cert, APK, QR codes) — no auth |
/install/mobux.apk |
GET | Built APK download — no auth |
/install/mobux-ca.crt |
GET | Local CA cert for Android trust store — no auth |
/.well-known/assetlinks.json |
GET | Digital Asset Links file proving the APK owns the domain — no auth |
Development
On a host already running mobux as a service (see DEPLOY.md), don't use
make run/make start/make restart— they bind:5151directly and collide with the systemd service. Run dev builds on a different port (make smoke-start→:8281, or an installable dev instance on:5152— see DEPLOY.md).
Deploying
mobux is a single self-contained binary (the frontend is embedded), so
deployment is just cargo install. The production instance runs as a
boot-persistent systemd user service. Full runbook — install, the service
unit, releasing to crates.io, and running an isolated dev instance — is in
DEPLOY.md.
Building the TWA
mobux ships its own Trusted Web Activity APK so you can install it as a real Android app (full-screen, no browser chrome, push notifications). The APK is built per-domain — different domain, different APK.
Prereqs: run bin/setup-twa once (installs JDK 17, Node, Android cmdline-tools,
@bubblewrap/cli). Then a single command builds everything:
This will:
- Generate a signing keystore at
~/.config/mobux/twa-signing.keystoreon first run (random password written to~/.config/mobux/twa-signing.password, mode 0600). Override the password viaMOBUX_TWA_KEYSTORE_PASSWORD, override the config dir viaMOBUX_CONFIG_DIR. - Render
twa/twa-manifest.jsonfrom the template with your domain. - Bootstrap the bubblewrap project skeleton via
node twa/init.js, which calls@bubblewrap/coredirectly with the rendered manifest. The bundledbubblewrap initCLI is interactive-only and treats--manifest=as a remote PWA manifest URL — neither fits a one-command build. - Run
bubblewrap build(passwords passed asBUBBLEWRAP_KEYSTORE_PASSWORD/BUBBLEWRAP_KEY_PASSWORDenv vars). - Copy the signed APK to
web/static/install/mobux.apk. - Write
web/static/.well-known/assetlinks.jsonwith the cert fingerprint so Android trusts the TWA → domain link.
The build needs mobux running with TLS during the icon-fetch step (bubblewrap
fetches iconUrl from the live server). If you use mobux's own self-signed CA,
set NODE_EXTRA_CA_CERTS=$HOME/.config/mobux/ca.crt before running make twa
(the target sets it automatically when the file exists).
BACK UP YOUR KEYSTORE. ~/.config/mobux/twa-signing.keystore (and the
matching password file) are the only thing standing between you and a broken
upgrade path: lose the key and existing installs cannot upgrade — only fresh
install with a new package will work, and the package id io.github.mvhenten.mobux
is then burned for those users.
Install on a phone
Once the APK is built, the rest of the install is self-service from the phone.
- On the phone, open
https://<your-mobux-host>/installin Chrome. (You'll get a "Not secure" warning until step 2 completes — tap through.) - Install the CA certificate first. Tap "Download CA certificate", then
open Android Settings → Security & privacy → Encryption & credentials →
Install a certificate → CA certificate → pick
mobux-ca.crtfrom Downloads. The page has the exact menu path; follow the steps in order. - Reload
/install— the address bar padlock should go green. - Tap "Download APK", install. The Mobux app appears in your launcher.
- Open the app, attach to a session, tap 🔔 in the input bar, accept the notification permission.
- Test it: from any session, run
echo -e '\a'with the phone locked — the lock screen lights up withsession N: 🔔and tapping it opens the session.
For a desktop → phone handoff (e.g. you're configuring a fresh phone from
your laptop), /install shows a QR code next to each download button. Scan it
with the phone camera to jump straight to the right URL.
If you're running mobux behind a publicly-trusted cert (Let's Encrypt — set
MOBUX_ACME_DOMAINS and MOBUX_ACME_EMAIL), the CA install step is skipped
entirely; the install page won't even render that section.
Third-Party Code
mobux includes vendored code from third parties. Full attribution and license texts are in THIRD_PARTY-LICENSES.md.
⚠️ License Compliance Notice: The current terminal emulator stack (web/static/vendor/aceterm/) is vendored from the now-discontinued AWS Cloud9 SDK (c9/core). This code is licensed under the Cloud9 SDK Non-Commercial License Agreement, which is incompatible with mobux's MIT license. These files are scheduled for replacement by @kattebak/sterk, a clean-room MIT-licensed terminal emulator. See the tracking issue for migration progress: #68
Other vendored components:
- Ace Editor (
web/static/vendor/ace.js) — BSD-3-Clause ✅ - wc.js (
web/static/vendor/aceterm/wc.js) — Permissive (Markus Kuhn's public-domain-like license) ✅
License
MIT