Files
Nomarchy/modules/home/options.nix
Bernardo Magri 7ebfab4bd4
All checks were successful
Check / eval (push) Successful in 3m32s
fix(docking): make clamshell hotplug safe and discoverable
Real dock QA exposed pointer-bound menus, an absent default rescue watcher, the unsafe zero-output panel-off path, missing keyboard/audio hotplug coverage, and unpainted new outputs.

Route Rofi to the focused output; replace panel disable with safe workspace migration; always run and reconnect the display watcher and repaint hotplugged outputs; expose a default per-device keyboard picker; handle audio card availability transitions.

Verified: V1 — nix flake check --no-build; checks.docking-ux real Home Manager artifact build; option-docs/template-sot; Nix parse, py_compile, and git diff --check. V2 unavailable: no readable KVM. V3 pending in agent/HARDWARE-QUEUE.md.
2026-07-13 11:53:01 +01:00

407 lines
18 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# User-level `nomarchy.*` options — the full surface downstream users
# configure in their home.nix. Kept small on purpose.
{ config, 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 07 (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 = config.nomarchy.settings.terminal or "ghostty";
description = "Terminal emulator command, used by keybinds and $TERMINAL.";
};
keyboard.layout = lib.mkOption {
type = lib.types.str;
default = config.nomarchy.settings.keyboard.layout or "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 shown first by the interactive new-keyboard
picker (all installed XKB layouts remain searchable). A small watcher
always runs in the Hyprland session: when a keyboard connects that
isn't covered by nomarchy.keyboard.devices and hasn't been chosen
before, it pops a rofi picker, 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; wlsunset in geo mode):
warm at night, no shift by day. Opt-in; tune the temperature +
sunrise/sunset (or latitude/longitude) 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. Ignored in geo mode.";
};
sunset = lib.mkOption {
type = lib.types.str;
default = "20:00";
example = "21:00";
description = "Time (HH:MM) the filter turns ON warm. Ignored in geo mode.";
};
latitude = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "52.52";
description = ''
Geo mode: set BOTH latitude and longitude and sunrise/sunset are
computed from your location every day (wlsunset replaces
hyprsunset; the fixed .sunrise/.sunset times are ignored).'';
};
longitude = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "13.40";
description = "Geo mode longitude set together with latitude.";
};
};
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 nomarchy-pull /
nomarchy-rebuild / nomarchy-home / flatpak update yourself'' // { default = config.nomarchy.settings.updates.enable or false; };
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; };
dockAudio.enable = lib.mkEnableOption "automatic default-output switch to dock/monitor audio (HDMI/DisplayPort/USB sinks) on hotplug, with a toast; EasyEffects follows along, and unplugging falls back to the built-in output" // { default = true; };
firstBootWelcome.enable = lib.mkEnableOption "one dismissible \"you're set\" toast on the first session (menu/themes/keys + network pointer); marker is settings.firstBootShown in the flake checkout" // { 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.";
};
};
}