nvdr lets you control a remote NVDA screen reader from a machine that isn't
running NVDA — a Linux box over SSH, a Mac, or an iPhone. It speaks the
NVDA Remote relay protocol as the master (controller): it hears what the
remote NVDA says and forwards your keystrokes back to it.
Disclaimer. This is a hobby project. I built it for personal use to see if the idea would work — nothing more. I'm aware of NVDA's native remote access and of the NVDA Remote controller app currently in TestFlight; this is not a replacement for either. What
nvdradds is (a) an SSH-bridged transport and (b) what I believe is better keyboard handling than that TestFlight app in some situations. Contributions are welcome — especially anything around security, but really anything at all.
This is a monorepo. Several independently-built components, different
languages, all speaking the same wire protocol (client_spec.md is the shared
contract):
| Path | What it is |
|---|---|
src/ + Cargo.toml |
The Rust terminal client. Also exposes nvdr --ipc, a line-oriented IPC bridge used by the apps below. |
addon/ |
An NVDA add-on (nvdrBridge) — lets a Windows NVDA use the SSH bridge. |
mac/ |
A native macOS SwiftUI app. |
ios/ |
An iOS SwiftUI app. |
android/ |
A native Android app (Kotlin + Jetpack Compose). |
The terminal client connects directly to the relay over TLS. The Mac app, iOS app, Android app, and NVDA add-on all go through the SSH bridge instead (see below).
The Mac app, the iOS app, the Android app, and the NVDA add-on do not open a
TLS connection to the relay themselves. They launch ssh and run nvdr --ipc
on a remote bridge box, and that machine dials the NVDA Remote relay:
your device (Mac / iOS / Android / Windows+NVDA)
│ SSH
▼
bridge box ── runs `nvdr --ipc` ── TLS ──▶ relay ──▶ remote NVDA
So to use the Mac app, iOS app, Android app, or add-on you need a reachable host
(Linux, macOS, or Windows) with the nvdr binary built and on its PATH, plus
SSH auth set up (the apps support a private key or a password).
This bridge is useful when you want to encrypt the whole remote session over SSH, or when you simply can't reach port 6837 (firewalled networks, captive Wi-Fi, etc.) — your only outbound connection is SSH.
- The Mac, iOS, and Android apps use the SSH bridge unconditionally — there is no way to turn it off. They will not connect to a relay directly. If you want a direct connection, use the Rust terminal client. A contribution that lets these apps dial the relay directly (no SSH bridge) would be very welcome — and I'll likely get around to it myself before long if there's demand for it.
- The bridge only works on the controller (master) side, not the host (slave) side. There is no way to make the machine being controlled run through this bridge — that machine still needs ordinary NVDA Remote.
- The bridge forwards keystrokes and speech only. Other NVDA audio — beeps, the forms-mode tone, progress-bar ticks, wave/tone effects — is not carried across, for now. You get speech text and that's it.
- Connecting NVDA's native remote access through this bridge is not implemented. If you'd like to add it, contributions are super appreciated.
A terminal client for the NVDA Remote relay. Designed to run on a VPS over SSH; works from any terminal on Linux, macOS, Windows, or iOS. This is the only component that talks to the relay directly (no bridge).
Requires Rust 1.75+.
cargo build --release # binary at target/release/nvdrDrop target/release/nvdr somewhere on your $PATH.
nvdr # localhost:6837, prompts for channel
nvdr --host relay.example.com --port 6837 --channel 123456789
nvdr --show-keys # dump the key reference and exitOn first connect nvdr pins the relay's TLS fingerprint (Trust On First Use)
into ~/.config/nvdr/known_hosts; later connects verify against it. Relay
certs are usually self-signed — that's expected. A cert change drops you into
an interactive re-pin prompt.
Type, and printable characters, arrows, F-keys, Tab/Enter/Esc/Backspace, and
any Ctrl+letter are forwarded to the remote NVDA — even Ctrl+C. Speech
prints as plain text lines (no ANSI color, screen-reader friendly).
A terminal can't send Insert or the Windows key, so there's a leader key
(default Ctrl+G, set with --leader):
| Shortcut | Sends | Meaning |
|---|---|---|
<leader> c |
Alt+F4 | close window |
<leader> w |
NVDA+T | read window title |
<leader> d |
Win+D | show desktop |
<leader> t |
Alt+Tab | switch window |
<leader> <leader> toggles a sticky NVDA modifier; <leader> : opens a
command prompt (:k win+r, :caps, :say <text>, :help, :quit, …).
There's also a one-shot scripting mode (-k / -s) — see
scripting.md.
CapsLock as a key, the Windows key alone, left/right modifier distinction, a
modifier pressed alone, most numpad keys. Use :k win+... for the Windows key.
nvdrBridge lets a Windows machine running NVDA reach a remote NVDA
through the SSH bridge instead of NVDA Remote's normal direct connection —
again, for SSH encryption or when port 6837 is blocked.
- Install: zip the contents of
addon/(somanifest.iniis at the archive root), rename tonvdrBridge.nvda-addon, install via NVDA's Add-on Store ("Install from external source"), restart NVDA. - Configure under NVDA → Preferences → Settings → nvdr Bridge (SSH host / user / key, relay host, channel key).
NVDA+F11toggles key forwarding between local and remote. While forwarding is on, every keystroke (exceptNVDA+F11itself) goes to the remote slave and the remote NVDA's speech is spoken on your local synth.
This is the controller side only — see the bridge caveats above. Full setup
and troubleshooting: addon/README.md.
A native SwiftUI app that bridges your Mac to a remote NVDA through the SSH
bridge. It installs a system-wide low-level keyboard hook
(CGEventTap + IOHIDManager).
NVDA+F11(e.g.CapsLock+F11) toggles forwarding on and off.- While forwarding is on, the app eats all keystrokes — including
Cmd+Q,Cmd+Tab, and other system shortcuts. They are sent to the remote machine, not handled by macOS. Turn forwarding off (NVDA+F11) to get your keyboard back. - The app requires Accessibility and Input Monitoring permissions to run the keyboard hook. It detects when they're missing and shows a banner with buttons that jump straight to the relevant System Settings pane.
- It is a Developer ID / non-sandboxed app — the keyboard hook can't work in the App Store sandbox.
A SwiftUI app that bridges an iPhone/iPad to a remote NVDA through the SSH bridge.
NVDA+F11toggles forwarding on and off, same as the Mac app.- While forwarding is on, the app captures all hardware-keyboard input — system combos included — and sends it to the remote machine.
A native Kotlin / Jetpack Compose app that bridges an Android device with a hardware (Bluetooth/USB) keyboard to a remote NVDA through the SSH bridge. Speech from the slave is spoken on the phone via Android Text-to-Speech.
NVDA+F11(e.g.CapsLock+F11) toggles forwarding on and off, same as the Mac and iOS apps.- Capture is focused-only by default — like the iOS app, it intercepts keys while nvdr is in the foreground, and releases any held keys on the slave when the app is backgrounded.
- Optionally, a system-wide capture path via an
AccessibilityServicegrabs combos Android would otherwise intercept first (Alt+Tab, the Meta/Windows key). It's the Android analog of the Mac app's system-wide hook — opt-in, and without it the app still does focused-only capture. - Needs Android 8.0 (API 26) or newer.
Full setup and configuration: android/README.md.
The two Swift apps use xcodegen-generated Xcode projects:
cd mac # or: cd ios
xcodegen generate
xcodebuildThe Android app is a Gradle project — needs JDK 17 and the Android SDK
(compileSdk 36):
cd android
./gradlew assembleDebug # → app/build/outputs/apk/debug/app-debug.apk
./gradlew installDebug # build + install on a connected device (needs adb)None of these apps will be published on the App Store / Play Store (the Mac one can't be sandboxed; the others aren't headed there either). Build them yourself from source.
This started as a personal experiment, so there's plenty of room to improve it. Some things to look at— security review and hardening especially, but also better protocol coverage (carrying NVDA's non-speech audio, wiring NVDA's native remote access through the bridge), more keyboard handling, docs, anything. Open an issue or a PR.
client_spec.md— the wire protocol this project implements.scripting.md— the-k/-sone-shot scripting grammar.~/.config/nvdr/known_hosts— TLS fingerprint cache for the terminal client.
Same as the NVDA Remote project.