feat(displays): declarative nomarchy.monitors + nwg-displays arranger
Add a per-output monitor schema (name / resolution / position / scale / transform / mirror / bitdepth / vrr / disable) that generates Hyprland `monitor` rules, keeping the ,preferred,auto,1 wildcard as the fallback. Hyprland applies the rules on hotplug, so a declared external/dock output arranges itself on connect (no kanshi -- it fights Hyprland's own output management). Default [] -> wildcard only, so no change for existing setups; set wayland.windowManager.hyprland.settings.monitor directly to override. nwg-displays ships behind nomarchy.displays.enable (default true) as an interactive arranger -- a helper to discover values; the declarative config stays the source of truth (its output file isn't sourced). Remaining (roadmap): docked/undocked profile switching, workspace-to-monitor binding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,8 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
| `nomarchy.stylix.enable` | `true` | GTK/Qt/cursor theming |
|
||||
| `nomarchy.fastfetch.enable` | `true` | fastfetch fronted by the themed Nomarchy logo |
|
||||
| `nomarchy.keys.enable` | `true` | gpg-agent fronting SSH + pinentry-qt |
|
||||
| `nomarchy.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
|
||||
| `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback |
|
||||
| `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed |
|
||||
| `nomarchy.system.plymouth.enable` | `true` | Branded boot splash, background from the theme JSON (recolors on system rebuilds) |
|
||||
| `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
|
||||
|
||||
@@ -233,12 +233,18 @@ how to override it. Items marked ✓ are shipped.
|
||||
surface or split (`nomarchy.services.*` system-side vs home-side packages);
|
||||
and how it relates to `nomarchy.apps.*` (opt-out suite). Unfree entries are
|
||||
already covered (`allowUnfree = true`).
|
||||
- **Display / monitor management:** today Hyprland just uses
|
||||
`monitor = ,preferred,auto,1` (mkDefault). Add a real external-monitor/dock
|
||||
story: declarative per-output config, hotplug profiles (kanshi-style, or
|
||||
Hyprland's own monitor rules), and optionally a GUI arranger (nwg-displays
|
||||
writes Hyprland monitor config). Decide declarative-only vs GUI, and the
|
||||
option shape (`nomarchy.monitors` vs plain Hyprland `monitor` lists).
|
||||
- ✓ **Display / monitor management:** declarative `nomarchy.monitors` (a
|
||||
per-output schema — name/resolution/position/scale/transform/mirror/
|
||||
bitdepth/vrr/disable) generates Hyprland `monitor` rules, keeping the
|
||||
`,preferred,auto,1` wildcard as the fallback; Hyprland applies them on
|
||||
hotplug, so a declared external/dock output arranges itself on connect
|
||||
(no kanshi — it fights Hyprland's own output management). `nwg-displays`
|
||||
ships behind `nomarchy.displays.enable` as an interactive arranger (a
|
||||
helper to find values; the declarative config stays the source of truth —
|
||||
its output file isn't sourced). Remaining: true docked/undocked **profile
|
||||
switching** of the *same* outputs (Hyprland's per-output rules cover the
|
||||
common "external connects → arrange" case, not multi-layout toggles), and
|
||||
optionally workspace-to-monitor binding.
|
||||
- **Night light / blue-light filter:** schedulable colour-temperature shift
|
||||
via `hyprsunset` (Hyprland-native; wlsunset/gammastep are alternatives) —
|
||||
sunset/sunrise or a fixed schedule, with a menu + Waybar toggle.
|
||||
|
||||
@@ -41,6 +41,19 @@ let
|
||||
# (rofi.nix renders the same list). Edit them in ./keybinds.nix.
|
||||
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
|
||||
);
|
||||
in
|
||||
{
|
||||
wayland.windowManager.hyprland = lib.mkIf config.nomarchy.hyprland.enable {
|
||||
@@ -59,9 +72,12 @@ in
|
||||
"$mod" = "SUPER";
|
||||
"$terminal" = config.nomarchy.terminal;
|
||||
|
||||
# mkDefault so a downstream `monitor = [...]` replaces it with a
|
||||
# plain assignment (the live ISO uses mkForce for the same reason).
|
||||
monitor = lib.mkDefault [ ",preferred,auto,1" ];
|
||||
# `,preferred,auto,1` is the wildcard fallback for any output not in
|
||||
# nomarchy.monitors; the generated rules are appended and win by name,
|
||||
# and Hyprland applies them on hotplug. mkDefault so a downstream
|
||||
# `monitor = [...]` replaces the lot (the live ISO uses mkForce too).
|
||||
monitor = lib.mkDefault
|
||||
([ ",preferred,auto,1" ] ++ map monitorRule config.nomarchy.monitors);
|
||||
|
||||
# exec-once is a list at normal priority, so downstream additions
|
||||
# CONCATENATE (your autostart runs alongside ours) rather than
|
||||
@@ -183,4 +199,12 @@ in
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# nwg-displays: interactive monitor arranger (applies live via hyprctl and
|
||||
# writes a config file). A helper to find values for nomarchy.monitors —
|
||||
# the declarative config stays the source of truth (we don't source its
|
||||
# output). Gated on its toggle; pointless without the Hyprland session.
|
||||
home.packages = lib.optionals
|
||||
(config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
||||
[ pkgs.nwg-displays ];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,60 @@
|
||||
# configure in their home.nix. Kept small on purpose.
|
||||
{ lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
# One output's layout — turned into a Hyprland `monitor` rule in
|
||||
# hyprland.nix. Friendlier than raw positional CSV; unset optional fields
|
||||
# are omitted from the rule.
|
||||
monitorType = lib.types.submodule {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "HDMI-A-1";
|
||||
description = "Output name (see `hyprctl monitors`), or a `desc:<…>` match.";
|
||||
};
|
||||
resolution = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "preferred";
|
||||
description = "`preferred` | `highres` | `highrr` | `<w>x<h>@<hz>` | `disable`.";
|
||||
};
|
||||
position = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "auto";
|
||||
description = "`auto` | `auto-right`/`auto-left`/… | `<x>x<y>`.";
|
||||
};
|
||||
scale = lib.mkOption {
|
||||
type = lib.types.oneOf [ lib.types.str lib.types.int lib.types.float ];
|
||||
default = 1;
|
||||
description = "Scale factor (1, 1.5, …) or `auto`.";
|
||||
};
|
||||
transform = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
description = "Rotation 0–7 (90°/180°/270° = 1/2/3).";
|
||||
};
|
||||
mirror = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Mirror another output by name.";
|
||||
};
|
||||
bitdepth = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
description = "Colour bit depth (8 or 10).";
|
||||
};
|
||||
vrr = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
description = "Variable refresh: 0 off, 1 on, 2 fullscreen-only.";
|
||||
};
|
||||
extra = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = "Extra comma-separated Hyprland monitor args, appended verbatim.";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options.nomarchy = {
|
||||
# ── Required ───────────────────────────────────────────────────
|
||||
@@ -60,6 +114,27 @@
|
||||
'';
|
||||
};
|
||||
|
||||
monitors = lib.mkOption {
|
||||
type = lib.types.listOf monitorType;
|
||||
default = [ ];
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
{ name = "eDP-1"; position = "0x0"; }
|
||||
{ name = "HDMI-A-1"; position = "auto-right"; scale = 1; }
|
||||
]
|
||||
'';
|
||||
description = ''
|
||||
Declarative per-output monitor layout. Each entry becomes a Hyprland
|
||||
`monitor` rule; Hyprland applies them on hotplug, so a declared
|
||||
external/dock output arranges itself when connected. The built-in
|
||||
`,preferred,auto,1` wildcard stays as the fallback for any output you
|
||||
don't list. For raw control set
|
||||
`wayland.windowManager.hyprland.settings.monitor` directly instead
|
||||
(it replaces this). Run `nwg-displays` (nomarchy.displays.enable) to
|
||||
find the right values interactively.
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Component toggles ──────────────────────────────────────────
|
||||
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
|
||||
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
|
||||
@@ -74,6 +149,7 @@
|
||||
ghostty.enable = lib.mkEnableOption "Nomarchy's Ghostty configuration" // { default = true; };
|
||||
btop.enable = lib.mkEnableOption "btop with the per-theme nomarchy theme" // { default = true; };
|
||||
stylix.enable = lib.mkEnableOption "Stylix theming for the long tail of apps (GTK, Qt, cursors)" // { default = true; };
|
||||
displays.enable = lib.mkEnableOption "the nwg-displays interactive monitor arranger (a helper to find nomarchy.monitors values; the declarative config stays the source of truth)" // { default = true; };
|
||||
|
||||
# ── Computed (read-only) ───────────────────────────────────────
|
||||
theme = lib.mkOption {
|
||||
|
||||
Reference in New Issue
Block a user