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
|
||||
|
||||
@@ -107,9 +107,12 @@ in
|
||||
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
||||
the primary nomarchy.keyboard.layout), applies the choice to that
|
||||
keyboard only (a per-device kb_layout, so the built-in board is left
|
||||
alone), and remembers it per-device (~/.local/state) — re-applying
|
||||
automatically on later reconnects. The runtime-remember complement to
|
||||
the declarative keyboard.devices; a stateful runtime piece by design.
|
||||
alone), and remembers it in the git-tracked in-flake state
|
||||
(settings.keyboard.devices, not ~/.local/state) — re-applied
|
||||
automatically on later reconnects and across reboots. Each remembered
|
||||
choice graduates into nomarchy.keyboard.devices on the next rebuild
|
||||
(a generated device block). The runtime-remember complement to the
|
||||
declarative keyboard.devices.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -136,7 +139,9 @@ in
|
||||
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
||||
keyboard that's physically a different layout than the laptop's
|
||||
built-in one. Hyprland applies it automatically whenever that
|
||||
keyboard is connected.
|
||||
keyboard is connected. The interactive watcher
|
||||
(nomarchy.keyboard.layouts) also writes its remembered picks here on
|
||||
the next rebuild, so a runtime choice graduates into reproducible config.
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
@@ -55,11 +55,18 @@ let
|
||||
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
||||
border = { active = "accent"; inactive = "overlay"; };
|
||||
|
||||
# Non-appearance feature settings the menu writes into this same in-flake
|
||||
# state. nomarchy.nightlight: `installed` (sticky — gates the unit, so the
|
||||
# first enable rebuilds) and `on` (instant runtime on/off). Defaulted so a
|
||||
# sparse/older state file still evaluates; nomarchy.settings exposes them.
|
||||
settings.nightlight = { installed = false; on = true; };
|
||||
# Non-appearance feature settings the menu/watchers write into this same
|
||||
# in-flake state. nomarchy.nightlight: `installed` (sticky — gates the unit,
|
||||
# so the first enable rebuilds) and `on` (instant runtime on/off).
|
||||
# settings.keyboard.devices: per-device layouts the new-keyboard watcher
|
||||
# remembers (device-name -> XKB layout), git-tracked instead of
|
||||
# ~/.local/state; they graduate into nomarchy.keyboard.devices on the next
|
||||
# rebuild. Defaulted so a sparse/older state file still evaluates;
|
||||
# nomarchy.settings exposes them.
|
||||
settings = {
|
||||
nightlight = { installed = false; on = true; };
|
||||
keyboard.devices = { };
|
||||
};
|
||||
};
|
||||
|
||||
parsed = lib.recursiveUpdate defaults themeState;
|
||||
|
||||
Reference in New Issue
Block a user