feat(menu): group keybinds cheatsheet Window/Workspace/Menu/Media (#108)
All checks were successful
Check / eval (push) Successful in 3m39s

Every bind in keybinds.nix carries a group; SUPER+? renders section
headers. checks.keybinds-cheatsheet asserts all groups are valid and
every description appears in the sheet (same source as Hyprland binds).

Verified: V0 flake check; built cheatsheet shows ── Window ── / etc.
This commit is contained in:
2026-07-15 10:04:33 +01:00
parent e6be5a5770
commit be2d0d200b
6 changed files with 131 additions and 61 deletions

View File

@@ -261,13 +261,6 @@ scoped implementation/verification tasks before work starts. Pass = every
current route has one deliberate home, navigation remains shallow, and no
root-level entry is added or lost.
### 108. Keybindings menu presentation and completeness audit
Group the menu as Window, Workspace, Menu, and Media, then prove every live
Hyprland binding appears from the canonical source. Do not repeat the obsolete
claim that float/move bindings are absent. Pass = presentation is scannable and
an automated comparison detects omissions or stale displayed bindings.
### 110. `[big]` Retire Control Center safely
Split this into two phases before implementation: first build a migration/drop

View File

@@ -19,6 +19,16 @@ Template:
---
## 2026-07-15 — #108 keybinds cheatsheet groups + completeness check
- **Task:** BACKLOG #108 — group SUPER+? as Window/Workspace/Menu/Media;
automated completeness.
- **Did:** every bind/extra in keybinds.nix gets `group`; cheatsheet renders
section headers; `checks.keybinds-cheatsheet` asserts groups + every desc
present (proved via assert).
- **Verified:** V0 flake check; cheatsheet file shows ── Window ── etc.
- **Pending:** V3 eyeball SUPER+? on a real session (optional).
- **Next suggestion:** #94 residual or #107 state.json rename.
## 2026-07-15 — Fix batch #112 disk picker, #106 menu Back, #113 offline theme
- **Task:** Second fix-only batch from NEXT/NOW residuals.
- **Did:** (112) `list_installable_disks` excludes fd/loop/sr/zram/tiny

View File

@@ -404,6 +404,10 @@ Design/decision records and a running log of shipped work (items marked
decision rather than a drive-by. Proved to fail: dropping snapshot makes it
name the missing entry. **V3 pending** — that the apps *launch* needs real
hardware (HARDWARE-QUEUE, Acer M5-481T).
- ✓ **Keybinds cheatsheet groups (#108):** SUPER+? sections Window /
Workspace / Menu / Media from keybinds.nix `group`;
`checks.keybinds-cheatsheet` fails if a bind lacks a group or a desc is
missing from the sheet.
- ✓ **Fix batch #112 / #106 / #113:** installer disk picker drops fd0/loop/
optical/<8GiB and lists largest-first (OVMF no longer defaults to floppy);
`checks.menu-back` guards every internal rofi list emits ↩ Back; offline

View File

@@ -477,6 +477,48 @@
touch $out
'';
# Keybinds cheatsheet completeness + grouping (#108): every bind
# from keybinds.nix has a group, and every desc appears in the
# rendered cheatsheet (same source hyprland.nix binds from).
keybinds-cheatsheet =
let
inherit (nixpkgs.lib) assertMsg concatLists concatMapStringsSep
concatStringsSep elem filter hasInfix;
kb = import ./modules/home/keybinds.nix;
groups = [ "Window" "Workspace" "Menu" "Media" ];
allBinds = kb.binds ++ kb.multiLayoutBinds;
badGroup = filter (b: !(elem (b.group or null) groups)) allBinds;
extraBad = filter (e: !(elem (e.group or null) groups)) kb.extra;
descs = map (b: b.desc) allBinds ++ map (e: e.desc) kb.extra;
linesFor = g:
let rows = filter (b: b.group == g) allBinds
++ filter (e: e.group == g) kb.extra;
in if rows == [ ] then [ ]
else [ (" " + g + " ") ] ++ map (r: r.desc) rows;
sheet = concatStringsSep "\n" (concatLists (map linesFor groups));
missing = filter (d: !(hasInfix d sheet)) descs;
ok = badGroup == [ ] && extraBad == [ ] && missing == [ ]
&& hasInfix " Window " sheet
&& hasInfix " Workspace " sheet
&& hasInfix " Menu " sheet
&& hasInfix " Media " sheet;
msg =
(if badGroup != [ ] then
"binds missing/invalid group: "
+ concatMapStringsSep ", " (b: b.desc) badGroup + "\n"
else "")
+ (if extraBad != [ ] then
"extra missing/invalid group: "
+ concatMapStringsSep ", " (e: e.desc) extraBad + "\n"
else "")
+ (if missing != [ ] then
"descs missing from grouped sheet: "
+ concatStringsSep ", " missing + "\n"
else "");
in
assert assertMsg ok ("keybinds-cheatsheet: " + msg);
pkgs.runCommand "nomarchy-keybinds-cheatsheet" { } "touch $out";
# Installer keyboard no-variant regression (item 91): gum's `(none)`
# row is display-only. Exercise it through the real template
# patcher, then build the exact us + empty-variant console keymap

View File

@@ -7,85 +7,86 @@
# key — the key name
# action — the Hyprland dispatcher (+args) that the bind runs
# desc — human label, shown in the cheatsheet
# group — cheatsheet section: Window | Workspace | Menu | Media (#108)
#
# `extra` holds cheatsheet-only rows for binds generated elsewhere
# (per-workspace numbers, mouse drags) so they still show up under SUPER+?.
{
binds = [
{ mods = "$mod"; key = "Return"; action = "exec, $terminal"; desc = "Open terminal"; }
{ mods = "$mod"; key = "Space"; action = "exec, rofi -show drun -theme launcher"; desc = "App launcher"; }
{ mods = "$mod"; key = "M"; action = "exec, nomarchy-menu"; desc = "Main menu"; }
{ mods = "$mod"; key = "E"; action = "exec, $terminal -e yazi"; desc = "File manager (yazi)"; }
{ mods = "$mod"; key = "Q"; action = "killactive"; desc = "Close window"; }
{ mods = "$mod"; key = "F"; action = "fullscreen"; desc = "Fullscreen"; }
{ mods = "$mod"; key = "V"; action = "togglefloating"; desc = "Toggle floating"; }
{ mods = "$mod SHIFT"; key = "E"; action = "exit"; desc = "Exit Hyprland"; }
{ mods = "$mod"; key = "Return"; action = "exec, $terminal"; desc = "Open terminal"; group = "Window"; }
{ mods = "$mod"; key = "Space"; action = "exec, rofi -show drun -theme launcher"; desc = "App launcher"; group = "Menu"; }
{ mods = "$mod"; key = "M"; action = "exec, nomarchy-menu"; desc = "Main menu"; group = "Menu"; }
{ mods = "$mod"; key = "E"; action = "exec, $terminal -e yazi"; desc = "File manager (yazi)"; group = "Window"; }
{ mods = "$mod"; key = "Q"; action = "killactive"; desc = "Close window"; group = "Window"; }
{ mods = "$mod"; key = "F"; action = "fullscreen"; desc = "Fullscreen"; group = "Window"; }
{ mods = "$mod"; key = "V"; action = "togglefloating"; desc = "Toggle floating"; group = "Window"; }
{ mods = "$mod SHIFT"; key = "E"; action = "exit"; desc = "Exit Hyprland"; group = "Window"; }
# Theme picker (menu dispatcher): apply writes the state and runs
# home-manager switch (progress via notify-send).
{ mods = "$mod"; key = "T"; action = "exec, nomarchy-menu theme"; desc = "Theme picker"; }
{ mods = "$mod"; key = "T"; action = "exec, nomarchy-menu theme"; desc = "Theme picker"; group = "Menu"; }
# Cycle the current theme's wallpapers (instant, no rebuild).
{ mods = "$mod SHIFT"; key = "T"; action = "exec, nomarchy-theme-sync bg next"; desc = "Next wallpaper"; }
{ mods = "$mod SHIFT"; key = "T"; action = "exec, nomarchy-theme-sync bg next"; desc = "Next wallpaper"; group = "Menu"; }
# Power menu via the dispatcher. Not Escape: Super+Escape gets
# swallowed before reaching the dispatcher on some setups.
{ mods = "$mod"; key = "X"; action = "exec, nomarchy-menu power"; desc = "Power menu"; }
{ mods = "$mod"; key = "X"; action = "exec, nomarchy-menu power"; desc = "Power menu"; group = "Menu"; }
{ mods = "$mod"; key = "N"; action = "exec, swaync-client -t"; desc = "Notification centre"; }
{ mods = "$mod"; key = "N"; action = "exec, swaync-client -t"; desc = "Notification centre"; group = "Media"; }
# SUPER+? — ? is Shift+/. SHIFT stays in the modmask (you hold it), but
# the keysym must be the BASE key `slash`, not `question`: Hyprland
# resolves the sym with Shift consumed, so `question` never matches while
# Shift is down — same as the `$mod SHIFT, 1` workspace binds. (item 26
# fixed the modmask but kept the shifted keysym → still dead; item 32.)
# The cheatsheet still renders this row as SUPER + ? (rofi.nix).
{ mods = "$mod SHIFT"; key = "slash"; action = "exec, nomarchy-menu keybinds"; desc = "Keybindings cheatsheet"; }
{ mods = "$mod SHIFT"; key = "slash"; action = "exec, nomarchy-menu keybinds"; desc = "Keybindings cheatsheet"; group = "Menu"; }
# Menu functions — SUPER+CTRL+<mnemonic> jumps straight to a
# nomarchy-menu module (all also reachable from the SUPER+M picker).
{ mods = "$mod CTRL"; key = "V"; action = "exec, nomarchy-menu clipboard"; desc = "Clipboard history"; }
{ mods = "$mod CTRL"; key = "C"; action = "exec, nomarchy-menu calc"; desc = "Calculator"; }
{ mods = "$mod CTRL"; key = "W"; action = "exec, nomarchy-menu web"; desc = "Web search"; }
{ mods = "$mod CTRL"; key = "F"; action = "exec, nomarchy-menu files"; desc = "File search"; }
{ mods = "$mod CTRL"; key = "E"; action = "exec, nomarchy-menu emoji"; desc = "Emoji picker"; }
{ mods = "$mod CTRL"; key = "N"; action = "exec, nomarchy-menu network"; desc = "Network (networkmanager_dmenu)"; }
{ mods = "$mod CTRL"; key = "B"; action = "exec, nomarchy-menu bluetooth"; desc = "Bluetooth"; }
{ mods = "$mod CTRL"; key = "K"; action = "exec, nomarchy-menu keyboard"; desc = "Keyboard layout"; }
{ mods = "$mod CTRL"; key = "S"; action = "exec, nomarchy-menu capture"; desc = "Screenshot / capture"; }
{ mods = "$mod CTRL"; key = "P"; action = "exec, nomarchy-menu colorpicker"; desc = "Color picker ( clipboard)"; }
{ mods = "$mod CTRL"; key = "A"; action = "exec, nomarchy-menu ask"; desc = "Ask Claude"; }
{ mods = "$mod CTRL"; key = "D"; action = "exec, nomarchy-menu dnd"; desc = "Do Not Disturb toggle"; }
{ mods = "$mod CTRL"; key = "V"; action = "exec, nomarchy-menu clipboard"; desc = "Clipboard history"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "C"; action = "exec, nomarchy-menu calc"; desc = "Calculator"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "W"; action = "exec, nomarchy-menu web"; desc = "Web search"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "F"; action = "exec, nomarchy-menu files"; desc = "File search"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "E"; action = "exec, nomarchy-menu emoji"; desc = "Emoji picker"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "N"; action = "exec, nomarchy-menu network"; desc = "Network (networkmanager_dmenu)"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "B"; action = "exec, nomarchy-menu bluetooth"; desc = "Bluetooth"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "K"; action = "exec, nomarchy-menu keyboard"; desc = "Keyboard layout"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "S"; action = "exec, nomarchy-menu capture"; desc = "Screenshot / capture"; group = "Media"; }
{ mods = "$mod CTRL"; key = "P"; action = "exec, nomarchy-menu colorpicker"; desc = "Color picker ( clipboard)"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "A"; action = "exec, nomarchy-menu ask"; desc = "Ask Claude"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "D"; action = "exec, nomarchy-menu dnd"; desc = "Do Not Disturb toggle"; group = "Media"; }
# I as in "settings" muscle memory (Super+I elsewhere); T for Tools.
{ mods = "$mod CTRL"; key = "I"; action = "exec, nomarchy-menu system"; desc = "System menu"; }
{ mods = "$mod CTRL"; key = "T"; action = "exec, nomarchy-menu tools"; desc = "Tools menu"; }
{ mods = "$mod CTRL"; key = "I"; action = "exec, nomarchy-menu system"; desc = "System menu"; group = "Menu"; }
{ mods = "$mod CTRL"; key = "T"; action = "exec, nomarchy-menu tools"; desc = "Tools menu"; group = "Menu"; }
# The one SUPER+CTRL row that is not a menu module: lock earns the
# family slot because bare SUPER+L is a common app bind and Escape is
# unusable (see the power-menu note above). Same dispatcher the power
# menu's Lock row uses (rofi.nix) — logind, not `hyprlock` directly, so
# the session is marked locked and hypridle's lock_cmd stays the one
# place that decides how a lock actually looks.
{ mods = "$mod CTRL"; key = "L"; action = "exec, loginctl lock-session"; desc = "Lock screen"; }
{ mods = "$mod SHIFT"; key = "C"; action = "exec, hyprpicker -a"; desc = "Color picker ( clipboard)"; }
{ mods = "$mod CTRL"; key = "L"; action = "exec, loginctl lock-session"; desc = "Lock screen"; group = "Menu"; }
{ mods = "$mod SHIFT"; key = "C"; action = "exec, hyprpicker -a"; desc = "Color picker ( clipboard)"; group = "Menu"; }
# Focus — SUPER + arrow keys.
{ mods = "$mod"; key = "left"; action = "movefocus, l"; desc = "Focus left"; }
{ mods = "$mod"; key = "right"; action = "movefocus, r"; desc = "Focus right"; }
{ mods = "$mod"; key = "up"; action = "movefocus, u"; desc = "Focus up"; }
{ mods = "$mod"; key = "down"; action = "movefocus, d"; desc = "Focus down"; }
{ mods = "$mod"; key = "left"; action = "movefocus, l"; desc = "Focus left"; group = "Window"; }
{ mods = "$mod"; key = "right"; action = "movefocus, r"; desc = "Focus right"; group = "Window"; }
{ mods = "$mod"; key = "up"; action = "movefocus, u"; desc = "Focus up"; group = "Window"; }
{ mods = "$mod"; key = "down"; action = "movefocus, d"; desc = "Focus down"; group = "Window"; }
# Multi-monitor workspace movement — SUPER + ALT + arrow keys.
{ mods = "$mod ALT"; key = "left"; action = "movecurrentworkspacetomonitor, l"; desc = "Move workspace to left monitor"; }
{ mods = "$mod ALT"; key = "right"; action = "movecurrentworkspacetomonitor, r"; desc = "Move workspace to right monitor"; }
{ mods = "$mod ALT"; key = "up"; action = "movecurrentworkspacetomonitor, u"; desc = "Move workspace to upper monitor"; }
{ mods = "$mod ALT"; key = "down"; action = "movecurrentworkspacetomonitor, d"; desc = "Move workspace to lower monitor"; }
{ mods = "$mod ALT"; key = "left"; action = "movecurrentworkspacetomonitor, l"; desc = "Move workspace to left monitor"; group = "Workspace"; }
{ mods = "$mod ALT"; key = "right"; action = "movecurrentworkspacetomonitor, r"; desc = "Move workspace to right monitor"; group = "Workspace"; }
{ mods = "$mod ALT"; key = "up"; action = "movecurrentworkspacetomonitor, u"; desc = "Move workspace to upper monitor"; group = "Workspace"; }
{ mods = "$mod ALT"; key = "down"; action = "movecurrentworkspacetomonitor, d"; desc = "Move workspace to lower monitor"; group = "Workspace"; }
# Screenshots (the menu's Capture module has the rest: OCR, recording).
# Bare Print → region to clipboard; the two → file binds save a
# timestamped PNG under ~/Pictures/Screenshots and toast the path, the
# same plumbing the Capture menu's "→ file" rows use.
{ mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy && notify-send Screenshot \"Region copied to clipboard.\""; desc = "Screenshot region clipboard"; }
{ mods = "SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot region file"; }
{ mods = "CTRL"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot screen file"; }
{ mods = "$mod SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" - | satty --filename - --fullscreen --output-filename \"$f\""; desc = "Annotate region"; }
{ mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy && notify-send Screenshot \"Region copied to clipboard.\""; desc = "Screenshot region clipboard"; group = "Media"; }
{ mods = "SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot region file"; group = "Media"; }
{ mods = "CTRL"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot screen file"; group = "Media"; }
{ mods = "$mod SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" - | satty --filename - --fullscreen --output-filename \"$f\""; desc = "Annotate region"; group = "Media"; }
];
# Rendered only when the session has >1 layout (a comma in
@@ -94,14 +95,14 @@
# 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"; }
{ mods = "$mod SHIFT"; key = "K"; action = "exec, hyprctl switchxkblayout current next"; desc = "Cycle keyboard layout"; group = "Menu"; }
];
extra = [
{ keys = "SUPER + 1-9, 0"; desc = "Switch to workspace 1-10 (0 = 10)"; }
{ keys = "SUPER + SHIFT + 1-9, 0"; desc = "Move window to workspace 1-10 (0 = 10)"; }
{ keys = "SUPER + drag"; desc = "Move (LMB) / resize (RMB) window"; }
{ keys = "Volume / Brightness"; desc = "Hardware keys, shown via the OSD"; }
{ keys = "Bar: 󰾪 click"; desc = "Caffeine hold the screen awake (idle inhibitor)"; }
{ keys = "SUPER + 1-9, 0"; desc = "Switch to workspace 1-10 (0 = 10)"; group = "Workspace"; }
{ keys = "SUPER + SHIFT + 1-9, 0"; desc = "Move window to workspace 1-10 (0 = 10)"; group = "Workspace"; }
{ keys = "SUPER + drag"; desc = "Move (LMB) / resize (RMB) window"; group = "Window"; }
{ keys = "Volume / Brightness"; desc = "Hardware keys, shown via the OSD"; group = "Media"; }
{ keys = "Bar: 󰾪 click"; desc = "Caffeine hold the screen awake (idle inhibitor)"; group = "Media"; }
];
}

View File

@@ -133,17 +133,37 @@ let
# renders "SHIFT + /". `?` IS Shift+/, so collapse that back to the
# single documented glyph — the cheatsheet reads "SUPER + ?".
in lib.replaceStrings [ "SHIFT + /" ] [ "?" ] (prefix + key);
cheatRows =
map (b: padRight 22 (prettyKeys b) + b.desc) (keybinds.binds
# Grouped cheatsheet (#108): Window → Workspace → Menu → Media.
# Section headers are non-selectable chrome (rofi still shows them as rows;
# Enter is a no-op either way). Order matches the group enum.
cheatGroupOrder = [ "Window" "Workspace" "Menu" "Media" ];
cheatBindRows =
map (b: {
inherit (b) group desc;
keys = prettyKeys b;
}) (keybinds.binds
++ lib.optionals (lib.hasInfix "," cfg.keyboard.layout)
keybinds.multiLayoutBinds)
# Launch-or-focus binds (nomarchy.launchOrFocus, generated in
# hyprland.nix) — same renderer, so they land in the cheatsheet too.
++ map (e: padRight 22 (prettyKeys e)
+ (if e.desc != "" then e.desc
else "Focus or launch ${if e.command == "" then lib.toLower e.class else e.command}"))
cfg.launchOrFocus
++ map (e: padRight 22 e.keys + e.desc) keybinds.extra;
++ map (e: {
group = "Menu";
keys = prettyKeys e;
desc = if e.desc != "" then e.desc
else "Focus or launch ${if e.command == "" then lib.toLower e.class else e.command}";
}) cfg.launchOrFocus
++ map (e: {
group = e.group or "Menu";
keys = e.keys;
inherit (e) desc;
}) keybinds.extra;
cheatRowsFor = g:
let rows = lib.filter (r: r.group == g) cheatBindRows;
in lib.optionals (rows != [ ]) (
[ (" " + g + " ") ]
++ map (r: padRight 22 r.keys + r.desc) rows
);
cheatRows = lib.concatLists (map cheatRowsFor cheatGroupOrder);
cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt"
(lib.concatStringsSep "\n" cheatRows + "\n");