wire
One line per keystroke
Everything omni sends is newline-delimited JSON: one versioned object per line, the same bytes for an Android keyboard, an iPhone extension, a watch and a desktop daemon. It is small enough to read by eye and to write by hand.
Commands
{"v":1,"type":"text","text":"hello"} {"v":1,"type":"key","key":"enter"} {"v":1,"type":"combo","combo":"ctrl,a,g"} {"v":1,"type":"scroll","amount":-3}
type | Field | Means |
|---|---|---|
text | text | Type this string |
key | key | Press one named key: backspace, delete, enter, tab, esc, space, left, right, up, down, home, end |
combo | combo | A chord: modifiers then keys, comma-separated, such as ctrl,a,g |
scroll | amount | Scroll by this many steps; negative scrolls back |
Each line carries v, type and exactly one payload field; absent fields are omitted. The definitions are a byte-for-byte mirror of the Swift source used by the iPhone and watch apps. The schema does not reject unknown key names, so new keys stay forward-compatible; mapping a name to a native key is each receiver's job.
From wire to native keys
On Android, omni delivers commands to the omni keyboard as broadcasts of the com.harryaskham.omni.INJECT action, either over adb from another machine or locally with the termux transport when omni runs on the phone itself. Desktop backends map the same names to their own key codes.
Application events
Some inputs should not be keystrokes at all: a button on a key grid that an app binds to an action, for example. Events travel the same routes but are never injected:
omni listen --events --daemon 127.0.0.1:28766 &omni send --relay 127.0.0.1:28765 --device quill event c100_grid_3_4 down[{"endpoint":"quill","delivered":1,"error":null}] omni send --relay 127.0.0.1:28765 --device quill event c100_grid_3_4 up[{"endpoint":"quill","delivered":1,"error":null}] # omni listen --events{"v":1,"type":"event","name":"c100_grid_3_4","phase":"down"} {"v":1,"type":"event","name":"c100_grid_3_4","phase":"up"}
Applications usually publish through a named target, omni --target NAME send event …, so routing stays in the configuration and the bindings stay in the receiving application.
- A phase is
down,up,triggerorcancel, and the name stays the same between down and up. - Names are 1 to 128 ASCII letters, digits or
_.:-. An optionalsourceand application-defined JSONdataare allowed, up to 8 KiB.datais never treated as a command. - An event named
enteris still not the Enter key. - Relays forward events only to registrations that advertise
events: true, so older devices never receive an unexpected payload. A send reports how many recipients took each event.
Locally, omni listen --events subscribes separately from keystroke listeners: in the session above, the keystroke subscriber saw no events, and the event subscriber saw no keystrokes.
Relay frames
The relay protocol is the same NDJSON over TCP, with a few control frames:
| From → to | Frame |
|---|---|
| device → hub | {"v":1,"op":"register","device":"id","platform":"p","token":"…"}, where apps send ios, watchos, wearos or android and daemons linux, macos or windows |
| sender → hub | {"v":1,"op":"cast","device":"id|all","cmd":{…},"token":"…"} |
| hub → device | a bare command line |
| hub → sender | {"v":1,"op":"result","device":"id","delivered":n} |
| app → daemon | {"v":1,"op":"subscribe","token":"…"}, then mirrored command lines |
| either way | {"v":1,"op":"ping"} / {"v":1,"op":"pong"} |
| hub → client | {"op":"error","v":1,"reason":"unauthorized"} |
Events add their own envelope (op: event, answered by event_result) and their own local subscription (subscribe_events, answered by events_subscribed), so the keystroke protocol is unchanged.
The SDK
omni-sdk, in sdk/, is a small Rust library with the wire, the configuration and persistent event clients, without the CLI, input injection, updater or feedback. Applications use it to publish or receive events through the same targets as the CLI:
| Type | Does |
|---|---|
SemanticEvent | Builds, validates, encodes and decodes an event line |
EventPublisher | Sends events to a named target from the omni configuration |
EventSender | A persistent connection that reports how many recipients took each event |
EventReceiver | Connects to a daemon and yields events, with a cancellation handle |
The CLI crate re-exports the SDK's modules under their old paths. The full API is in the Rust documentation.