Keyboard¶
Press keys when a control is active. The second target this library ships, and the proof that the control standard reaches past a hand.
[dofs]
close = "vhi.prediction.index" # a finger
walk = "keyboard.hold.letter.w" # held while the control is above 0.5
fire = "keyboard.tap.edit.space" # one press per crossing
Both targets share one file and one ControlBus. Nothing in the map format, the bus or the
core distinguishes a key from a finger.
Keyboard controls are discrete DOFs¶
A key is a two-state discrete control, so every part of "press it when the signal goes over 0.5" already existed:
| what you want | what does it |
|---|---|
| activate above a threshold | a scalar selects the non-rest state of a two-state control |
| choose the threshold | Capability.activation_threshold, declared by this target as 0.5 |
| override it per control | threshold_fraction on the binding |
| ignore a chattering signal | debounce_s on the binding |
| know when it changed | ControlBus delivers discrete edges, not levels |
So this module is small: it maps an edge onto a key press and nothing else.
Addresses¶
keyboard.<mode>.<category>.<key>, mode first, around 220 of them: every key in both
modes. The dots are what the editor's picker builds its tree from, so the address shape is
also the shape you navigate.
keyboard
├─ hold key down while the control is active
│ ├─ letter a … z
│ ├─ digit 0 … 9
│ ├─ nav left, right, up, down, home, end, page_up, page_down
│ ├─ edit enter, tab, space, escape, backspace, delete, insert
│ ├─ modifier shift, ctrl, alt, cmd
│ ├─ function f1 … f20
│ ├─ numpad n0 … n9, add, subtract, multiply, divide, decimal
│ ├─ punctuation
│ └─ media
└─ tap one press-and-release per activation
└─ (the same categories)
hold is right for movement: walking, aiming, push-to-talk. tap is right for commands,
where one gesture should mean one keystroke however long you hold it.
This types into whatever window has focus
A rapidly changing signal on keyboard.tap.edit.enter submits repeatedly to the focused window. A
KeyboardTarget therefore starts disarmed and
sends nothing until arm is called; it disarms
itself on stop, on a backend failure, and when the process exits.
Prefer tap for anything destructive. A held key outlives the process that set it, and
no teardown runs on SIGKILL.
Installing, and the macOS permission
Needs the keyboard extra: uv sync --extra keyboard. That installs
pynput, not the PyPI package called keyboard,
which needs root on macOS and Linux and is effectively unmaintained.
On macOS the process also needs Accessibility permission, under System Settings ›
Privacy & Security › Accessibility. Without it pynput reports success and nothing
happens, indistinguishable from a broken map, so arm checks the permission and raises
with the reason.
Reference¶
KeyboardTarget
¶
Press keys from control values.
A myogestic.controls.Target: construct it, hand it to a
myogestic.controls.ControlBus, and register stop with app.cleanup_hooks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend
|
Any
|
Anything with |
None
|
armed
|
bool
|
Start armed. Defaults to False: a resolved map is live the moment it binds, into whatever window has focus. |
False
|
Notes
send acts only on the edges ControlBus reports, never on the level.
Examples:
>>> from myogestic.controls import ControlBus, load_control_map, resolve
>>> from myogestic.keyboard import KeyboardTarget, keyboard_capabilities
>>>
>>> control_map = load_control_map({"dofs": {"go": "keyboard.hold.letter.w"}})
>>> controls = resolve(control_map, keyboard_capabilities())
>>> keys = KeyboardTarget()
>>> bus = ControlBus(controls, targets=[keys])
>>> keys.armed # nothing is sent until it is armed
False
claims
property
¶
Which aliases this target drives, for ControlBus's coverage check.
arm_refusal
property
¶
arm_refusal: str
Why arm would refuse right now, or "" when it would work.
Asked before the click, so the switch is not a trap. Cheap enough to read every
frame — one AXIsProcessTrusted and one module lookup.
capabilities
¶
capabilities() -> tuple[Capability, ...]
The manifest, so this can be handed to an editor like a remote target's client.
request_accessibility
staticmethod
¶
request_accessibility() -> str
Ask macOS for the permission this target needs. See request_accessibility.
arm
¶
Start sending key events, building the backend if it does not exist yet.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
When the backend cannot be built (normally |
disarm
¶
Stop sending, letting go of anything still held. Idempotent.
Releases first: disarming mid-gesture would otherwise leave a key down with nothing left to lift it.
bind
¶
bind(controls: ControlSet) -> None
Accept the keyboard aliases in controls, and refuse a key that is not real.
Raises:
| Type | Description |
|---|---|
ValueError
|
When an alias routes to a |
send
¶
Act on the state changes this tick. Never raises.
Only changed is read: a level would re-press a held key every tick. ControlBus
already computes the edge, including the debounce.
keyboard_capabilities
¶
keyboard_capabilities() -> tuple[Capability, ...]
Every key this target can press, in both modes.
The manifest a myogestic.controls.resolve call validates a map against, exactly like
a remote target's. Around 220 entries — every key twice.
Returns:
| Type | Description |
|---|---|
tuple[Capability, ...]
|
Discrete and two-state. A held state travels over its target's own command channel, never on a stream, so nothing may try to route one onto a wire. |
Examples: