From f70838c5b53d52df81409626f2401f13d3f66cf7 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sat, 4 Jul 2026 14:11:12 +0100 Subject: [PATCH] feat(keyboard): multi-layout cycle bind + summer-bar language parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SUPER+SHIFT+K → hyprctl switchxkblayout current next, rendered only when the session layout has a comma — the same gate as the Waybar language indicator. Bind data is a separate multiLayoutBinds list in keybinds.nix (still one source); hyprland.nix and the rofi cheatsheet gate on the identical condition, so bind and cheatsheet cannot drift. `current` targets the focused keyboard, so per-device-overridden boards (single layout) are a no-op. Parity: hyprland/language (with the keyboard glyph) added to both summer waybar.jsonc whole-swaps + #language in their CSS. Static JSON cannot eval-gate, so on summer themes the module shows even with one layout — parity-over-minimalism, deliberate. Found en route: summer-night has an idle_inhibitor the generated bar lacks (reverse gap) — folded into backlog item 13. Also — agent/BACKLOG.md gains items 21-24 from Bernardo's Latitude 5410 hardware QA (waybar crash on theme switch + no respawn; nameless network-menu rows; a no-update sys-rebuild; Back-everywhere audit), queued ahead of everything; HARDWARE-QUEUE's snapshot re-test now carries the "update to >= a47aa3a + relogin" precondition, since findings 5/6 match the pre-polkit-agent behavior exactly. Verified: V0 (flake check, both summer JSONs parse) + V1 both directions (default single-layout build renders no bind; us,de via extendModules renders bind AND cheatsheet row). V3 cycle check queued. Co-Authored-By: Claude Fable 5 --- agent/BACKLOG.md | 58 ++++++++++++++++++++++++++------ agent/HARDWARE-QUEUE.md | 13 ++++++- agent/JOURNAL.md | 20 +++++++++++ docs/ROADMAP.md | 22 +++++++++--- modules/home/hyprland.nix | 9 +++-- modules/home/keybinds.nix | 9 +++++ modules/home/rofi.nix | 4 ++- themes/summer-day/waybar.css | 1 + themes/summer-day/waybar.jsonc | 7 +++- themes/summer-night/waybar.css | 1 + themes/summer-night/waybar.jsonc | 7 +++- 11 files changed, 130 insertions(+), 21 deletions(-) diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index da6edf5..5468bda 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -22,14 +22,51 @@ next, in what order*. ## NOW -### 5. Keyboard layouts — cycle bind + summer-bar parity -ROADMAP § Keyboard layouts, remaining bullets: (a) a multi-layout cycle -bind (`hyprctl switchxkblayout` current-device, or xkb `grp:` option) -when >1 session layout, in the cheatsheet; (b) add `hyprland/language` -to the summer-day/night `waybar.jsonc` whole-swaps (parity rule — the -generated bar has it, the static ones don't; gating doesn't translate, -decide the static behavior and note it). **Verify:** V1 + `jq` the -rendered configs; V3 queue the on-hardware cycle check. +*Items 21–24: real-hardware QA findings from the Latitude 5410 +(Bernardo, 2026-07-04) — bugs found on metal outrank the queue.* + +### 21. Waybar crash on theme switch — no respawn +Reported: after a theme switch on the Latitude, Waybar crashed and +never came back (exec-once = no supervisor; a crash orphans the session +until relogin). Two halves: (a) **resilience** — Waybar must respawn on +crash regardless of cause. exec-once a small supervisor loop (restart +with backoff; must not fight `pkill -x waybar` intentional stops), or +re-attempt the systemd user unit with an IPC-readiness gate (the naive +unit raced Hyprland IPC on relogin — see waybar.nix's comment). (b) +**root cause** — the switch path both flips `style.css` (Waybar's +`reload_style_on_change` picks it up) *and* sends SIGUSR2 +(`nomarchy-theme-sync`), a double-reload during the config/css symlink +flip; suspect the race crashes Waybar's reload. Consider: drop the +SIGUSR2 when `reload_style_on_change` is on, or replace reload with a +clean restart on switch. **Verify:** V2 (kill waybar in the themed-VM +recipe → respawns; switch theme → bar survives); V3 re-check on the +Latitude. + +### 22. Network menu shows nameless/weird entries +Reported: `networkmanager_dmenu` lists entries with no names (likely +hidden-SSID APs rendered as blank rows, possibly other adapters/ +duplicate BSSIDs). Investigate its config.ini surface for filtering, +or patch the list source; blank rows in a themed picker read as +breakage. **Verify:** V1 (config renders) — behavior needs Wi-Fi +hardware → V3 on the Latitude. + +### 23. `sys-rebuild` — rebuild without updating the lock +Reported: there's no way to rebuild the system *without* `sys-update`'s +`nix flake update` (home-update already skips the lock; the system side +lacks the twin). Add `sys-rebuild` (snapshot-first like sys-update, +rebuild against the current lock, no update) alongside `sys-update`; +document both in README §5 + the motd cheat sheet if it lists them. +**Verify:** V1 + eval that both scripts land on PATH. + +### 24. "Back" audit — every list menu ends in ↩ Back +Reported: some submenu items/tools still lack a Back option. The "Back +everywhere" pass covered the hand-rolled dmenu lists at the time; audit +what's been added since (VPN, Display, power-profile, printers…) and +the external/plugin modules (networkmanager_dmenu, rofi-pulse-select, +rofi-calc/emoji modi) — hand-rolled lists get the shared `back` helper; +external modi that can't take an injected row get documented as such +(and Esc always works). **Verify:** V1 + a grep-the-generated-script +audit listing which menus have/lack Back; V3 spot-check. ### 6. Full docs review & restructure ROADMAP § Full docs review. The roadmap/backlog split is done (this @@ -100,8 +137,9 @@ supports it, else V3 queue. ### 13. Small niceties batch (one slice per iteration) Each is a small, self-contained polish item in the existing patterns: - **Idle-inhibit (caffeine) toggle:** Waybar `idle_inhibitor` module - (blocks hypridle lock/suspend during video/presentations) + summer - whole-swap parity + cheatsheet mention. + (blocks hypridle lock/suspend during video/presentations) in the + GENERATED bar — summer-night already has one (reverse parity gap + found 2026-07-04); add to summer-day too + cheatsheet mention. - **Low-battery notifications:** the bar colors at 25/10% but nothing *notifies*; gate on `power.laptop` (poweralertd, or a small upower watcher consistent with how the bar reads state). diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index 6900c1b..f525692 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -21,11 +21,22 @@ QA machine), the **T14s** (webcam case). - [ ] **Auto-timezone** — enable from the menu; confirm geoclue finds the zone, `/etc/localtime` updates, and the Waybar clock refreshes (SIGUSR2 watcher) — also after a manual `timedatectl set-timezone`. +- [ ] **Keyboard layout cycle bind** — with a comma layout (e.g. + `nomarchy.keyboard.layout = "us,de"`), SUPER+SHIFT+K cycles the + focused keyboard's layout, the Waybar `󰌌` indicator follows, and + the row shows in the SUPER+? cheatsheet. - [ ] **Keyboard hotplug picker (re-verify after in-flake graduation)** — plug an external keyboard post-login, pick a layout in rofi, confirm it applies per-device only, persists in `settings.keyboard.devices`, and graduates into a `device{}` block on the next rebuild. -- [ ] **Snapshots restore/rollback** — `nomarchy-menu snapshot` now opens +- [ ] **Snapshots restore/rollback** — ⚠ PRECONDITION: update to main ≥ + a47aa3a and RELOGIN first (the polkit agent starts with the session). + Bernardo's 2026-07-04 Latitude findings 5/6 ("btrfs-assistant still + crashes", "menu snapshots shows nothing") match the PRE-fix behavior + exactly: no agent → pkexec fails silently (= menu does nothing), and + a direct unprivileged run crashes (= the libbtrfsutil bug). If either + still reproduces ON THE FIXED BUILD after relogin, reopen BACKLOG + item 4 as [stuck]. Then: `nomarchy-menu snapshot` now opens the **btrfs-assistant GUI**: a *themed* polkit prompt must appear (hyprpolkitagent — first on-hardware outing) and the GUI must open as root. Also exercise the fzf fallback in a terminal diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 3606010..900752d 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,26 @@ Template: --- +## 2026-07-04 — Keyboard cycle bind + parity (iteration #6) + Latitude triage +- **Task:** BACKLOG NOW#5; mid-iteration Bernardo delivered Latitude + 5410 hardware-QA findings (the machine got a real session!). +- **Did:** `multiLayoutBinds` in keybinds.nix (SUPER+SHIFT+K → + `switchxkblayout current next`), gated in hyprland.nix AND the rofi + cheatsheet on the same comma-in-layout condition; `hyprland/language` + + `#language` CSS added to both summer whole-swaps (shows even + single-layout there — static JSON can't gate; deliberate). Reverse + parity gap found (summer-night has idle_inhibitor, generated bar + doesn't) → folded into item 13. **Triage:** four new NOW items from + hardware: #21 Waybar crash on theme switch + no respawn (top), + #22 nameless network-menu entries, #23 `sys-rebuild` (no-update + rebuild), #24 Back-everywhere audit. +- **Verified:** V0 (JSON + flake check) + V1 both directions + (single-layout build: no bind; `us,de` via extendModules: bind + the + cheatsheet row render; summer JSONs parse). +- **Pending:** V3 cycle-bind check queued; items 21/22/24 carry V3 + re-checks on the Latitude. +- **Next suggestion:** #21 (waybar crash — stability pillar, user-hit). + ## 2026-07-04 — Webcam follow-ups (loop iteration #5, quick item) - **Task:** BACKLOG NOW#7 (Bernardo asked for "a quick item"). - **Did:** template `home.packages` gains a commented "Webcam tuning" diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 59eb5f0..d8cc102 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -721,11 +721,23 @@ how to override it. Items marked ✓ are shipped. which the watcher sees it as declared and steps back. Eval-verified (graduation + precedence) and the writer round-trips; **still needs the on-hardware hotplug re-verify** (the picker path isn't testable in CI). - - 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 - (the gating doesn't translate to their static JSON). Keep the system - (console/initrd) and session layouts in sync (the LUKS-keymap work). + - ✓ **Multi-layout cycle bind + summer parity (2026-07-04):** + `SUPER+SHIFT+K` → `hyprctl switchxkblayout current next`, rendered + only when the session layout has a comma (same gate as the Waybar + language indicator) — data lives in `keybinds.nix` as a separate + `multiLayoutBinds` list, so hyprland.nix and the cheatsheet consume + one source and gate identically (verified both ways: absent on a + single layout; bind + cheatsheet row render under `us,de` via + extendModules). `current` targets the focused keyboard, so a + per-device-overridden board (one layout) is a no-op, never a leak. + `hyprland/language` (`󰌌 {short}`) added to both summer + `waybar.jsonc` whole-swaps + `#language` in their CSS — static JSON + can't eval-gate, so on summer themes the module shows even with one + layout (a deliberate parity-over-minimalism call; it's small). + Noticed en route: summer-night carries an `idle_inhibitor` the + generated bar lacks — folded into the idle-inhibit backlog item. + - Remaining: keep the system (console/initrd) and session layouts in + sync (the LUKS-keymap work). - ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu (`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell indicator (`custom/notification` via `swaync-client -swb`: shows the diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index af8a3b9..3a8cc68 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -304,8 +304,13 @@ in # Rendered from ./keybinds.nix (the cheatsheet reads the same list), # plus the generated per-workspace binds. Like exec-once above this # stays a normal-priority list, so a downstream `bind = [...]` - # concatenates rather than replaces. - bind = map mkBind keybinds.binds ++ workspaceBinds; + # concatenates rather than replaces. The layout-cycle bind only + # exists when there is something to cycle (same comma condition + # the Waybar language indicator gates on). + bind = map mkBind (keybinds.binds + ++ lib.optionals (lib.hasInfix "," config.nomarchy.keyboard.layout) + keybinds.multiLayoutBinds) + ++ workspaceBinds; # Media keys via swayosd-client: it performs the action AND shows # the on-screen display (the nomarchy.osd module). e = repeat, diff --git a/modules/home/keybinds.nix b/modules/home/keybinds.nix index a42a852..9b55cc3 100644 --- a/modules/home/keybinds.nix +++ b/modules/home/keybinds.nix @@ -59,6 +59,15 @@ { mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy"; desc = "Screenshot region → clipboard"; } ]; + # Rendered only when the session has >1 layout (a comma in + # nomarchy.keyboard.layout) — hyprland.nix and rofi.nix both gate on + # that same condition, so the bind and its cheatsheet row stay in + # step. `current` targets the focused keyboard, so a board with its + # own per-device layout (a single one) is a no-op, never a leak. + multiLayoutBinds = [ + { mods = "$mod SHIFT"; key = "K"; action = "exec, hyprctl switchxkblayout current next"; desc = "Cycle keyboard layout"; } + ]; + extra = [ { keys = "SUPER + 1-9"; desc = "Switch to workspace 1-9"; } { keys = "SUPER + SHIFT + 1-9"; desc = "Move window to workspace 1-9"; } diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index bca2fb6..7cd7908 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -119,7 +119,9 @@ let key = lib.replaceStrings [ "question" "slash" ] [ "?" "/" ] b.key; in prefix + key; cheatRows = - map (b: padRight 22 (prettyKeys b) + b.desc) keybinds.binds + map (b: padRight 22 (prettyKeys b) + b.desc) (keybinds.binds + ++ lib.optionals (lib.hasInfix "," cfg.keyboard.layout) + keybinds.multiLayoutBinds) ++ map (e: padRight 22 e.keys + e.desc) keybinds.extra; cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt" (lib.concatStringsSep "\n" cheatRows + "\n"); diff --git a/themes/summer-day/waybar.css b/themes/summer-day/waybar.css index b515de0..4d484b1 100755 --- a/themes/summer-day/waybar.css +++ b/themes/summer-day/waybar.css @@ -49,6 +49,7 @@ window#waybar { #pulseaudio, #custom-powerprofile, #custom-nightlight, +#language, #custom-updates, #battery, #tray, diff --git a/themes/summer-day/waybar.jsonc b/themes/summer-day/waybar.jsonc index 03e021e..f72c440 100644 --- a/themes/summer-day/waybar.jsonc +++ b/themes/summer-day/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/launcher", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], + "modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -86,6 +86,11 @@ "on-click": "nomarchy-vpn" }, + "hyprland/language": { + "format": "󰌌 {short}", + "tooltip": false + }, + "custom/nightlight": { "return-type": "json", "interval": 3, diff --git a/themes/summer-night/waybar.css b/themes/summer-night/waybar.css index 6643fc3..f68ab66 100644 --- a/themes/summer-night/waybar.css +++ b/themes/summer-night/waybar.css @@ -55,6 +55,7 @@ window#waybar { #pulseaudio, #custom-powerprofile, #custom-nightlight, +#language, #custom-updates, #idle_inhibitor, #battery, diff --git a/themes/summer-night/waybar.jsonc b/themes/summer-night/waybar.jsonc index d2ce19d..deca81c 100644 --- a/themes/summer-night/waybar.jsonc +++ b/themes/summer-night/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/nomarchy", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], + "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -93,6 +93,11 @@ "on-click": "nomarchy-vpn" }, + "hyprland/language": { + "format": "󰌌 {short}", + "tooltip": false + }, + "custom/nightlight": { "return-type": "json", "interval": 3,