Control your terminal from your phone. Run a lightweight server on your Mac or Linux machine, scan a QR code, and get a full interactive shell on your iOS device over LAN.
┌──────────────┐ WebSocket (LAN) ┌──────────────────┐
│ iPhone │ ◄──────────────────────► │ Mac / Linux │
│ │ │ │
│ xterm.js │ JSON protocol over WS │ PTY sessions │
│ terminal │ ◄──────────────────────► │ (zsh/bash/...) │
│ in WKWebView│ │ │
└──────────────┘ └──────────────────┘
iOS App bash-mirror CLI
- Start
bash-mirroron your computer — it launches a WebSocket server and displays a QR code - Open the iOS app and scan the QR code (or enter the IP/token manually)
- The app authenticates with a one-time token and creates a PTY session
- You get a fully interactive terminal on your phone, rendered with xterm.js
| Crate | Purpose |
|---|---|
| tokio | Async runtime |
| tokio-tungstenite | WebSocket server |
| portable-pty | Cross-platform PTY management |
| ratatui + crossterm | TUI dashboard |
| rcgen + rustls | Self-signed TLS certificate generation |
| clap | CLI argument parsing |
| qr2term | QR code display in terminal |
| Technology | Purpose |
|---|---|
| SwiftUI | Native UI framework |
| WKWebView + xterm.js | Terminal rendering |
| AVFoundation | QR code scanning |
| URLSessionWebSocketTask | WebSocket client |
- Rust 1.70+ — Install
- Xcode 15+ with iOS 26.0 SDK (for the iOS app)
- XcodeGen (optional, for regenerating the Xcode project) —
brew install xcodegen
# Clone the repository
git clone https://github.com/user/bash-mirror.git
cd bash-mirror
# Build
cargo build --release
# Install to ~/.cargo/bin (makes `bash-mirror` available globally)
cargo install --path crates/bash-mirror-cli --lockedcd ios/BashMirror
# Regenerate Xcode project (if needed)
xcodegen generate
# Open in Xcode
open BashMirror.xcodeprojSet your development team in Xcode signing settings, then build and run on a physical device (camera required for QR scanning).
# Default: TUI dashboard, random port, uses $SHELL
bash-mirror
# Specify port and shell
bash-mirror --port 8080 --shell /bin/bash
# Headless mode (no TUI, prints connection info to stdout)
bash-mirror --no-tui
# Verbose logging
bash-mirror --verbose
# Custom token expiry (default: 300s)
bash-mirror --token-ttl 600When running with the TUI (default), the dashboard shows:
- Connection info — IP, port, WebSocket URL, and auth token
- Connected devices — currently paired phones
- Active sessions — running PTY sessions with status
- Log — timestamped event stream
Keyboard shortcuts:
| Key | Action |
|---|---|
q |
Quit |
r |
Regenerate auth token |
d |
Disconnect all devices |
- Open the BashMirror app
- Scan QR — point the camera at the QR code shown in the terminal
- Or connect manually — enter the IP address, port, and token displayed by the server
Once connected, you have a full interactive terminal. The app supports:
- Touch-based text selection
- Custom command pad with common shortcuts
- Full keyboard input
- Terminal resize (adapts to screen orientation)
Communication uses JSON messages over WebSocket with a "type" discriminator:
Client Server
│ │
│──── Auth { token } ──────────►│
│◄─── AuthOk { device_id } ─────│
│ │
│──── SessionCreate ───────────►│
│◄─── SessionCreated { id } ────│
│ │
│──── Input { session, data } ─►│
│◄─── Output { session, data } ─│ (base64-encoded)
│ │
│──── Resize { cols, rows } ───►│
│──── Ping { timestamp } ──────►│
│◄─── Pong { timestamp } ───────│
│ │
│──── SessionClose { id } ─────►│
│◄─── SessionClosed { id } ─────│
bash-mirror/
├── crates/
│ ├── bash-mirror-proto/ # Shared protocol types (no async deps)
│ ├── bash-mirror-core/ # PTY sessions, auth, TLS, session management
│ └── bash-mirror-cli/ # Binary: WebSocket server + TUI dashboard
└── ios/
└── BashMirror/ # SwiftUI iOS app
├── Views/ # SwiftUI views (scanner, terminal, keyboard)
├── Services/ # WebSocket + connection management
├── Models/ # Protocol message types
└── Resources/ # xterm.js, terminal.html, CSS
- One-time tokens — auth tokens are consumed on first use and expire after a configurable TTL (default 5 minutes)
- LAN-only — designed for local network use; binds to
0.0.0.0but intended for trusted networks - TLS ready — self-signed certificate generation is built in (rcgen + rustls), certificate fingerprint is included in the QR payload for pinning
- No data persistence — no session data is stored on disk; everything lives in memory
MIT

