feat(keyboard): per-device layout memory in-flake (no ~/.local/state)
Phase 2 of the in-flake-state philosophy, mirroring night-light: the
nomarchy-keyboard-watch daemon now remembers per-device layouts in the
git-tracked state file (settings.keyboard.devices) instead of
~/.local/state/nomarchy/keyboard-layouts.
- Reads the LIVE working-tree map (theme-sync get → jq) so a pick this
session is honoured at once; writes instantly with --no-switch (no
rebuild). Writes the whole map back at the dot-free parent path
(read-modify-write) so a device name with a dot can't corrupt the
tool's dotted set-path (Hyprland only sanitises spaces→-).
- Graduation: nomarchy.keyboard.devices now mkDefault-merges entries
from settings.keyboard.devices, so each remembered device becomes a
generated Hyprland device{} block on the next rebuild (reproducible,
hotplug-applied). A hand-written keyboard.devices.<name> still wins;
once baked, the watcher's is_declared check makes it step back.
- theme.nix seeds settings.keyboard.devices = {} (sparse state still
evaluates); options/template descriptions updated; ROADMAP marked done.
Eval-verified (graduation + mkDefault precedence) and the set/get
writer round-trips. Still needs the on-hardware hotplug re-verify (the
picker path isn't testable in CI).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,10 @@ let
|
||||
c = t.colors;
|
||||
inherit (config.nomarchy.lib) rgb rgba;
|
||||
|
||||
# nomarchy-theme-sync — the in-flake state writer the keyboard watcher uses
|
||||
# to remember per-device layouts (same path night-light's toggle takes).
|
||||
sync = lib.getExe config.nomarchy.package;
|
||||
|
||||
# swayosd's `--input-volume mute-toggle` draws the OSD but never flips the
|
||||
# source mute (verified on hardware: wpctl toggles the state, swayosd's
|
||||
# input-mute path is a no-op — unlike its working output-mute path). Do the
|
||||
@@ -76,19 +80,37 @@ let
|
||||
# `device[<name>]:kb_layout` keyword — the runtime twin of the declarative
|
||||
# device blocks, so it isolates that one keyboard and never disturbs the
|
||||
# built-in board (switchxkblayout flipped the *shared* layout, which leaked
|
||||
# onto the laptop and stuck after unplug). NOTE: needs an on-hardware test
|
||||
# (hotplug isn't verifiable in CI).
|
||||
# onto the laptop and stuck after unplug).
|
||||
#
|
||||
# The remembered choices live in the git-tracked in-flake state
|
||||
# (settings.keyboard.devices), NOT ~/.local/state — read live from the
|
||||
# working tree (so a pick this session is honoured at once) and written
|
||||
# instantly with `--no-switch` (no rebuild). A later rebuild graduates them
|
||||
# into nomarchy.keyboard.devices (generated device{} blocks), after which the
|
||||
# watcher treats them as declared and steps back. NOTE: needs an on-hardware
|
||||
# test (hotplug isn't verifiable in CI).
|
||||
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
|
||||
set -u
|
||||
layouts="${lib.concatStringsSep " " pickerLayouts}"
|
||||
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
||||
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
|
||||
mkdir -p "$(dirname "$state")"; [ -f "$state" ] || : > "$state"
|
||||
sync=${sync}
|
||||
|
||||
apply() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&1; }
|
||||
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
|
||||
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||
saved_for() { while IFS='=' read -r k v; do [ "$k" = "$1" ] && { printf '%s' "$v"; return; }; done < "$state"; }
|
||||
|
||||
# The remembered map (device-name -> layout) from the LIVE in-flake state;
|
||||
# an absent key (sparse state) reads as an empty object.
|
||||
saved_map() { "$sync" get settings.keyboard.devices 2>/dev/null || echo '{}'; }
|
||||
saved_for() { saved_map | jq -r --arg k "$1" '.[$k] // empty'; }
|
||||
|
||||
# Persist a pick INSTANTLY: merge it into the map and write the whole object
|
||||
# back at the dot-free parent path, so a device name containing a dot can't
|
||||
# corrupt the dotted set-path. --no-switch = write only, no rebuild.
|
||||
remember() {
|
||||
map=$(saved_map | jq -c --arg k "$1" --arg v "$2" '. + {($k): $v}') || return
|
||||
"$sync" --quiet set settings.keyboard.devices "$map" --no-switch
|
||||
}
|
||||
|
||||
# Startup: re-apply remembered layouts, never prompt — so the built-in
|
||||
# keyboard just stays on the session default.
|
||||
@@ -112,7 +134,7 @@ let
|
||||
apply "$kb" "$s"
|
||||
else
|
||||
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
||||
[ -n "$choice" ] && { printf '%s=%s\n' "$kb" "$choice" >> "$state"; apply "$kb" "$choice"; }
|
||||
[ -n "$choice" ] && { remember "$kb" "$choice"; apply "$kb" "$choice"; }
|
||||
fi
|
||||
done
|
||||
prev="$cur"
|
||||
@@ -280,6 +302,16 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# Runtime-remembered per-device layouts (settings.keyboard.devices, written
|
||||
# by the watcher into the in-flake state) graduate into the declarative
|
||||
# keyboard.devices: each becomes a generated Hyprland device{} block —
|
||||
# reproducible and applied natively on hotplug — after which the watcher sees
|
||||
# it as declared and steps back. mkDefault so a hand-written
|
||||
# keyboard.devices.<name> in home.nix still wins for the same keyboard.
|
||||
nomarchy.keyboard.devices = lib.mapAttrs
|
||||
(_name: layout: { layout = lib.mkDefault layout; })
|
||||
config.nomarchy.settings.keyboard.devices;
|
||||
|
||||
# nwg-displays: interactive monitor arranger (applies live via hyprctl and
|
||||
# writes a config file). A helper to find values for nomarchy.monitors —
|
||||
# the declarative config stays the source of truth (we don't source its
|
||||
|
||||
Reference in New Issue
Block a user