diff --git a/README.md b/README.md index 72f5f22..b9d9ea6 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ two tables below are split along exactly that line. | `nomarchy.viewers.enable` | `true` | Document/image viewers: zathura (Stylix-themed PDF) + imv | | `nomarchy.mime.enable` | `true` | Default "open with" associations (PDF/image/video/text/browser/directory); entries for apps you removed are skipped, so it degrades with the suite | | `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback | -| `nomarchy.displayProfiles` | `{}` | Named layouts for the same outputs (docked/undocked/…), switched from System › Display › Profiles: instant via hyprctl, persisted in-flake (`settings.displayProfile`), baked over `nomarchy.monitors` at the next rebuild | +| `nomarchy.displayProfiles` | `{}` | Named layouts for the same outputs (docked/undocked/…), switched from System › Display › Profiles: instant via hyprctl, persisted in-flake (`settings.displayProfile`), baked over `nomarchy.monitors` at the next rebuild. The menu's Auto-switch row (`settings.displayProfileAuto`) makes plugging/unplugging outputs apply the matching profile automatically | | `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed | | `nomarchy.package` | overlay's `nomarchy-theme-sync` | The theme/state tool package, overridable if you fork it | diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 94b7a11..6b2db11 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -71,12 +71,14 @@ the *same* outputs. **Plan (designed 2026-07-05, iteration #27):** - **Slices:** (a) ✓ shipped 2026-07-05 (iteration #28): option + overlay (pure `modules/home/monitor-rules.nix`, unit-tested by checks.display-profiles) + `nomarchy-display-profile` applier + menu - Profiles row + template example — V3 (real dock) queued; (b) - opt-in hotplug auto-switch: a socket2 watcher on monitoradded/ - removed picks the profile whose named-output set matches connected - outputs (exact match, else largest subset, ties = no switch) — - decide the flag shape (`settings.displayProfile = "auto"` vs a - separate boolean) at implementation; (c) optional per-profile + Profiles row + template example — V3 (real dock) queued; (b) ✓ + shipped 2026-07-05 (iteration #29): hotplug auto-switch — `match` + subcommand (exact set, else unambiguous largest subset, ties = + none) + a poll watcher (exec-once, the keyboard-watcher pattern) + gated live on `settings.displayProfileAuto` (a separate boolean, + NOT an "auto" pseudo-profile, so auto-picks persist concrete + profiles that rebuilds bake) + menu Auto-switch row — V3 rides the + slice-a dock test; (c) optional per-profile `workspaces = { "1" = "DP-3"; }` → Hyprland `workspace` keywords. - **Non-goals:** no kanshi (fights Hyprland's output management — decided in ROADMAP), nwg-displays stays a find-the-values helper. diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index 1ed5122..fd5efca 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -148,6 +148,11 @@ QA machine), the **T14s** (webcam case). overlay); "Base layout" restores; a menu resolution pick made earlier must NOT re-enable a profile-disabled panel after the next rebuild. +- [ ] **Display profiles auto-switch, slice b (item 15)** — same setup, + then menu › Profiles › Auto-switch on: plugging the dock/monitor + applies the matching profile within ~3s (toast names it); + unplugging switches back; Auto-switch off stops that; with two + profiles naming the same outputs, no flapping (ties do nothing). - [ ] **Color picker (item 13, final slice)** — SUPER+CTRL+P (or Tools › Color picker): hyprpicker's zoom loupe appears; click a pixel → toast shows the hex and `wl-paste` yields it; Esc cancels with no diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 8628cb6..b2863e6 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,28 @@ Template: --- +## 2026-07-05 — display profiles auto-switch (iteration #29, item 15b) +- **Task:** BACKLOG NEXT#15 slice b — opt-in hotplug auto-switch. +- **Did:** nomarchy-display-profile gains `match ` (exact set + → else unambiguous largest subset → ties/none = exit 1); + nomarchy-display-profile-watch polls `hyprctl monitors all` every 3s + (exec-once, the keyboard-watcher pattern — session units race + Hyprland IPC), reacts only to CHANGES, reads settings.displayProfileAuto + LIVE each change (instant menu toggle, no rebuild) and applies via + the tool, so auto-picks persist concrete profiles that rebuilds + bake. Chose a separate boolean over an "auto" pseudo-profile for + exactly that bake semantics. Menu: Auto-switch (on/off) row in + Profiles. Template + README notes. +- **Verified:** V0; V1 — scratch 4-profile downstream builds; both + scripts bash -n; match matrix exercised: exact ×3, unambiguous + subset, tie → none, no-fit → none; watcher exec-once present; menu + renders with the row. Real hotplug needs a session+dock → V3 (rides + the slice-a queue entry). +- **Pending:** V3. Slice c (workspace binding) remains, tagged + optional. +- **Next suggestion:** #16 greeter theming — item 15's remaining + slice c is optional; a fresh item keeps variety. + ## 2026-07-05 — display profiles slice a (iteration #28, item 15a) - **Task:** BACKLOG NEXT#15 slice a, per the #27 design: option + overlay + applier + menu + template example. diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index 06a17d2..47b5a44 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -94,12 +94,73 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList ${sync} --quiet set settings.displayProfile "" --no-switch hyprctl reload >/dev/null 2>&1 notify-send "Display profile" "Base layout restored." ;; + match) + # Which profile fits these connected outputs? (args = output names, + # e.g. `match $(hyprctl monitors all -j | jq -r '.[].name')`.) + # An exact set match wins; else the largest profile whose named + # outputs are all connected; any tie prints nothing (exit 1) — the + # auto-switch watcher must never flap between ambiguous layouts. + shift + [ "$#" -gt 0 ] || exit 1 + con=" $* " ncon=$# + exact= exactdup= best= bestn=0 dup= + while IFS=: read -r p names; do + [ -n "$p" ] || continue + n=0 ok=1 + for o in $names; do + case "$con" in *" $o "*) n=$((n+1)) ;; *) ok=; break ;; esac + done + [ -n "$ok" ] || continue + if [ "$n" -eq "$ncon" ]; then + if [ -n "$exact" ]; then exactdup=1; else exact=$p; fi + fi + if [ "$n" -gt "$bestn" ]; then best=$p bestn=$n dup= + elif [ "$n" -eq "$bestn" ] && [ "$n" -gt 0 ]; then dup=1 + fi + done < <(printf '%s\n' ${lib.concatMapStringsSep " " lib.escapeShellArg + (lib.mapAttrsToList + (name: entries: "${name}:${lib.concatMapStringsSep " " (m: m.name) entries}") + profiles)}) + if [ -n "$exact" ] && [ -z "$exactdup" ]; then echo "$exact"; exit 0; fi + if [ -n "$best" ] && [ -z "$dup" ]; then echo "$best"; exit 0; fi + exit 1 ;; *) - echo "usage: nomarchy-display-profile [list|active|apply |base]" >&2 + echo "usage: nomarchy-display-profile [list|active|apply |base|match ...]" >&2 exit 64 ;; esac ''; + # Hotplug auto-switch (opt-in via the menu's Auto-switch row → + # settings.displayProfileAuto, read LIVE on every output change so the + # toggle is instant, no rebuild). Polls like the keyboard watcher — + # exec-once, not a systemd unit (graphical-session-bound units raced + # Hyprland's IPC on relogin; see the waybar note below). Only reacts to + # CHANGES after session start: the baked config already encodes the + # last choice, and login must not fight a deliberate manual pick. + # `match` picks deterministically (exact set, else unambiguous largest + # subset); applying via the tool persists the concrete profile, so the + # next rebuild bakes what auto chose. + displayProfileWatch = pkgs.writeShellScriptBin "nomarchy-display-profile-watch" '' + set -u + auto_on() { + v=$(${sync} get settings.displayProfileAuto 2>/dev/null) || v=false + case "$v" in true|True) return 0 ;; *) return 1 ;; esac + } + outputs() { hyprctl monitors all -j 2>/dev/null | jq -r '.[].name' | sort; } + prev=$(outputs) + while :; do + sleep 3 + cur=$(outputs) + [ "$cur" = "$prev" ] && continue + prev=$cur + [ -n "$cur" ] || continue + auto_on || continue + target=$(nomarchy-display-profile match $cur) || continue + [ "$target" = "$(nomarchy-display-profile active)" ] && continue + nomarchy-display-profile apply "$target" + done + ''; + # Candidate layouts offered by the new-keyboard picker: the session # layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout # session) plus the extra nomarchy.keyboard.layouts pool. The pool is @@ -228,7 +289,8 @@ in # the clean pkill a theme switch now does — respawns the bar instead # of orphaning the session bar-less. ] ++ lib.optional config.nomarchy.waybar.enable "nomarchy-waybar" - ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch"; + ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch" + ++ lib.optional (profiles != { }) "${displayProfileWatch}/bin/nomarchy-display-profile-watch"; # ── Theme-driven look ────────────────────────────────────────── # These flow from theme-state.json and stay at NORMAL priority: @@ -375,6 +437,8 @@ in # 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 - # The display-profile switcher (the menu's Profiles row gates on it). - ++ lib.optional (config.nomarchy.hyprland.enable && profiles != { }) displayProfileTool; + # The display-profile switcher (the menu's Profiles row gates on it) + # + its hotplug watcher, on PATH for debugging like the keyboard one. + ++ lib.optionals (config.nomarchy.hyprland.enable && profiles != { }) + [ displayProfileTool displayProfileWatch ]; } diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 2d08ab7..0ee5b49 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -394,11 +394,26 @@ ${themeRows} if [ "$p" = "$active" ]; then printf '● %s\n' "$p"; else printf '○ %s\n' "$p"; fi done printf 'Base layout\n' + if [ "$(nomarchy-theme-sync get settings.displayProfileAuto 2>/dev/null)" = true ] + then printf 'Auto-switch (on)\n' + else printf 'Auto-switch (off)\n' + fi printf '%s\n' "$BACK" } | rofi -dmenu -p Profiles ) || exit 0 case "$choice" in "$BACK") exec "$0" display ;; "Base layout") exec nomarchy-display-profile base ;; + *"Auto-switch"*) + # Instant in-flake flag; the watcher reads it live on the next + # output change — no rebuild, no unit juggling. + if [ "$(nomarchy-theme-sync get settings.displayProfileAuto 2>/dev/null)" = true ]; then + nomarchy-theme-sync --quiet set settings.displayProfileAuto false --no-switch + notify-send "Display profiles" "Auto-switch off — profiles change only from this menu." + else + nomarchy-theme-sync --quiet set settings.displayProfileAuto true --no-switch + notify-send "Display profiles" "Auto-switch on — plugging/unplugging outputs picks the matching profile." + fi + exec "$0" display-profile ;; *) exec nomarchy-display-profile apply "''${choice#[●○] }" ;; esac ;; diff --git a/templates/downstream/home.nix b/templates/downstream/home.nix index f289a1f..bbf1e90 100644 --- a/templates/downstream/home.nix +++ b/templates/downstream/home.nix @@ -42,7 +42,9 @@ # ]; # undocked = [ { name = "eDP-1"; position = "0x0"; } ]; # }; # applied instantly + remembered in the - # # flake state (settings.displayProfile) + # # flake state (settings.displayProfile); + # # the menu's Auto-switch row makes dock/ + # # undock pick the matching profile # # nomarchy.keyboard.devices = { # a specific keyboard's own layout # "keychron-keychron-k2" = { layout = "de"; };