← Otto Developer GuideThe Debug Action Hook
Otto’s keyboard shortcuts are matched inside the libinput handler, before the
key ever reaches a client. Keys synthesised through the virtual-keyboard
protocol (wtype, wlrctl, the RDP bridge) deliberately skip that filter and
go straight to the focused surface; see
src/state/virtual_keyboard_handler.rs. A script therefore cannot drive
compositor UI by pressing keys.
The action hook is the way in. Once per event-loop iteration each backend
reads a file; if it holds the name of a builtin shortcut action, Otto runs
that action exactly as if its key had been pressed, then requests a redraw so
the scheduled lay-rs transactions actually tick.
Like every file-driven hook (touch /tmp/otto-* toggles, the scripted
gesture driver), it exists only in builds with the debug-hooks feature,
which dev enables: a file read per loop turn is not something a release
session should pay for. Build with cargo build --features "debug-hooks"
(or dev) to use it; see crates/otto-render/src/debug_hooks.rs.
echo ExposeShowAll > "$XDG_RUNTIME_DIR/otto-action"
The file is consumed (deleted) when it is read.
OTTO_ACTION_FILE
The path defaults to otto-action in the user’s runtime directory
($XDG_RUNTIME_DIR, else /run/user/<uid>), never /tmp, where any local
user could write actions into the session; with no runtime directory and no
override the hook is off. It is overridden by the OTTO_ACTION_FILE
environment variable, read once from Otto’s own environment:
OTTO_ACTION_FILE=/run/user/1000/otto-harness.action cargo run --release
...
echo ApplicationSwitchNext > /run/user/1000/otto-harness.action
Give each session its own path when more than one Otto is running on the
machine — a harness on a tty and a nested --winit instance otherwise fight
over the same well-known file.
Action names
Any name parse_builtin_name accepts (src/config/shortcuts.rs), the same
names the [shortcuts] config table uses:
| Group | Names |
|---|---|
| Overview | ExposeShowAll, ExposeShowDesktop |
| App switcher | ApplicationSwitchNext, ApplicationSwitchPrev, ApplicationSwitchNextWindow, ApplicationSwitchQuit |
| Windows | ToggleMaximizeWindow, TileWindowLeft, TileWindowRight, CloseWindow, ToggleDecorations |
| Tiling | TilingToggle, FocusLeft, FocusRight, FocusUp, FocusDown, MoveContainerLeft, MoveContainerRight, MoveContainerUp, MoveContainerDown, SplitHorizontal, SplitVertical, ResizeGrowWidth, ResizeShrinkWidth, ResizeGrowHeight, ResizeShrinkHeight, EqualizeContainer, FloatingToggle, FocusModeToggle |
| Session | Quit, LockSession |
| OSD | BrightnessUp, BrightnessDown, VolumeUp, VolumeDown, VolumeMute |
| Media | MediaPlayPause, MediaNext, MediaPrev, MediaStop |
| Debug dumps | SceneSnapshot (alias ExportSceneJson), SkpSnapshot (alias ExportSceneSkp) |
Workspace and Screen need an index, which the bare-name parser cannot
carry, so they are not reachable through the hook — a workspace switch bound
to a shortcut still resolves to WorkspaceNum and runs, but only via a real
key press. Backend-specific actions (ScaleUp, ScaleDown, RotateOutput)
are dispatched by the per-backend keyboard handlers; reaching them from the
hook logs a warning rather than doing anything.
An unknown name logs unknown debug action: … and is otherwise ignored.
Where it lives
Otto::poll_debug_action_file and process_debug_key_action in
src/input/actions.rs. Both backends (src/winit.rs and src/udev/init.rs)
call the same function and differ only in how they ask for the redraw
afterwards, so the two are guaranteed to accept the same set of actions.
Note that process_common_key_action warns and drops anything it does not
own; the window-management actions live in the per-backend keyboard
dispatchers. process_debug_key_action handles those explicitly, which is why
the hook can drive the app switcher and the tiling actions at all.