oomniv0.3.0

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}
typeFieldMeans
texttextType this string
keykeyPress one named key: backspace, delete, enter, tab, esc, space, left, right, up, down, home, end
combocomboA chord: modifiers then keys, comma-separated, such as ctrl,a,g
scrollamountScroll 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

One combo, two native formsOn the left, a strip of tape holds one wire line: version 1, type combo, combo ctrl,a,g. Two lanes leave it. The Android lane turns it into two broadcasts to the omni keyboard, each a key event with the Ctrl modifier held: keycode 29 for A and keycode 35 for G, both with meta state 4096. The desktop lane, through the ydotool backend, uses Linux evdev codes: 29 for Ctrl, 30 for A and 34 for G. The sender wrote the line once; each receiver maps the same semantic keys to its own codes. {"v":1,"type":"combo", "combo":"ctrl,a,g"} Android · omni keyboard key keycode 29 (A) meta 4096 (Ctrl) key keycode 35 (G) meta 4096 (Ctrl) Linux desktop · ydotool evdev 29 (Ctrl) held evdev 30 (A), 34 (G) written once, mapped by each receiver
One combo, two native forms.The same three words become Android key events with a modifier state on a phone, and evdev codes on a Linux desktop.

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.

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 → toFrame
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 → devicea 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:

TypeDoes
SemanticEventBuilds, validates, encodes and decodes an event line
EventPublisherSends events to a named target from the omni configuration
EventSenderA persistent connection that reports how many recipients took each event
EventReceiverConnects 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.