A display profile can now pin workspaces to outputs:
docked = {
monitors = [ … ];
workspaces = { "1" = "DP-3"; "9" = "eDP-1"; };
};
The bare list-of-monitors shape still works (either-type; hyprland.nix
normalizes — coercedTo refuses list-of-submodule sources). Pins bake as
Hyprland `workspace` rules from the active profile and apply instantly
on `nomarchy-display-profile apply`: keyword sets the session rule,
moveworkspacetomonitor moves open workspaces over. Pins from a
previously applied profile linger in-session until reload/rebuild
(hyprctl can only add) — inert if the output is absent, noted in the
applier.
V1: flake check green (checks.display-profiles now asserts
workspaceRule) + scratch-downstream eval: baked settings carry both
rule sets, the generated applier has the keyword+dispatch pairs
(bash -n), match/list correct with mixed shapes. V3 queued in
HARDWARE-QUEUE (rides the dock test). Item 15 complete — deleted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
387 lines
16 KiB
Nix
387 lines
16 KiB
Nix
# User-level `nomarchy.*` options — the full surface downstream users
|
||
# 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.";
|
||
};
|
||
};
|
||
};
|
||
|
||
# One display profile: a monitor layout plus optional workspace→output
|
||
# pinning. A bare list of monitor entries still works (the original
|
||
# shape) — hyprland.nix normalizes it to { monitors = […]; }.
|
||
# (either, not coercedTo: coercedTo refuses list-of-submodule sources.)
|
||
displayProfileType = lib.types.either
|
||
(lib.types.listOf monitorType)
|
||
(lib.types.submodule {
|
||
options = {
|
||
monitors = lib.mkOption {
|
||
type = lib.types.listOf monitorType;
|
||
default = [ ];
|
||
description = "nomarchy.monitors-style entries; each replaces the base entry for the same output whole.";
|
||
};
|
||
workspaces = lib.mkOption {
|
||
type = lib.types.attrsOf lib.types.str;
|
||
default = { };
|
||
example = { "1" = "DP-3"; "9" = "eDP-1"; };
|
||
description = ''
|
||
Workspace → output pinning while this profile is active
|
||
(Hyprland `workspace = <ws>, monitor:<output>` rules).
|
||
Applied instantly on profile switch (existing workspaces are
|
||
moved over) and baked at the next rebuild.
|
||
'';
|
||
};
|
||
};
|
||
});
|
||
in
|
||
{
|
||
options.nomarchy = {
|
||
# ── Required ───────────────────────────────────────────────────
|
||
stateFile = lib.mkOption {
|
||
type = lib.types.path;
|
||
example = lib.literalExpression "./theme-state.json";
|
||
description = ''
|
||
Path to theme-state.json, the single source of truth for all UI
|
||
configuration. Must live inside your flake (so evaluation stays
|
||
pure) and be git-tracked. nomarchy-theme-sync writes to the
|
||
on-disk copy; rebuilds bake it into the generation.
|
||
'';
|
||
};
|
||
|
||
# ── Preferences ────────────────────────────────────────────────
|
||
terminal = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "ghostty";
|
||
description = "Terminal emulator command, used by keybinds and $TERMINAL.";
|
||
};
|
||
|
||
keyboard.layout = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "us";
|
||
example = "de";
|
||
description = ''
|
||
XKB layout for the Hyprland session. The console (and the LUKS
|
||
passphrase prompt) take theirs from the system side — the
|
||
installer writes services.xserver.xkb + console.useXkbConfig
|
||
into system.nix with the same value.
|
||
'';
|
||
};
|
||
|
||
keyboard.variant = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
example = "nodeadkeys";
|
||
description = "XKB variant for the Hyprland session.";
|
||
};
|
||
|
||
keyboard.layouts = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ ];
|
||
example = [ "de" "fr" ];
|
||
description = ''
|
||
Extra candidate layouts offered by the interactive new-keyboard
|
||
picker. When non-empty, a small watcher runs in the session: when a
|
||
keyboard connects that isn't covered by nomarchy.keyboard.devices and
|
||
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
||
the primary nomarchy.keyboard.layout), applies the choice to that
|
||
keyboard only (a per-device kb_layout, so the built-in board is left
|
||
alone), and remembers it in the git-tracked in-flake state
|
||
(settings.keyboard.devices, not ~/.local/state) — re-applied
|
||
automatically on later reconnects and across reboots. Each remembered
|
||
choice graduates into nomarchy.keyboard.devices on the next rebuild
|
||
(a generated device block). The runtime-remember complement to the
|
||
declarative keyboard.devices.
|
||
'';
|
||
};
|
||
|
||
keyboard.devices = lib.mkOption {
|
||
type = lib.types.attrsOf (lib.types.submodule {
|
||
options = {
|
||
layout = lib.mkOption {
|
||
type = lib.types.str;
|
||
description = "XKB layout for this specific keyboard.";
|
||
};
|
||
variant = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = "XKB variant for this keyboard.";
|
||
};
|
||
};
|
||
});
|
||
default = { };
|
||
example = lib.literalExpression ''{ "keychron-keychron-k2" = { layout = "de"; }; }'';
|
||
description = ''
|
||
Per-device keyboard layout. The key is the device name from
|
||
`hyprctl devices` (lower-case, hyphenated). Each entry generates a
|
||
Hyprland `device` block that overrides the session-wide
|
||
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
||
keyboard that's physically a different layout than the laptop's
|
||
built-in one. Hyprland applies it automatically whenever that
|
||
keyboard is connected. The interactive watcher
|
||
(nomarchy.keyboard.layouts) also writes its remembered picks here on
|
||
the next rebuild, so a runtime choice graduates into reproducible config.
|
||
'';
|
||
};
|
||
|
||
package = lib.mkOption {
|
||
type = lib.types.package;
|
||
default = pkgs.nomarchy-theme-sync;
|
||
defaultText = lib.literalExpression "pkgs.nomarchy-theme-sync";
|
||
description = "The nomarchy-theme-sync package (provided by overlays.default).";
|
||
};
|
||
|
||
themesDir = lib.mkOption {
|
||
type = lib.types.path;
|
||
default = ../../themes;
|
||
defaultText = lib.literalExpression "\"\${nomarchy}/themes\"";
|
||
description = ''
|
||
Theme presets/assets directory probed at eval time for per-theme
|
||
app overrides (<slug>/waybar.css, <slug>/waybar.jsonc). Point it
|
||
at your own directory to ship custom layouts downstream.
|
||
'';
|
||
};
|
||
|
||
nightlight = {
|
||
enable = lib.mkEnableOption ''
|
||
a scheduled blue-light filter (hyprsunset): warm at night, no shift
|
||
by day. Opt-in; tune the temperature + sunrise/sunset below'';
|
||
|
||
temperature = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 4000;
|
||
example = 3500;
|
||
description = "Warm colour temperature (K) applied at night — lower is warmer.";
|
||
};
|
||
|
||
sunrise = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "07:00";
|
||
example = "06:30";
|
||
description = "Time (HH:MM) the filter turns OFF — daytime, no colour shift.";
|
||
};
|
||
|
||
sunset = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "20:00";
|
||
example = "21:00";
|
||
description = "Time (HH:MM) the filter turns ON — warm.";
|
||
};
|
||
};
|
||
|
||
updates = {
|
||
enable = lib.mkEnableOption ''
|
||
passive update awareness: a background check (systemd user timer) that
|
||
compares the flake's locked inputs (nixpkgs, the Nomarchy input, …)
|
||
against upstream and — when Flatpak is enabled — counts Flatpak
|
||
updates, surfacing a Waybar indicator + a notification when something
|
||
is available. It never changes anything; you still run sys-update /
|
||
home-update / flatpak update yourself'';
|
||
|
||
interval = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "daily";
|
||
example = "6h";
|
||
description = "How often to check, as a systemd OnCalendar expression.";
|
||
};
|
||
|
||
flatpak = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = true;
|
||
description = ''
|
||
Also count available Flatpak updates when the `flatpak` CLI is
|
||
present (i.e. nomarchy.services.flatpak is on). No effect otherwise.
|
||
'';
|
||
};
|
||
};
|
||
|
||
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.
|
||
'';
|
||
};
|
||
|
||
launchOrFocus = lib.mkOption {
|
||
type = lib.types.listOf (lib.types.submodule {
|
||
options = {
|
||
key = lib.mkOption {
|
||
type = lib.types.str;
|
||
example = "B";
|
||
description = "The key (with `mods`) that focuses-or-launches the app.";
|
||
};
|
||
mods = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "$mod";
|
||
description = "Modifier string, Hyprland syntax (\"$mod\", \"$mod SHIFT\").";
|
||
};
|
||
class = lib.mkOption {
|
||
type = lib.types.str;
|
||
example = "firefox";
|
||
description = "Window class to focus (case-insensitive; see `hyprctl clients`).";
|
||
};
|
||
command = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = "Command to launch when no window matches (defaults to the class).";
|
||
};
|
||
desc = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = "Cheatsheet label (defaults to \"Focus or launch <command>\").";
|
||
};
|
||
};
|
||
});
|
||
default = [ ];
|
||
example = lib.literalExpression ''
|
||
[
|
||
{ key = "B"; class = "firefox"; }
|
||
{ key = "O"; class = "obsidian"; }
|
||
]
|
||
'';
|
||
description = ''
|
||
Launch-or-focus binds: the key focuses the app's existing window
|
||
(case-insensitive class match) or launches it if none is open. Each
|
||
entry generates a Hyprland bind AND a SUPER+? cheatsheet row. The
|
||
launcher self-gates: a bind whose command isn't installed notifies
|
||
instead of failing silently.
|
||
'';
|
||
};
|
||
|
||
displayProfiles = lib.mkOption {
|
||
type = lib.types.attrsOf displayProfileType;
|
||
default = { };
|
||
example = lib.literalExpression ''
|
||
{
|
||
docked = {
|
||
monitors = [
|
||
{ name = "eDP-1"; resolution = "disable"; }
|
||
{ name = "DP-3"; position = "0x0"; }
|
||
{ name = "DP-4"; position = "auto-right"; }
|
||
];
|
||
workspaces = { "1" = "DP-3"; "9" = "DP-4"; };
|
||
};
|
||
undocked = [ { name = "eDP-1"; position = "0x0"; } ];
|
||
}
|
||
'';
|
||
description = ''
|
||
Named display layouts for the same outputs (docked, undocked, …):
|
||
a list of nomarchy.monitors-style entries, or an attrset with
|
||
`monitors` plus optional `workspaces` (workspace → output pinning
|
||
while the profile is active). 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 ──────────────────────────────────────────
|
||
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
|
||
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
|
||
rofi.enable = lib.mkEnableOption "Nomarchy's themed rofi launcher + the nomarchy-menu dispatcher" // { default = true; };
|
||
swaync.enable = lib.mkEnableOption "swaync notifications, themed from the state file" // { default = true; };
|
||
batteryNotify.enable = lib.mkEnableOption "low-battery notifications at the bar's thresholds (25% low, 10% critical — that one stays up until dismissed); self-gating, a silent no-op on machines without a battery" // { default = true; };
|
||
idle.enable = lib.mkEnableOption "hyprlock + hypridle (idle lock, display off, suspend)" // { default = true; };
|
||
yazi.enable = lib.mkEnableOption "the yazi TUI file manager, themed with a curated plugin set" // { default = true; };
|
||
osd.enable = lib.mkEnableOption "swayosd on-screen display for volume/brightness/mute" // { default = true; };
|
||
shell.enable = lib.mkEnableOption "the zsh shell experience (starship prompt, bat/eza/zoxide)" // { default = true; };
|
||
keys.enable = lib.mkEnableOption "the SSH + GPG agent (gpg-agent fronting SSH, pinentry-qt)" // { default = true; };
|
||
fastfetch.enable = lib.mkEnableOption "fastfetch system info fronted by the themed Nomarchy logo" // { default = true; };
|
||
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; };
|
||
viewers.enable = lib.mkEnableOption "the document/image viewers (zathura, Stylix-themed, + imv)" // { default = true; };
|
||
mime.enable = lib.mkEnableOption "default file associations (xdg mimeapps.list: PDF/image/video/text/browser/directory); entries for absent apps are skipped, so it degrades with the package suite" // { default = true; };
|
||
|
||
# ── Computed (read-only) ───────────────────────────────────────
|
||
theme = lib.mkOption {
|
||
type = lib.types.attrs;
|
||
readOnly = true;
|
||
description = "The parsed theme state (stateFile merged over defaults).";
|
||
};
|
||
|
||
settings = lib.mkOption {
|
||
type = lib.types.attrs;
|
||
readOnly = true;
|
||
description = ''
|
||
Parsed feature settings — the `settings` section of the state file,
|
||
what the menu/Waybar toggles write (e.g. settings.nightlight.enable).
|
||
Feature options mkDefault-read from here, so a menu toggle lands in the
|
||
in-flake state (git-tracked, reproducible) rather than ~/.local/state.
|
||
'';
|
||
};
|
||
|
||
lib = lib.mkOption {
|
||
type = lib.types.attrs;
|
||
readOnly = true;
|
||
description = "Color-format helpers shared by the theme consumers.";
|
||
};
|
||
};
|
||
}
|