Skip to content

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

KeyboardTarget(backend: Any = None, *, armed: bool = False)

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 press(spec) and release(spec). Defaults to a pynput one, constructed on the first arm rather than here, so importing this module, listing its capabilities and resolving a map all work without the extra installed.

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

claims: frozenset[str]

Which aliases this target drives, for ControlBus's coverage check.

armed property

armed: bool

Whether key events actually leave this process.

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

arm() -> None

Start sending key events, building the backend if it does not exist yet.

Raises:

Type Description
RuntimeError

When the backend cannot be built (normally pynput not installed), or when macOS has not granted Accessibility permission. Raised rather than logged, so no target reports itself armed and then presses nothing.

disarm

disarm() -> None

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 keyboard. address this target does not export. Refused here, on the main thread, rather than discovered as a key that never fires.

send

send(values: Mapping[str, float | str], changed: Mapping[str, str]) -> None

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.

stop

stop() -> None

Let go of every held key and disarm. Idempotent.

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:

>>> from myogestic.keyboard import keyboard_capabilities
>>> caps = {c.address: c for c in keyboard_capabilities()}
>>> caps["keyboard.hold.letter.a"].states
('up', 'down')
>>> caps["keyboard.tap.edit.space"].activation_threshold
0.5