Control your Mac from your Android phone with one hand, while the other arm is busy holding a sleeping baby.
Your phone becomes a trackpad + dictation mic for the Mac plugged into the TV. No app install, no account, no cloud relay. Open a web page on your phone, and you're in.
phone (browser) --- Wi-Fi / WebSocket ---> Mac (Python server) ---> Quartz mouse/keyboard events
After a feed, a newborn sleeps on you, and you're pinned to the couch for up to two hours with exactly one hand free. That hand has a phone in it. The Mac is across the room on the TV. onehand bridges the gap with the two things you actually need:
- A mouse (trackpad with tap, right-click, scroll, drag)
- Dictation (speak into the phone, text appears on the Mac)
- (Bonus) A keyboard, when you really need it
- Trackpad: 1-finger move, tap to click, 2-finger scroll, 2-finger tap right-click, double-tap-and-hold to drag
- Dictation: continuous speech recognition in the phone browser; final text is typed into whatever app has focus on the Mac
- Typing: a text box plus Enter / Backspace / Esc keys
- Security: LAN-only, per-session random PIN, self-signed TLS
- Zero phone install: any modern mobile browser (Chrome on Android recommended for dictation)
- macOS 12+ (uses Quartz CGEvents via pyobjc)
- Python 3.10+
- Phone and Mac on the same Wi-Fi network
- Android: Chrome (Web Speech API powers dictation)
- Mac: clone the repo, then
pip install -r requirements.txt && ./scripts/build_mac_app.shand opendist/onehand.app. A ✋ icon appears in the menu bar with Start/Stop, Copy link, a QR code for pairing, and Start at login. - Android: install
onehand.apkfrom the latest release (sideload; enable "install unknown apps" for your browser). Open it, type the address shown on the Mac, tap Connect. It remembers the Mac and grants the mic automatically.
No app needed. Open the server URL in Safari, accept the certificate warning, then Share -> Add to Home Screen for a fullscreen app-like icon.
- Trackpad, buttons, and typing work in Safari.
- Dictation: iOS Safari's speech recognition can be unreliable in continuous mode. If the 🎤 button misbehaves, use the ⌨️ panel and tap the mic on the iOS keyboard instead. Apple's dictation fills the box and Send pushes the text to the Mac.
git clone https://github.com/jonathanpopham/onehand.git
cd onehand
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# one-time: generate a self-signed cert (dictation requires HTTPS)
./scripts/make_certs.sh
python server.pyThe server prints something like:
[onehand] open on your phone: https://192.168.1.42:8741/?pin=7261
- Open that URL on your phone (same Wi-Fi).
- Accept the one-time self-signed-certificate warning (Advanced -> Proceed).
- Grant microphone permission when you tap Dictate.
macOS silently drops synthetic input events from untrusted processes. Grant Accessibility access to the app you run the server from:
System Settings -> Privacy & Security -> Accessibility -> enable your terminal app (Terminal, iTerm2, etc.), then restart the server.
The server warns at startup if it is not trusted.
| Gesture | Action |
|---|---|
| 1-finger drag | move cursor |
| tap | left click |
| 2-finger tap | right click |
| 2-finger drag | scroll |
| double-tap, hold, drag | drag (text select, window move) |
| 🎤 Dictate | continuous dictation, tap again to stop |
| ⌨️ | show text box for typing |
Dictation flow: speech is recognized on the phone (Chrome's built-in speech recognition) and only the final text is sent over the WebSocket, where it is typed into the focused Mac app. Interim results are shown on the phone so you can see it tracking.
Prefer to run recognition on the Mac instead (Whisper, macOS dictation,
anything that reads a mic)? See docs/alternatives.md for the
audio-streaming route and other trade-offs.
- The server binds to your LAN. Anyone on your network with the URL still needs the 4-digit PIN, which rotates every time you restart the server.
- TLS is self-signed; the point is to satisfy the browser's secure-context requirement for mic access, not to defeat a hostile network.
- Don't run this on a network you don't trust. There is no rate limiting, no user accounts, and the PIN is 4 digits.
- Pass
--pin XXXXto fix the PIN,--httpto skip TLS (mouse-only).
server.py aiohttp web + WebSocket server
input_mac.py Quartz event synthesis (mouse, scroll, keys, unicode typing)
menubar.py macOS menu bar app (rumps): start/stop, QR pairing, login item
static/index.html the entire phone UI (vanilla JS, no build step)
android/ Android WebView app (pairing screen + mic permission handling)
scripts/make_certs.sh self-signed cert generator
scripts/build_mac_app.sh build onehand.app bundle
scripts/build_android.sh build the APK
MIT. See LICENSE.