Every `serve` bound a random port, so a restart left each phone holding addresses that no longer answered. Where the n0 relays are unreachable, which is common behind the Great Firewall, the phone had no other way to find the gateway until it was paired again. - `serve` listens on the same UDP port every time. It picks one on first run, keeps it in `<config dir>/mobile/port`, and moves only if the port is taken. IPv6 binding may fail, as in iroh's own defaults. - Both ends add mDNS lookup (`iroh-mdns-address-lookup`, service `_tty7._udp`). On the same network a phone finds the gateway by key when its addresses are stale, such as after a new DHCP lease or a new IPv6 prefix. The gateway advertises and the phone only listens. If multicast is refused, each side starts without it and says so, rather than failing. - iOS: `Info.ios.plist` declares the local-network use and the Bonjour service, without which iOS blocks multicast. - An ignored test (`--test mdns`) connects by key alone over mDNS, for a machine that allows multicast.
tty7 mobile
Watch and drive your desktop's tty7 panes and agents from a phone.
phone (Tauri: WebView + Rust) ──iroh──▶ tty7-gateway ──local sockets──▶ tty7 server
xterm.js ◀─ raw bytes ─ tty7-mobile-client (crates/tty7-gateway)
- Transport: iroh. The phone dials the desktop by public key. Connections hole-punch to a direct path when the network allows and fall back to a relay when it doesn't. Either way they're end-to-end encrypted. No port forwarding, no VPN.
- Protocol:
crates/tty7-mobile-proto. The phone never speaks the daemon's own protocols. The gateway exposes a small vocabulary: pair, a live tree of workspaces, tabs, panes and agent status, and one stream per open pane. - Panes are observed, not attached. A phone never resizes a pane or takes it away from
the desktop window showing it. Keystrokes go in beside the observer (
SendInput). The terminal keeps the desktop's size, and the app shrinks the font to fit the width. - Auth: pairing with a one-time code, valid 10 minutes, single use. After that the
gateway admits only the phone keys on its device list (
<config dir>/mobile/devices.json).
Run it
On the desktop, next to a running tty7:
cargo run -p tty7-gateway -- serve # keep this running
cargo run -p tty7-gateway -- pair # prints a QR code + a tty7pair:… code
cargo run -p tty7-gateway -- devices # paired phones; `revoke <name>` removes one
The app on the desktop (fastest loop)
Tauri builds the same app for macOS, which is the quickest way to work on the UI:
cd mobile
npm install
npm run tauri dev
Paste the tty7pair: code and tap Pair.
iOS
Needs the full Xcode, not just the Command Line Tools, plus an Apple developer account to run on a device.
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
cd mobile
npm run tauri ios init # once: generates src-tauri/gen/apple
npm run tauri ios dev # simulator, or pick a connected device
Android
Needs Android Studio's SDK and NDK, with ANDROID_HOME and NDK_HOME set.
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
cd mobile
npm run tauri android init
npm run tauri android dev
Without a phone
crates/tty7-mobile-client/examples/probe.rs is a phone in a shell. It pairs, prints the
tree, and times keystroke echo on a pane:
cargo run -p tty7-mobile-client --example probe -- pair '<code>'
cargo run -p tty7-mobile-client --example probe -- tree
cargo run -p tty7-mobile-client --example probe -- type 1 'echo hi'
Finding the computer again
A pairing code carries the gateway's addresses at the time it was made. Those
stay good across restarts: serve listens on the same UDP port every time
(<config dir>/mobile/port, a fresh one only if it is taken). When they go stale
anyway — a new DHCP lease, a new IPv6 prefix — the phone looks the gateway up by
key: through n0's relay and DNS where those are reachable, and by mDNS
(_tty7._udp) on the same local network where they are not.
mDNS needs the OS's local-network permission on both ends. On macOS that belongs
to whatever launched the gateway (your terminal), and it answers "Allow … to find
devices on local networks" once. On iOS the app declares it in
src-tauri/Info.ios.plist. Without it, known addresses and the relay still work.
Not done yet
- QR scanning. The app takes a pasted code for now. Next step is
tauri-plugin-barcode-scanneron mobile. - Push notifications when an agent starts waiting. The gateway sees the status change, but APNs/FCM delivery needs a small native plugin and a push relay.
- Keychain / Keystore for the phone's key. It lives in the app's private data directory today.
- Remote machines. The gateway serves the local machine's panes. Panes the desktop reaches over SSH aren't exposed yet.
- Take over. Attaching a pane at the phone's size, for when the desktop isn't in use.
- A self-hosted iroh relay for production, instead of n0's public ones.