evidence · 7.7 KiB

keyboard-appearance-host.md

Committed at docs/evidence/omarchy-themes/keyboard-appearance-host.md. This is what grounds the requirements that cite it.

Open raw source

On-screen keyboard colours: host checkpoint

Observed 2026-09-24 from integration base 69e23b3f, on branch impl/keyboard-theme. This closes task 3.3’s keyboard slice only (see tasks.md’s 3.3.k); Settings, notifications and the touch chooser integration under 3.3 remain open and are not claimed here.

What wvkbd actually accepts

wvkbd-mobintl --help (pinned nixpkgs 20b1ddd1aa5ace70c9468305030aa4f9ef79671b, package wvkbd-0.20) confirmed the exact flag set used: --bg, --fg, --fg-sp, --press, --press-sp, --text, --text-sp (each rrggbb[aa]), plus --hidden (“Start hidden (send SIGUSR2 to show)”). There is no live-recolor IPC; colours are read only at process start.

Source

tools/keyboard_appearance.py. Its input is an already-acknowledged theme generation’s bounded report.json palette (the same file app_appearance.py reads). It writes only keyboard-appearance/generations/ID/{wvkbd.args,coverage.json} under private theme state, and publishes a separate keyboard-appearance/active pointer for the supervised launcher — mirroring app_appearance.py’s own generation-cache and activation-lock conventions (it reuses app_appearance.activation_lock and theme_transaction._pointer, wrapped under this module’s own KeyboardAppearanceError so callers need not import app_appearance’s exception type). sync() rejects a foreign pointer or foreign generation directory and reports whether the resolved colours actually changed, so a restart is never triggered for a no-op resync.

Colour mapping

Seven flags map onto the same palette roles the card/grip appearance contract (nix/card-shell/adapter.c) already uses, so the keyboard reads as the same surface family:

flagpalette key
--bgbackground
--fgdark_background
--fg-spdarker_background
--pressselection
--press-splighter_background
--textforeground
--text-spbright_foreground

All seven keys are present in every report.json produced by the pinned upstream resolver (omarchy-theme-color --all), including the packaged Catppuccin default and the media-free recovery generation, which this checkpoint confirmed have identical palettes.

Restart, without a race

wvkbd cannot be recolored live, so a theme change restarts the process. keyboard_appearance.restart() never manages the process directly: it sends a same-uid SIGTERM to wvkbd-mobintl, exactly the existing k230-keyboard-gesture-signal show/hide convention (same-uid pkill -USR1/-USR2). It only does this when $XDG_RUNTIME_DIR/k230-keyboard-supervised exists — a sentinel the supervised launcher (k230-supervised-keyboard in nix/shell.nix) writes right after it finds the session’s Wayland socket, on every start. Without that sentinel (the non-coherent-shell legacy swayConfig-exec’d keyboard, which nothing resupervises), restart() reports {"restarted": false, "reason": "not-supervised"} instead of killing a keyboard that would then stay dead. systemd’s shell-keyboard unit already carries Restart = "always"; RestartSec = 3;, so the relaunch is bounded.

Shown/hidden state survives the restart without any post-restart signal replay: k230-keyboard-gesture-signal now also persists $XDG_RUNTIME_DIR/k230-keyboard-visible (show/hide) whenever the compositor’s gesture policy calls it, and the launcher reads that marker at its next start to decide whether to pass --hidden at all — sidestepping any assumption about when a freshly started wvkbd is ready to receive a signal.

Wiring into activation

theme_activate.py’s --activate path and theme_catalog.py’s activate action each call keyboard_appearance.sync_and_restart(...) immediately after the existing activate_generation(...) call, passing expected_generation so a superseding activation is reported rather than recoloring to a stale generation. This is deliberately not wired inside theme_transaction.activate_generation() itself: that function’s return shape and call graph are exercised by dozens of existing assertions across several actively-edited test files (test_omarchy_theme_transaction.py, test_paired_theme_endpoints_runtime.py, test_real_theme_paired_runtime.py), and changing its default behavior for every caller was a larger, riskier blast radius than this task needed. Both CLI entry points now report keyboard_appearance in their JSON output, independent of app_appearance’s own status, so a keyboard-colour failure never revisits an already- acknowledged shell generation (same non-blocking convention as app_appearance.sync).

Packaged default

nix/handheld-theme-default/wvkbd.args is a pinned static file (same pattern as terminal-foot.ini/monitor-foot.ini), generated and checked by tools/generate_default_keyboard.py (--check mode). default.nix installs it into both the bundled Catppuccin and the media-free recovery generation directories; both currently have identical palettes, so the same file is correct for both, exactly like the existing Foot configs. The supervised launcher falls back to this file when no theme has ever been activated (fresh home) or the active pointer/file is missing or malformed (checked by counting exactly 14 lines).

Commands run and results

python3 tests/test_handheld_keyboard_theme.py -v   PASS 15 host tests
python3 tests/test_handheld_app_themes.py           PASS 9 host tests
python3 tests/test_handheld_theme_rendering.py       PASS 3 host tests
python3 tests/test_handheld_theme_default.py         PASS 3 host tests
python3 tests/test_omarchy_theme_activation.py       PASS 9 host tests
python3 tests/test_omarchy_theme_transaction.py      PASS 18 host tests
python3 tests/test_theme_catalog.py                  PASS 9 host tests
python3 tests/test_theme_preferences.py              PASS 9 host tests
python3 tests/test_omarchy_theme_resolution.py       PASS 4 host tests
python3 tests/test_omarchy_theme_sources.py          PASS 5 host tests
python3 tests/test_omarchy_theme_tools.py            PASS 2 host tests
python3 tools/generate_default_keyboard.py --check   PASS (no drift)
nix eval .#nixosConfigurations.k230.config.system.build.toplevel.drvPath
  -> /nix/store/8k3hyx5x9l086bpy0h7qiq7qisz50bll-nixos-system-nixos-26.11.20260919.20b1ddd.drv
nix build --no-link --max-jobs 1 --cores 4 .#handheld-theme-default .#handheld-theme-command
  -> built; both generation dirs contain wvkbd.args; k230-theme/omarchy-theme-set
     wrappers carry --pkill <cross procps>/bin/pkill
python3 tests/test_handheld_theme_bundle.py --package <built handheld-theme-default>
  -> PASS (includes the new wvkbd.args identity check)
openspec validate the-shell-loads-omarchy-themes --strict   valid
openspec validate --all --strict                             31 passed, 0 failed

bash -n was also run against both edited/added nix/shell.nix script bodies (k230-keyboard-gesture-signal, k230-supervised-keyboard) with their Nix string interpolations placeholder-substituted, since neither script is separately exposed as a buildable flake attribute; this proves syntax only, not runtime behavior.

What remains unproven

No board, QEMU, or real-glass evidence exists for this change: whether the restart is visually imperceptible or causes a frame of default-colour flash, whether focus is actually preserved through the restart, whether the shown/hidden marker round-trips correctly against the compositor’s own shell.keyboard.progress state machine on real touch gestures, and whether RestartSec = 3 is an acceptable perceived delay are all <!-- UNVERIFIED --> and need the reserved board. Settings/notifications/chooser keyboard surfacing (the rest of task 3.3) and the --surface system test-flag machinery are separately unimplemented.