diff --git a/README.md b/README.md index d8ef99a..cf8f9e2 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**. | `nomarchy.keyboard.layout` | `"us"` | XKB layout for the Hyprland session (installer writes the matching `services.xserver.xkb` in system.nix; the console + LUKS prompt follow via the distro default) | | `nomarchy.keyboard.variant` | `""` | XKB variant for the session | | `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant | +| `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device | | `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config | | `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar | | `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e02777d..81c2476 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -293,17 +293,20 @@ how to override it. Items marked ✓ are shipped. declarative way). A Waybar `hyprland/language` indicator shows the active layout, gated on more than one layout being in play (comma in the session layout, or any per-device override) so single-layout bars stay clean. - - **Interactive new-keyboard prompt (runtime + persist):** a small daemon - listening to Hyprland's socket for keyboard hotplug — when an *unknown* - keyboard connects, pop a rofi layout picker, apply the choice - (`hyprctl switchxkblayout` among a configured candidate set, or reload the - device config), and persist it per-device to a state file so it re-applies - automatically on every later reconnect. The runtime-remember complement to - the declarative `nomarchy.keyboard.devices` above (set it once, on the fly, - no config editing). Nice bridge: offer to also write the choice into - `nomarchy.keyboard.devices` so it graduates into the reproducible config. - Note: a stateful runtime piece (like the wallpaper) — it sits outside the - pure-declarative model by design. + - ✓ **Interactive new-keyboard prompt (runtime + persist):** set + `nomarchy.keyboard.layouts` (candidate layouts) and a `nomarchy-keyboard- + watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a + keyboard connects *after* login that isn't in `keyboard.devices` and + hasn't been chosen before, it pops a rofi layout picker, applies the + choice with `hyprctl switchxkblayout` (an index into the candidate set, + which `input.kb_layout` carries), and remembers it per-device in + `~/.local/state/nomarchy/keyboard-layouts` — re-applied silently on later + reconnects. The boot-time set (incl. the built-in keyboard) is never + prompted. The runtime-remember complement to the declarative + `keyboard.devices`; a stateful runtime piece by design. **Pending an + on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus): + offer to write a choice into `keyboard.devices` so it graduates to the + reproducible config. - Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb `grp:` options) for switching layouts on one keyboard; add the `hyprland/language` module to the summer whole-swap themes for parity diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index f85653a..7cba27f 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -54,6 +54,64 @@ let ++ lib.optionals (m.vrr != null) [ "vrr" (toString m.vrr) ] ++ lib.optional (m.extra != "") m.extra ); + + # Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on + # commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all + # of them are switchable, and feeds the watcher's picker. + kbLayouts = lib.unique ( + (lib.splitString "," config.nomarchy.keyboard.layout) + ++ config.nomarchy.keyboard.layouts + ); + kbAutoSwitch = config.nomarchy.keyboard.layouts != [ ]; + + # On a keyboard that connects after login (not declared in + # keyboard.devices, not already remembered), ask for a layout and persist it + # per-device, re-applying silently on later reconnects. Stateful runtime + # piece — the complement to the declarative keyboard.devices. Reliable + # primitives only: poll hyprctl devices, apply via switchxkblayout (an index + # into kbLayouts). NOTE: needs an on-hardware test (hotplug isn't verifiable + # in CI). + keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" '' + set -u + layouts="${lib.concatStringsSep " " kbLayouts}" + 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" + + layout_index() { i=0; for l in $layouts; do [ "$l" = "$1" ] && { printf %s "$i"; return; }; i=$((i + 1)); done; printf %s -1; } + apply() { idx=$(layout_index "$2"); [ "$idx" -ge 0 ] && hyprctl switchxkblayout "$1" "$idx" >/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"; } + + # Startup: re-apply remembered layouts, never prompt — so the built-in + # keyboard just stays on the session default. + prev=" " + for kb in $(keyboards); do + prev="$prev$kb " + is_declared "$kb" && continue + s=$(saved_for "$kb"); [ -n "$s" ] && apply "$kb" "$s" + done + + # Watch for keyboards that connect later. + while :; do + sleep 3 + cur=" " + for kb in $(keyboards); do + cur="$cur$kb " + case "$prev" in *" $kb "*) continue ;; esac + is_declared "$kb" && continue + s=$(saved_for "$kb") + if [ -n "$s" ]; then + 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"; } + fi + done + prev="$cur" + done + ''; in { wayland.windowManager.hyprland = lib.mkIf config.nomarchy.hyprland.enable { @@ -88,7 +146,7 @@ in # Paint the wallpaper as soon as the session is up (waits for # the daemon internally). "nomarchy-theme-sync wallpaper" - ]; + ] ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch"; # ── Theme-driven look ────────────────────────────────────────── # These flow from theme-state.json and stay at NORMAL priority: @@ -144,7 +202,7 @@ in input = { # kb_layout/variant come from nomarchy.keyboard.* — set that # option (the installer writes it; it also drives tty/LUKS). - kb_layout = config.nomarchy.keyboard.layout; + kb_layout = lib.concatStringsSep "," kbLayouts; kb_variant = config.nomarchy.keyboard.variant; follow_mouse = lib.mkDefault 1; touchpad.natural_scroll = lib.mkDefault true; diff --git a/modules/home/options.nix b/modules/home/options.nix index f8f3bc9..d694049 100644 --- a/modules/home/options.nix +++ b/modules/home/options.nix @@ -96,6 +96,22 @@ in description = "XKB variant for the Hyprland session."; }; + keyboard.layouts = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "de" "fr" ]; + description = '' + Extra candidate layouts offered by the interactive new-keyboard + picker. When non-empty, a small watcher runs in the session: when a + keyboard connects that isn't covered by nomarchy.keyboard.devices and + hasn't been chosen before, it pops a rofi picker (these layouts plus + the primary nomarchy.keyboard.layout), applies the choice, 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. + ''; + }; + keyboard.devices = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index c07fdb3..98eb5c3 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -16,6 +16,7 @@ let # is in play — multiple session layouts (comma-separated) or per-device # overrides (nomarchy.keyboard.devices) — so single-layout bars stay clean. showLanguage = lib.hasInfix "," config.nomarchy.keyboard.layout + || config.nomarchy.keyboard.layouts != [ ] || config.nomarchy.keyboard.devices != { }; # Power-profile indicator (power-profiles-daemon). Both scripts self-