fix(keyboard): isolate new-keyboard layout to its own device
The interactive new-keyboard picker applied the chosen layout with `hyprctl switchxkblayout <device> <index>`, which flips the *shared* layout for every keyboard without its own per-device config — so picking a layout for an external board dragged the built-in keyboard along, and the change stuck after unplug (it's global state, not device-bound). Compounding it, the candidate `keyboard.layouts` pool was merged into `input.kb_layout`, loading those layouts onto the session keyboard too. Apply the choice as a per-device `hyprctl keyword device[<name>]:kb_layout` instead — the runtime twin of the declarative `keyboard.devices` blocks — so it isolates that one keyboard and never touches the built-in board. Keep the candidate pool out of `input.kb_layout` (session keyboard carries only `keyboard.layout`); the pool is for external boards, applied per-device. Also put `nomarchy-keyboard-watch` on PATH when enabled (it was store-path only via exec-once, so undiscoverable) for debugging. Found on the Latitude 5410 hardware sweep; picker itself verified working. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -372,15 +372,20 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
|
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
|
||||||
keyboard connects *after* login that isn't in `keyboard.devices` and
|
keyboard connects *after* login that isn't in `keyboard.devices` and
|
||||||
hasn't been chosen before, it pops a rofi layout picker, applies the
|
hasn't been chosen before, it pops a rofi layout picker, applies the
|
||||||
choice with `hyprctl switchxkblayout` (an index into the candidate set,
|
choice as a **per-device** `hyprctl keyword device[<name>]:kb_layout` (the
|
||||||
which `input.kb_layout` carries), and remembers it per-device in
|
runtime twin of the declarative `device{}` blocks — so it isolates that
|
||||||
`~/.local/state/nomarchy/keyboard-layouts` — re-applied silently on later
|
one keyboard and never touches the built-in board), and remembers it
|
||||||
reconnects. The boot-time set (incl. the built-in keyboard) is never
|
per-device in `~/.local/state/nomarchy/keyboard-layouts` — re-applied
|
||||||
prompted. The runtime-remember complement to the declarative
|
silently on later reconnects. The boot-time set (incl. the built-in
|
||||||
`keyboard.devices`; a stateful runtime piece by design. **Pending an
|
keyboard) is never prompted. The runtime-remember complement to the
|
||||||
on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus):
|
declarative `keyboard.devices`; a stateful runtime piece by design.
|
||||||
offer to write a choice into `keyboard.devices` so it graduates to the
|
**Picker verified on hardware (2026-06-18)**; the first cut applied the
|
||||||
reproducible config.
|
choice with `switchxkblayout` (a global layout-index flip) and merged the
|
||||||
|
candidate pool into `input.kb_layout`, so a selection leaked onto the
|
||||||
|
laptop keyboard and stuck after unplug — fixed by the per-device keyword +
|
||||||
|
keeping the pool out of the session layout. Remaining (bonus): offer to
|
||||||
|
write a choice into `keyboard.devices` so it graduates to the reproducible
|
||||||
|
config; **re-verify the per-device isolation on hardware**.
|
||||||
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
|
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
|
||||||
`grp:` options) for switching layouts on one keyboard; add the
|
`grp:` options) for switching layouts on one keyboard; add the
|
||||||
`hyprland/language` module to the summer whole-swap themes for parity
|
`hyprland/language` module to the summer whole-swap themes for parity
|
||||||
|
|||||||
@@ -55,10 +55,14 @@ let
|
|||||||
++ lib.optional (m.extra != "") m.extra
|
++ lib.optional (m.extra != "") m.extra
|
||||||
);
|
);
|
||||||
|
|
||||||
# Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on
|
# Candidate layouts offered by the new-keyboard picker: the session
|
||||||
# commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all
|
# layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
|
||||||
# of them are switchable, and feeds the watcher's picker.
|
# session) plus the extra nomarchy.keyboard.layouts pool. The pool is
|
||||||
kbLayouts = lib.unique (
|
# deliberately NOT merged into input.kb_layout — those candidates are for
|
||||||
|
# *external* keyboards and get applied per-device by the watcher's apply().
|
||||||
|
# Loading them onto the session keyboard is what let a global switch flip
|
||||||
|
# the built-in board to the wrong layout.
|
||||||
|
pickerLayouts = lib.unique (
|
||||||
(lib.splitString "," config.nomarchy.keyboard.layout)
|
(lib.splitString "," config.nomarchy.keyboard.layout)
|
||||||
++ config.nomarchy.keyboard.layouts
|
++ config.nomarchy.keyboard.layouts
|
||||||
);
|
);
|
||||||
@@ -68,18 +72,20 @@ let
|
|||||||
# keyboard.devices, not already remembered), ask for a layout and persist it
|
# keyboard.devices, not already remembered), ask for a layout and persist it
|
||||||
# per-device, re-applying silently on later reconnects. Stateful runtime
|
# per-device, re-applying silently on later reconnects. Stateful runtime
|
||||||
# piece — the complement to the declarative keyboard.devices. Reliable
|
# piece — the complement to the declarative keyboard.devices. Reliable
|
||||||
# primitives only: poll hyprctl devices, apply via switchxkblayout (an index
|
# primitives only: poll hyprctl devices, apply with a per-device
|
||||||
# into kbLayouts). NOTE: needs an on-hardware test (hotplug isn't verifiable
|
# `device[<name>]:kb_layout` keyword — the runtime twin of the declarative
|
||||||
# in CI).
|
# 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).
|
||||||
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
|
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
|
||||||
set -u
|
set -u
|
||||||
layouts="${lib.concatStringsSep " " kbLayouts}"
|
layouts="${lib.concatStringsSep " " pickerLayouts}"
|
||||||
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
||||||
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
|
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
|
||||||
mkdir -p "$(dirname "$state")"; [ -f "$state" ] || : > "$state"
|
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() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&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'; }
|
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
|
||||||
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
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"; }
|
saved_for() { while IFS='=' read -r k v; do [ "$k" = "$1" ] && { printf '%s' "$v"; return; }; done < "$state"; }
|
||||||
@@ -201,8 +207,10 @@ in
|
|||||||
|
|
||||||
input = {
|
input = {
|
||||||
# kb_layout/variant come from nomarchy.keyboard.* — set that
|
# kb_layout/variant come from nomarchy.keyboard.* — set that
|
||||||
# option (the installer writes it; it also drives tty/LUKS).
|
# option (the installer writes it; it also drives tty/LUKS). Only the
|
||||||
kb_layout = lib.concatStringsSep "," kbLayouts;
|
# session layout(s) land here; the keyboard.layouts pool is applied
|
||||||
|
# per-device to external boards, never onto the session keyboard.
|
||||||
|
kb_layout = config.nomarchy.keyboard.layout;
|
||||||
kb_variant = config.nomarchy.keyboard.variant;
|
kb_variant = config.nomarchy.keyboard.variant;
|
||||||
follow_mouse = lib.mkDefault 1;
|
follow_mouse = lib.mkDefault 1;
|
||||||
touchpad.natural_scroll = lib.mkDefault true;
|
touchpad.natural_scroll = lib.mkDefault true;
|
||||||
@@ -270,7 +278,10 @@ in
|
|||||||
# writes a config file). A helper to find values for nomarchy.monitors —
|
# 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
|
# the declarative config stays the source of truth (we don't source its
|
||||||
# output). Gated on its toggle; pointless without the Hyprland session.
|
# output). Gated on its toggle; pointless without the Hyprland session.
|
||||||
home.packages = lib.optionals
|
home.packages =
|
||||||
(config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
lib.optionals (config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
||||||
[ pkgs.nwg-displays ];
|
[ pkgs.nwg-displays ]
|
||||||
|
# The new-keyboard watcher on PATH (when enabled) so it's discoverable and
|
||||||
|
# runnable by name for debugging — Hyprland still exec-once's it by store path.
|
||||||
|
++ lib.optional (config.nomarchy.hyprland.enable && kbAutoSwitch) keyboardWatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,10 +105,11 @@ in
|
|||||||
picker. When non-empty, a small watcher runs in the session: when a
|
picker. When non-empty, a small watcher runs in the session: when a
|
||||||
keyboard connects that isn't covered by nomarchy.keyboard.devices and
|
keyboard connects that isn't covered by nomarchy.keyboard.devices and
|
||||||
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
||||||
the primary nomarchy.keyboard.layout), applies the choice, and
|
the primary nomarchy.keyboard.layout), applies the choice to that
|
||||||
remembers it per-device (~/.local/state) — re-applying automatically
|
keyboard only (a per-device kb_layout, so the built-in board is left
|
||||||
on later reconnects. The runtime-remember complement to the
|
alone), and remembers it per-device (~/.local/state) — re-applying
|
||||||
declarative keyboard.devices; a stateful runtime piece by design.
|
automatically on later reconnects. The runtime-remember complement to
|
||||||
|
the declarative keyboard.devices; a stateful runtime piece by design.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user