feat(displays): hotplug auto-switch for display profiles (item 15b)
All checks were successful
Check / eval (push) Successful in 2m57s
All checks were successful
Check / eval (push) Successful in 2m57s
nomarchy-display-profile gains `match <outputs>`: an exact set match wins, else the largest profile whose named outputs are all connected; any tie prints nothing — the watcher must never flap between ambiguous layouts. nomarchy-display-profile-watch (exec-once when profiles are declared, the keyboard-watcher poll pattern — session units race Hyprland IPC on relogin) reacts to output-set CHANGES only, reads settings.displayProfileAuto from the live state each time (the menu's new Auto-switch row toggles it instantly, no rebuild), and applies via the tool — so an auto-pick persists a concrete profile that the next rebuild bakes. A separate boolean rather than an "auto" pseudo-profile for exactly that bake semantics. Verified: V0 (flake check); V1 — a scratch 4-profile downstream builds; tool + watcher pass bash -n; the match matrix exercised end-to-end: three exact picks, an unambiguous-superset pick, a tie -> none, a no-fit -> none; the watcher is exec-once'd in the rendered config; the menu renders the Auto-switch row. Remains: V3 real dock/undock (HARDWARE-QUEUE, rides the slice-a entry). Slice c (workspace binding) stays, tagged optional. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.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.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.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.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 |
|
| `nomarchy.package` | overlay's `nomarchy-theme-sync` | The theme/state tool package, overridable if you fork it |
|
||||||
|
|
||||||
|
|||||||
@@ -71,12 +71,14 @@ the *same* outputs. **Plan (designed 2026-07-05, iteration #27):**
|
|||||||
- **Slices:** (a) ✓ shipped 2026-07-05 (iteration #28): option +
|
- **Slices:** (a) ✓ shipped 2026-07-05 (iteration #28): option +
|
||||||
overlay (pure `modules/home/monitor-rules.nix`, unit-tested by
|
overlay (pure `modules/home/monitor-rules.nix`, unit-tested by
|
||||||
checks.display-profiles) + `nomarchy-display-profile` applier + menu
|
checks.display-profiles) + `nomarchy-display-profile` applier + menu
|
||||||
Profiles row + template example — V3 (real dock) queued; (b)
|
Profiles row + template example — V3 (real dock) queued; (b) ✓
|
||||||
opt-in hotplug auto-switch: a socket2 watcher on monitoradded/
|
shipped 2026-07-05 (iteration #29): hotplug auto-switch — `match`
|
||||||
removed picks the profile whose named-output set matches connected
|
subcommand (exact set, else unambiguous largest subset, ties =
|
||||||
outputs (exact match, else largest subset, ties = no switch) —
|
none) + a poll watcher (exec-once, the keyboard-watcher pattern)
|
||||||
decide the flag shape (`settings.displayProfile = "auto"` vs a
|
gated live on `settings.displayProfileAuto` (a separate boolean,
|
||||||
separate boolean) at implementation; (c) optional per-profile
|
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.
|
`workspaces = { "1" = "DP-3"; }` → Hyprland `workspace` keywords.
|
||||||
- **Non-goals:** no kanshi (fights Hyprland's output management —
|
- **Non-goals:** no kanshi (fights Hyprland's output management —
|
||||||
decided in ROADMAP), nwg-displays stays a find-the-values helper.
|
decided in ROADMAP), nwg-displays stays a find-the-values helper.
|
||||||
|
|||||||
@@ -148,6 +148,11 @@ QA machine), the **T14s** (webcam case).
|
|||||||
overlay); "Base layout" restores; a menu resolution pick made
|
overlay); "Base layout" restores; a menu resolution pick made
|
||||||
earlier must NOT re-enable a profile-disabled panel after the
|
earlier must NOT re-enable a profile-disabled panel after the
|
||||||
next rebuild.
|
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 (item 13, final slice)** — SUPER+CTRL+P (or Tools ›
|
||||||
Color picker): hyprpicker's zoom loupe appears; click a pixel →
|
Color picker): hyprpicker's zoom loupe appears; click a pixel →
|
||||||
toast shows the hex and `wl-paste` yields it; Esc cancels with no
|
toast shows the hex and `wl-paste` yields it; Esc cancels with no
|
||||||
|
|||||||
@@ -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 <outputs>` (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)
|
## 2026-07-05 — display profiles slice a (iteration #28, item 15a)
|
||||||
- **Task:** BACKLOG NEXT#15 slice a, per the #27 design: option +
|
- **Task:** BACKLOG NEXT#15 slice a, per the #27 design: option +
|
||||||
overlay + applier + menu + template example.
|
overlay + applier + menu + template example.
|
||||||
|
|||||||
@@ -94,12 +94,73 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
|||||||
${sync} --quiet set settings.displayProfile "" --no-switch
|
${sync} --quiet set settings.displayProfile "" --no-switch
|
||||||
hyprctl reload >/dev/null 2>&1
|
hyprctl reload >/dev/null 2>&1
|
||||||
notify-send "Display profile" "Base layout restored." ;;
|
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 <name>|base]" >&2
|
echo "usage: nomarchy-display-profile [list|active|apply <name>|base|match <output>...]" >&2
|
||||||
exit 64 ;;
|
exit 64 ;;
|
||||||
esac
|
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
|
# Candidate layouts offered by the new-keyboard picker: the session
|
||||||
# layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
|
# layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
|
||||||
# session) plus the extra nomarchy.keyboard.layouts pool. The pool is
|
# 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
|
# the clean pkill a theme switch now does — respawns the bar instead
|
||||||
# of orphaning the session bar-less.
|
# of orphaning the session bar-less.
|
||||||
] ++ lib.optional config.nomarchy.waybar.enable "nomarchy-waybar"
|
] ++ 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 ──────────────────────────────────────────
|
# ── Theme-driven look ──────────────────────────────────────────
|
||||||
# These flow from theme-state.json and stay at NORMAL priority:
|
# 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
|
# 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.
|
# runnable by name for debugging — Hyprland still exec-once's it by store path.
|
||||||
++ lib.optional (config.nomarchy.hyprland.enable && kbAutoSwitch) keyboardWatch
|
++ lib.optional (config.nomarchy.hyprland.enable && kbAutoSwitch) keyboardWatch
|
||||||
# The display-profile switcher (the menu's Profiles row gates on it).
|
# The display-profile switcher (the menu's Profiles row gates on it)
|
||||||
++ lib.optional (config.nomarchy.hyprland.enable && profiles != { }) displayProfileTool;
|
# + its hotplug watcher, on PATH for debugging like the keyboard one.
|
||||||
|
++ lib.optionals (config.nomarchy.hyprland.enable && profiles != { })
|
||||||
|
[ displayProfileTool displayProfileWatch ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,11 +394,26 @@ ${themeRows}
|
|||||||
if [ "$p" = "$active" ]; then printf '● %s\n' "$p"; else printf '○ %s\n' "$p"; fi
|
if [ "$p" = "$active" ]; then printf '● %s\n' "$p"; else printf '○ %s\n' "$p"; fi
|
||||||
done
|
done
|
||||||
printf 'Base layout\n'
|
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"
|
printf '%s\n' "$BACK"
|
||||||
} | rofi -dmenu -p Profiles ) || exit 0
|
} | rofi -dmenu -p Profiles ) || exit 0
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
"$BACK") exec "$0" display ;;
|
"$BACK") exec "$0" display ;;
|
||||||
"Base layout") exec nomarchy-display-profile base ;;
|
"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#[●○] }" ;;
|
*) exec nomarchy-display-profile apply "''${choice#[●○] }" ;;
|
||||||
esac ;;
|
esac ;;
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,9 @@
|
|||||||
# ];
|
# ];
|
||||||
# undocked = [ { name = "eDP-1"; position = "0x0"; } ];
|
# undocked = [ { name = "eDP-1"; position = "0x0"; } ];
|
||||||
# }; # applied instantly + remembered in the
|
# }; # 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
|
# nomarchy.keyboard.devices = { # a specific keyboard's own layout
|
||||||
# "keychron-keychron-k2" = { layout = "de"; };
|
# "keychron-keychron-k2" = { layout = "de"; };
|
||||||
|
|||||||
Reference in New Issue
Block a user