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:
67
modules/home/monitor-rules.nix
Normal file
67
modules/home/monitor-rules.nix
Normal file
@@ -0,0 +1,67 @@
|
||||
# Pure monitor-rule composition, split out of hyprland.nix so
|
||||
# checks.display-profiles can exercise the overlay semantics directly —
|
||||
# no Home Manager evaluation, no state-file fixture.
|
||||
{ lib }:
|
||||
|
||||
rec {
|
||||
# Skeleton for an entry built outside the option type (mirrors the
|
||||
# monitorType defaults in options.nix).
|
||||
defaults = {
|
||||
resolution = "preferred"; position = "auto"; scale = 1;
|
||||
transform = null; mirror = null; bitdepth = null; vrr = null; extra = "";
|
||||
};
|
||||
|
||||
# An entry -> a Hyprland `monitor` rule. Unset optional fields are
|
||||
# omitted; `resolution = "disable"` collapses to the short form.
|
||||
rule = 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
|
||||
);
|
||||
|
||||
# The full overlay, three layers:
|
||||
#
|
||||
# 1. base (nomarchy.monitors) with the ACTIVE display profile's entries
|
||||
# (settings.displayProfile naming a nomarchy.displayProfiles key)
|
||||
# replacing base entries WHOLE, by name — a profile entry is a
|
||||
# complete statement about that output. Outputs a profile doesn't
|
||||
# name keep their base rules; a cleared/unknown/non-string value
|
||||
# ("" after `nomarchy-display-profile base`, or hand-edited junk —
|
||||
# the validator only warns) means base config only.
|
||||
#
|
||||
# 2. Menu-remembered per-output resolutions (settings.monitors:
|
||||
# output-name -> "WxH@R", written instantly by the Display menu)
|
||||
# overlaid field-level 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 pick wins over a hand-set resolution
|
||||
# (it's the explicit live action) — EXCEPT onto disabled entries: a
|
||||
# stale pick, made while the output was enabled, must not resurrect
|
||||
# a panel the active profile (or the base config) disables.
|
||||
#
|
||||
# 3. Anything still uncovered falls to the wildcard (hyprland.nix
|
||||
# prepends it).
|
||||
resolve = { base, profiles, active, resOverrides }:
|
||||
let
|
||||
activeName = if builtins.isString active then active else "";
|
||||
profileEntries = profiles.${activeName} or [ ];
|
||||
profileNames = map (m: m.name) profileEntries;
|
||||
baseMonitors =
|
||||
lib.filter (m: ! lib.elem m.name profileNames) base
|
||||
++ profileEntries;
|
||||
declaredNames = map (m: m.name) baseMonitors;
|
||||
in
|
||||
map (m: if resOverrides ? ${m.name} && m.resolution != "disable"
|
||||
then m // { resolution = resOverrides.${m.name}; }
|
||||
else m)
|
||||
baseMonitors
|
||||
++ lib.mapAttrsToList
|
||||
(name: res: defaults // { inherit name; resolution = res; })
|
||||
(lib.filterAttrs (name: _: ! lib.elem name declaredNames) resOverrides);
|
||||
}
|
||||
Reference in New Issue
Block a user