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:
@@ -228,6 +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.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 |
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,10 @@ the *same* outputs. **Plan (designed 2026-07-05, iteration #27):**
|
|||||||
`hyprctl keyword monitor <rule>` per entry (rules baked into the
|
`hyprctl keyword monitor <rule>` per entry (rules baked into the
|
||||||
dispatcher at build, like everything else in rofi.nix) + state write
|
dispatcher at build, like everything else in rofi.nix) + state write
|
||||||
+ toast. Outputs the profile doesn't name keep their base rules.
|
+ toast. Outputs the profile doesn't name keep their base rules.
|
||||||
- **Slices:** (a) option + overlay + menu picker + template example
|
- **Slices:** (a) ✓ shipped 2026-07-05 (iteration #28): option +
|
||||||
(opt-in ⇒ commented `templates/downstream/home.nix` block) — V1 +
|
overlay (pure `modules/home/monitor-rules.nix`, unit-tested by
|
||||||
a checks.* grep of the rendered hyprland conf, V3 real dock; (b)
|
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/
|
opt-in hotplug auto-switch: a socket2 watcher on monitoradded/
|
||||||
removed picks the profile whose named-output set matches connected
|
removed picks the profile whose named-output set matches connected
|
||||||
outputs (exact match, else largest subset, ties = no switch) —
|
outputs (exact match, else largest subset, ties = no switch) —
|
||||||
|
|||||||
@@ -140,6 +140,14 @@ QA machine), the **T14s** (webcam case).
|
|||||||
now reflects the real state (the `= true` comparison fix).
|
now reflects the real state (the `= true` comparison fix).
|
||||||
— 2026-07-04 Bernardo: PASS — theme changes committed and the
|
— 2026-07-04 Bernardo: PASS — theme changes committed and the
|
||||||
toggle self-committed.
|
toggle self-committed.
|
||||||
|
- [ ] **Display profiles, slice a (item 15)** — declare two
|
||||||
|
`nomarchy.displayProfiles` (e.g. docked disables eDP-1 + arranges
|
||||||
|
the externals; undocked re-enables it), `home-update`, then:
|
||||||
|
System › Display › Profiles lists them (● marks active); applying
|
||||||
|
is instant (no rebuild) and survives a relogin (the baked
|
||||||
|
overlay); "Base layout" restores; a menu resolution pick made
|
||||||
|
earlier must NOT re-enable a profile-disabled panel after the
|
||||||
|
next rebuild.
|
||||||
- [ ] **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,32 @@ Template:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- **Did:** nomarchy.displayProfiles (attrsOf (listOf monitorType));
|
||||||
|
monitor logic extracted to pure modules/home/monitor-rules.nix
|
||||||
|
(rule renderer + 3-layer resolve: profile replaces whole-by-name →
|
||||||
|
resolution picks field-level with the disable-guard → wildcard),
|
||||||
|
hyprland.nix consumes it and gains nomarchy-display-profile
|
||||||
|
(list/active/apply/base; apply = baked hyprctl rules + state write
|
||||||
|
--no-switch, base = clear + hyprctl reload) on PATH only when
|
||||||
|
profiles exist; menu: Display gains a gated "Profiles ›" row + new
|
||||||
|
display-profile mode (●/○ active mark, Base layout row); template
|
||||||
|
example + README row. checks.display-profiles unit-tests resolve
|
||||||
|
(incl. junk-state degradation) — the first HM-eval fixture attempt
|
||||||
|
hit "path not valid" (eval store won't realise writeText/toFile
|
||||||
|
under flake check), hence the pure-function extraction.
|
||||||
|
- **Verified:** V0 (check's asserts force at eval); V1 — scratch
|
||||||
|
downstream through lib.mkFlake builds: tool bash -n + correct baked
|
||||||
|
rules (disable, vrr, floats), hyprland.conf shows the exact overlay
|
||||||
|
(wildcard + kept base + disabled panel + profile output), menu bash
|
||||||
|
-n, list/usage smoke. Live switching needs a session+dock → V3.
|
||||||
|
- **Pending:** V3 queued. Slices b (hotplug auto-switch) and c
|
||||||
|
(workspace binding) remain in the item.
|
||||||
|
- **Next suggestion:** #16 greeter theming, or 15b if momentum on
|
||||||
|
displays is preferred.
|
||||||
|
|
||||||
## 2026-07-05 — display-profiles design pass (iteration #27, item 15, no code)
|
## 2026-07-05 — display-profiles design pass (iteration #27, item 15, no code)
|
||||||
- **Task:** BACKLOG NEXT#15 — the entry itself demands "design first,
|
- **Task:** BACKLOG NEXT#15 — the entry itself demands "design first,
|
||||||
write the plan into this entry before coding". Backlog-only iteration.
|
write the plan into this entry before coding". Backlog-only iteration.
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ here the moment a debugging session teaches you something a future
|
|||||||
iteration would otherwise rediscover.
|
iteration would otherwise rediscover.
|
||||||
|
|
||||||
## Testing & VM recipes
|
## Testing & VM recipes
|
||||||
|
- A checks.* fixture CANNOT be a writeText/toFile state file read at
|
||||||
|
eval time ("path … is not valid" — flake check's eval store won't
|
||||||
|
realise it): extract the logic into a pure importable file and
|
||||||
|
unit-test THAT (monitor-rules.nix / checks.display-profiles is the
|
||||||
|
pattern).
|
||||||
- CI (`.gitea/workflows/check.yml`) is **eval-tier only**: the act_runner
|
- CI (`.gitea/workflows/check.yml`) is **eval-tier only**: the act_runner
|
||||||
is a docker container (no systemd, no /dev/kvm). Container gotchas are
|
is a docker container (no systemd, no /dev/kvm). Container gotchas are
|
||||||
documented in the workflow header (single-user Nix + nixbld users,
|
documented in the workflow header (single-user Nix + nixbld users,
|
||||||
|
|||||||
43
flake.nix
43
flake.nix
@@ -332,6 +332,49 @@
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Unit test of the monitor-rule overlay (modules/home/
|
||||||
|
# monitor-rules.nix — pure, imported directly, no HM eval): the
|
||||||
|
# active profile must replace base entries whole by name, leave
|
||||||
|
# unnamed base outputs alone, and the disable-guard must stop a
|
||||||
|
# stale menu resolution pick (settings.monitors) resurrecting a
|
||||||
|
# panel the profile disables. Junk in settings.displayProfile
|
||||||
|
# degrades to base config, never an eval error.
|
||||||
|
display-profiles =
|
||||||
|
let
|
||||||
|
inherit (nixpkgs) lib;
|
||||||
|
monitorLib = import ./modules/home/monitor-rules.nix { inherit lib; };
|
||||||
|
entry = a: monitorLib.defaults // a;
|
||||||
|
fixture = {
|
||||||
|
base = [
|
||||||
|
(entry { name = "eDP-1"; position = "0x0"; })
|
||||||
|
(entry { name = "HDMI-A-1"; position = "auto-right"; })
|
||||||
|
];
|
||||||
|
profiles.docked = [
|
||||||
|
(entry { name = "eDP-1"; resolution = "disable"; })
|
||||||
|
(entry { name = "DP-3"; position = "0x0"; })
|
||||||
|
];
|
||||||
|
resOverrides."eDP-1" = "1920x1080@60"; # stale pick — must stay dead
|
||||||
|
};
|
||||||
|
rulesFor = active: map monitorLib.rule
|
||||||
|
(monitorLib.resolve (fixture // { inherit active; }));
|
||||||
|
docked = rulesFor "docked";
|
||||||
|
junk = rulesFor 42; # hand-edited state (validator only warns)
|
||||||
|
expect = cond: msg: lib.assertMsg cond
|
||||||
|
"display-profiles: ${msg} (rendered: ${builtins.toJSON docked})";
|
||||||
|
ok =
|
||||||
|
expect (lib.elem "eDP-1, disable" docked)
|
||||||
|
"the profile did not disable eDP-1"
|
||||||
|
&& expect (! lib.any (lib.hasInfix "1920x1080") docked)
|
||||||
|
"a stale resolution pick resurrected the disabled panel"
|
||||||
|
&& expect (lib.elem "DP-3, preferred, 0x0, 1" docked)
|
||||||
|
"the profile-only output's rule is missing"
|
||||||
|
&& expect (lib.elem "HDMI-A-1, preferred, auto-right, 1" docked)
|
||||||
|
"an unnamed base output lost its rule"
|
||||||
|
&& expect (lib.elem "eDP-1, 1920x1080@60, 0x0, 1" junk)
|
||||||
|
"junk active profile must degrade to base config (with the pick applied)";
|
||||||
|
in
|
||||||
|
assert ok; pkgs.runCommand "nomarchy-display-profiles" { } "touch $out";
|
||||||
|
|
||||||
# The low-battery watcher's crossing logic: one toast per downward
|
# The low-battery watcher's crossing logic: one toast per downward
|
||||||
# crossing (normal at 25%, critical at 10%), re-armed by charging.
|
# crossing (normal at 25%, critical at 10%), re-armed by charging.
|
||||||
# test_power fakes a battery whose capacity/status we script;
|
# test_power fakes a battery whose capacity/status we script;
|
||||||
|
|||||||
@@ -46,44 +46,59 @@ let
|
|||||||
keybinds = import ./keybinds.nix;
|
keybinds = import ./keybinds.nix;
|
||||||
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
|
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
|
||||||
|
|
||||||
# A nomarchy.monitors entry -> a Hyprland `monitor` rule. Unset optional
|
# Monitor-rule composition (rendering + the display-profile/resolution
|
||||||
# fields are omitted; `resolution = "disable"` collapses to the short form.
|
# overlays) lives in ./monitor-rules.nix — pure, so
|
||||||
monitorRule = m:
|
# checks.display-profiles unit-tests the overlay semantics directly.
|
||||||
if m.resolution == "disable" then "${m.name}, disable"
|
# The layering story (profiles replace whole-by-name, menu resolution
|
||||||
else lib.concatStringsSep ", " (
|
# picks apply field-level, disable-guard) is documented there.
|
||||||
[ m.name m.resolution (toString m.position) (toString m.scale) ]
|
monitorLib = import ./monitor-rules.nix { inherit lib; };
|
||||||
++ lib.optionals (m.transform != null) [ "transform" (toString m.transform) ]
|
monitorRule = monitorLib.rule;
|
||||||
++ 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
|
|
||||||
);
|
|
||||||
|
|
||||||
# Menu-remembered per-output resolutions (settings.monitors: output-name ->
|
# The night-light pattern: nomarchy-display-profile applies rules live
|
||||||
# "WxH@R", written instantly by the Display menu — see rofi.nix) overlaid onto
|
# via hyprctl and writes settings.displayProfile with --no-switch;
|
||||||
# the declarative nomarchy.monitors by name: a declared output keeps its
|
# resolve bakes the same choice here at the next rebuild.
|
||||||
# position/scale/etc and only its resolution changes; an output covered solely
|
profiles = config.nomarchy.displayProfiles;
|
||||||
# by the `,preferred,auto,1` wildcard (e.g. the laptop's built-in panel)
|
resolvedMonitors = monitorLib.resolve {
|
||||||
# becomes a new rule with default position/scale. The menu writes the in-flake
|
base = config.nomarchy.monitors;
|
||||||
# state with --no-switch (no rebuild) and applies the mode live via hyprctl;
|
inherit profiles;
|
||||||
# this bakes the same choice on the next rebuild — the monitor twin of the
|
active = config.nomarchy.settings.displayProfile or "";
|
||||||
# keyboard graduation. The menu pick wins over a hand-set resolution (it's the
|
resOverrides = config.nomarchy.settings.monitors;
|
||||||
# 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 = "";
|
|
||||||
};
|
};
|
||||||
declaredNames = map (m: m.name) config.nomarchy.monitors;
|
|
||||||
resolvedMonitors =
|
# Profile applier — on PATH only when profiles are declared (the menu
|
||||||
map (m: if resOverrides ? ${m.name}
|
# row self-gates on it). apply: the profile's rules live via hyprctl +
|
||||||
then m // { resolution = resOverrides.${m.name}; }
|
# the in-flake state write; base: clear the state, then hyprctl reload
|
||||||
else m)
|
# re-applies the generated config's rules.
|
||||||
config.nomarchy.monitors
|
displayProfileTool = pkgs.writeShellScriptBin "nomarchy-display-profile" ''
|
||||||
++ lib.mapAttrsToList
|
case "''${1:-}" in
|
||||||
(name: res: monitorDefaults // { inherit name; resolution = res; })
|
list)
|
||||||
(lib.filterAttrs (name: _: ! lib.elem name declaredNames) resOverrides);
|
${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
|
# 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
|
||||||
@@ -359,5 +374,7 @@ in
|
|||||||
[ pkgs.nwg-displays ]
|
[ pkgs.nwg-displays ]
|
||||||
# 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).
|
||||||
|
++ lib.optional (config.nomarchy.hyprland.enable && profiles != { }) displayProfileTool;
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
@@ -237,6 +237,31 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
displayProfiles = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.listOf monitorType);
|
||||||
|
default = { };
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
docked = [
|
||||||
|
{ name = "eDP-1"; resolution = "disable"; }
|
||||||
|
{ name = "DP-3"; position = "0x0"; }
|
||||||
|
{ name = "DP-4"; position = "auto-right"; }
|
||||||
|
];
|
||||||
|
undocked = [ { name = "eDP-1"; position = "0x0"; } ];
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
Named display layouts for the same outputs (docked, undocked, …),
|
||||||
|
each a list of nomarchy.monitors-style entries. Switch from the menu
|
||||||
|
(System › Display › Profiles) or `nomarchy-display-profile apply
|
||||||
|
<name>`: the profile's rules apply instantly via hyprctl and the
|
||||||
|
choice persists in the in-flake state (settings.displayProfile), so
|
||||||
|
the next rebuild bakes the active profile's entries over
|
||||||
|
nomarchy.monitors by name. Outputs a profile doesn't name keep
|
||||||
|
their base rules.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
# ── Component toggles ──────────────────────────────────────────
|
# ── Component toggles ──────────────────────────────────────────
|
||||||
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
|
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
|
||||||
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
|
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
|
||||||
|
|||||||
@@ -303,13 +303,17 @@ ${themeRows}
|
|||||||
|| { notify-send "Display" "Hyprland is not running."; exit 0; }
|
|| { notify-send "Display" "Hyprland is not running."; exit 0; }
|
||||||
mons=$(hyprctl monitors -j)
|
mons=$(hyprctl monitors -j)
|
||||||
|
|
||||||
# Choose the output (skip the chooser when only one is connected).
|
# Choose the output (skip the chooser when only one is connected and
|
||||||
if [ "$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)" -gt 1 ]; then
|
# there are no profiles — the Profiles row needs the chooser to live in).
|
||||||
|
profs=; command -v nomarchy-display-profile >/dev/null 2>&1 && profs=1
|
||||||
|
if [ -n "$profs" ] || [ "$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)" -gt 1 ]; then
|
||||||
name=$( {
|
name=$( {
|
||||||
|
[ -n "$profs" ] && printf 'Profiles ›\n'
|
||||||
printf '%s' "$mons" | jq -r '.[] | "\(.name) · \(.width)x\(.height)@\(.refreshRate|round)Hz"'
|
printf '%s' "$mons" | jq -r '.[] | "\(.name) · \(.width)x\(.height)@\(.refreshRate|round)Hz"'
|
||||||
printf '%s\n' "$BACK"
|
printf '%s\n' "$BACK"
|
||||||
} | rofi -dmenu -p Display ) || exit 0
|
} | rofi -dmenu -p Display ) || exit 0
|
||||||
[ "$name" = "$BACK" ] && exec "$0" system
|
[ "$name" = "$BACK" ] && exec "$0" system
|
||||||
|
[ "$name" = "Profiles ›" ] && exec "$0" display-profile
|
||||||
name=''${name%% ·*} # strip the " · WxH@R" hint
|
name=''${name%% ·*} # strip the " · WxH@R" hint
|
||||||
else
|
else
|
||||||
name=$(printf '%s' "$mons" | jq -r '.[0].name')
|
name=$(printf '%s' "$mons" | jq -r '.[0].name')
|
||||||
@@ -377,6 +381,27 @@ ${themeRows}
|
|||||||
*"Record screen"*) exec nomarchy-record start screen ;;
|
*"Record screen"*) exec nomarchy-record start screen ;;
|
||||||
esac ;;
|
esac ;;
|
||||||
|
|
||||||
|
display-profile)
|
||||||
|
# Named multi-output layouts (nomarchy.displayProfiles). Applying is
|
||||||
|
# instant (hyprctl per entry, via nomarchy-display-profile) and
|
||||||
|
# persisted in-flake (settings.displayProfile) so the next rebuild
|
||||||
|
# bakes it. "Base layout" clears the state and hyprctl-reloads.
|
||||||
|
command -v nomarchy-display-profile >/dev/null 2>&1 \
|
||||||
|
|| { notify-send "Display profiles" "None declared (nomarchy.displayProfiles)."; exit 0; }
|
||||||
|
active=$(nomarchy-display-profile active)
|
||||||
|
choice=$( {
|
||||||
|
nomarchy-display-profile list | while IFS= read -r p; do
|
||||||
|
if [ "$p" = "$active" ]; then printf '● %s\n' "$p"; else printf '○ %s\n' "$p"; fi
|
||||||
|
done
|
||||||
|
printf 'Base layout\n'
|
||||||
|
printf '%s\n' "$BACK"
|
||||||
|
} | rofi -dmenu -p Profiles ) || exit 0
|
||||||
|
case "$choice" in
|
||||||
|
"$BACK") exec "$0" display ;;
|
||||||
|
"Base layout") exec nomarchy-display-profile base ;;
|
||||||
|
*) exec nomarchy-display-profile apply "''${choice#[●○] }" ;;
|
||||||
|
esac ;;
|
||||||
|
|
||||||
colorpicker)
|
colorpicker)
|
||||||
# Pick a pixel's color (hyprpicker's zoom loupe) → clipboard, hex
|
# Pick a pixel's color (hyprpicker's zoom loupe) → clipboard, hex
|
||||||
# in the confirmation toast. Esc/cancel exits silently. Self-gated
|
# in the confirmation toast. Esc/cancel exits silently. Self-gated
|
||||||
@@ -610,7 +635,7 @@ ${themeRows}
|
|||||||
esac ;;
|
esac ;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot]" >&2
|
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot]" >&2
|
||||||
exit 64 ;;
|
exit 64 ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -34,6 +34,16 @@
|
|||||||
# { name = "HDMI-A-1"; position = "auto-right"; }
|
# { name = "HDMI-A-1"; position = "auto-right"; }
|
||||||
# ]; # names from `hyprctl devices`
|
# ]; # names from `hyprctl devices`
|
||||||
#
|
#
|
||||||
|
# nomarchy.displayProfiles = { # named layouts for the SAME outputs —
|
||||||
|
# docked = [ # switch: System › Display › Profiles
|
||||||
|
# { name = "eDP-1"; resolution = "disable"; }
|
||||||
|
# { name = "DP-3"; position = "0x0"; }
|
||||||
|
# { name = "DP-4"; position = "auto-right"; }
|
||||||
|
# ];
|
||||||
|
# undocked = [ { name = "eDP-1"; position = "0x0"; } ];
|
||||||
|
# }; # applied instantly + remembered in the
|
||||||
|
# # flake state (settings.displayProfile)
|
||||||
|
#
|
||||||
# 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