# 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` | `x@` | `disable`."; }; position = lib.mkOption { type = lib.types.str; default = "auto"; description = "`auto` | `auto-right`/`auto-left`/… | `x`."; }; 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 = , monitor:` 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 "kitty"; example = "kitty"; description = '' Terminal emulator command for keybinds and `$TERMINAL`. Nomarchy ships and themes **Kitty only** (sole supported terminal — works on older GPUs that Ghostty's OpenGL 4.3 floor rejected). Override only if you install another emulator yourself. ''; }; kitty.enable = lib.mkEnableOption "Nomarchy's Kitty configuration (palette/font/opacity from theme-state)" // { default = true; }; 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 (/waybar.css, /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 \")."; }; }; }); 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 `: 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 "settled PipeWire/WirePlumber reprobe and automatic default-output switch to an available dock/monitor sink (HDMI/DisplayPort/USB) on fresh display hotplug, with a toast and journal result; a later manual choice sticks until the next plug" // { 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; }; idle.fingerprint = lib.mkOption { type = lib.types.bool; default = config.nomarchy.settings.fingerprint.pam or false; defaultText = lib.literalExpression "(settings.fingerprint.pam from theme-state.json) or false"; description = '' Unlock the lock screen with a fingerprint as well as the password, and say so on the input field. Reads the SAME `settings.fingerprint.pam` state key that `nomarchy.hardware.fingerprint.pam` defaults from, so the one System › Fingerprint toggle moves the lock screen and login/sudo together — "fingerprint on" is one decision, not two that can drift. (It has to arrive by state, not by reading the NixOS side: hyprlock is configured here, in standalone Home Manager, which has no `osConfig`.) The two remain separate *mechanisms*, which is why this option still exists to be set by hand: hyprlock does NOT unlock by fingerprint through PAM. Its PAM stack only runs on submit, so a parallel module never gets to poll — hyprlock has its own fprintd-over-D-Bus backend instead, and this is the switch for it. ''; }; 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; }; 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."; }; }; }