feat(displays): display profiles — docked/undocked switching (item 15a)
All checks were successful
Check / eval (push) Successful in 3m4s
All checks were successful
Check / eval (push) Successful in 3m4s
nomarchy.displayProfiles: named layouts for the same outputs, each a list of nomarchy.monitors-style entries. The active profile lives in the in-flake state (settings.displayProfile) on the night-light pattern: nomarchy-display-profile apply <name> fires the baked hyprctl rules instantly and writes the state with --no-switch; the next rebuild bakes the profile's entries over nomarchy.monitors whole-by-name. Monitor-rule composition is extracted to pure monitor-rules.nix (renderer + 3-layer resolve, incl. a disable-guard so a stale menu resolution pick can't resurrect a profile-disabled panel, and junk state degrading to base config instead of an eval error) — because a writeText/toFile state fixture is unreadable at flake-check eval time, the new checks.display-profiles unit-tests the pure function directly. Menu: System › Display gains a self-gated "Profiles ›" row (●/○ active mark, Base layout to clear); commented template example + README row. Verified: V0 (flake check, forcing the new check's asserts); V1 — a scratch downstream through lib.mkFlake builds: the generated tool passes bash -n with correct rules (disable/vrr/float scale), the baked hyprland.conf shows the exact expected overlay, the menu renders, and list/usage smoke-test clean. Remains: V3 live switching on a dock (HARDWARE-QUEUE). Slices b (hotplug auto-switch) + c (workspace binding) stay in item 15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,44 +46,59 @@ let
|
||||
keybinds = import ./keybinds.nix;
|
||||
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
|
||||
|
||||
# A nomarchy.monitors entry -> a Hyprland `monitor` rule. Unset optional
|
||||
# fields are omitted; `resolution = "disable"` collapses to the short form.
|
||||
monitorRule = m:
|
||||
if m.resolution == "disable" then "${m.name}, disable"
|
||||
else lib.concatStringsSep ", " (
|
||||
[ m.name m.resolution (toString m.position) (toString m.scale) ]
|
||||
++ lib.optionals (m.transform != null) [ "transform" (toString m.transform) ]
|
||||
++ lib.optionals (m.mirror != null) [ "mirror" m.mirror ]
|
||||
++ lib.optionals (m.bitdepth != null) [ "bitdepth" (toString m.bitdepth) ]
|
||||
++ lib.optionals (m.vrr != null) [ "vrr" (toString m.vrr) ]
|
||||
++ lib.optional (m.extra != "") m.extra
|
||||
);
|
||||
# Monitor-rule composition (rendering + the display-profile/resolution
|
||||
# overlays) lives in ./monitor-rules.nix — pure, so
|
||||
# checks.display-profiles unit-tests the overlay semantics directly.
|
||||
# The layering story (profiles replace whole-by-name, menu resolution
|
||||
# picks apply field-level, disable-guard) is documented there.
|
||||
monitorLib = import ./monitor-rules.nix { inherit lib; };
|
||||
monitorRule = monitorLib.rule;
|
||||
|
||||
# Menu-remembered per-output resolutions (settings.monitors: output-name ->
|
||||
# "WxH@R", written instantly by the Display menu — see rofi.nix) overlaid onto
|
||||
# the declarative nomarchy.monitors by name: a declared output keeps its
|
||||
# position/scale/etc and only its resolution changes; an output covered solely
|
||||
# by the `,preferred,auto,1` wildcard (e.g. the laptop's built-in panel)
|
||||
# becomes a new rule with default position/scale. The menu writes the in-flake
|
||||
# state with --no-switch (no rebuild) and applies the mode live via hyprctl;
|
||||
# this bakes the same choice on the next rebuild — the monitor twin of the
|
||||
# keyboard graduation. The menu pick wins over a hand-set resolution (it's the
|
||||
# explicit live action); other declared fields are untouched.
|
||||
resOverrides = config.nomarchy.settings.monitors;
|
||||
# Skeleton for an output that's only menu-set (mirrors the monitorType defaults).
|
||||
monitorDefaults = {
|
||||
resolution = "preferred"; position = "auto"; scale = 1;
|
||||
transform = null; mirror = null; bitdepth = null; vrr = null; extra = "";
|
||||
# The night-light pattern: nomarchy-display-profile applies rules live
|
||||
# via hyprctl and writes settings.displayProfile with --no-switch;
|
||||
# resolve bakes the same choice here at the next rebuild.
|
||||
profiles = config.nomarchy.displayProfiles;
|
||||
resolvedMonitors = monitorLib.resolve {
|
||||
base = config.nomarchy.monitors;
|
||||
inherit profiles;
|
||||
active = config.nomarchy.settings.displayProfile or "";
|
||||
resOverrides = config.nomarchy.settings.monitors;
|
||||
};
|
||||
declaredNames = map (m: m.name) config.nomarchy.monitors;
|
||||
resolvedMonitors =
|
||||
map (m: if resOverrides ? ${m.name}
|
||||
then m // { resolution = resOverrides.${m.name}; }
|
||||
else m)
|
||||
config.nomarchy.monitors
|
||||
++ lib.mapAttrsToList
|
||||
(name: res: monitorDefaults // { inherit name; resolution = res; })
|
||||
(lib.filterAttrs (name: _: ! lib.elem name declaredNames) resOverrides);
|
||||
|
||||
# Profile applier — on PATH only when profiles are declared (the menu
|
||||
# row self-gates on it). apply: the profile's rules live via hyprctl +
|
||||
# the in-flake state write; base: clear the state, then hyprctl reload
|
||||
# re-applies the generated config's rules.
|
||||
displayProfileTool = pkgs.writeShellScriptBin "nomarchy-display-profile" ''
|
||||
case "''${1:-}" in
|
||||
list)
|
||||
${lib.concatMapStringsSep "\n" (n: " echo ${lib.escapeShellArg n}") (lib.attrNames profiles)} ;;
|
||||
active)
|
||||
cur=$(${sync} get settings.displayProfile 2>/dev/null) || cur=
|
||||
echo "''${cur:-none}" ;;
|
||||
apply)
|
||||
case "''${2:-}" in
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
||||
(name: entries:
|
||||
" ${lib.escapeShellArg name})\n"
|
||||
+ lib.concatMapStringsSep "\n"
|
||||
(m: " hyprctl keyword monitor ${lib.escapeShellArg (monitorRule m)} >/dev/null 2>&1")
|
||||
entries
|
||||
+ " ;;")
|
||||
profiles)}
|
||||
*) echo "unknown profile '$2' — try: nomarchy-display-profile list" >&2; exit 64 ;;
|
||||
esac
|
||||
${sync} --quiet set settings.displayProfile "$2" --no-switch
|
||||
notify-send "Display profile" "Applied: $2" ;;
|
||||
base)
|
||||
${sync} --quiet set settings.displayProfile "" --no-switch
|
||||
hyprctl reload >/dev/null 2>&1
|
||||
notify-send "Display profile" "Base layout restored." ;;
|
||||
*)
|
||||
echo "usage: nomarchy-display-profile [list|active|apply <name>|base]" >&2
|
||||
exit 64 ;;
|
||||
esac
|
||||
'';
|
||||
|
||||
# Candidate layouts offered by the new-keyboard picker: the session
|
||||
# layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
|
||||
@@ -359,5 +374,7 @@ in
|
||||
[ 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;
|
||||
++ 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user