Files
Nomarchy/modules/home/options.nix
Bernardo Magri e2de9062d8
All checks were successful
Check / eval (push) Successful in 2m59s
feat(idle): unlock by fingerprint, and say so — hyprlock never used the PAM flag
Bernardo tested the lock screen after turning `fingerprint.pam` on: sudo took
a finger, hyprlock did nothing. The PAM stack was not the problem — grosshack
sits in /etc/pam.d/hyprlock at order 11400 exactly like sudo's. hyprlock just
never asks it: its PAM conversation runs only on submit, so a module that
wants to prompt *while* polling the reader never gets a turn. hyprlock has its
own fprintd-over-D-Bus backend instead, behind `auth:fingerprint:enabled`,
which nothing in the distro ever set. So `fingerprint.pam = true` promised
"login + sudo" and silently skipped the surface you meet most often.

The second half is the one worth having: enabling the backend alone yields a
reader that works while the field still reads "password…". $FPRINTPROMPT is
the ONLY slot hyprlock renders the ready/scanning/failed messages into, and
ours hardcoded the placeholder — proven on hardware, where a test config with
the backend on unlocked by finger and said nothing whatsoever. A feature that
works and cannot be discovered fails VISION's "discoverable without reading
the README" either way, so the placeholder now carries $FPRINTPROMPT and the
messages mirror sudo's "Enter Password or Place finger".

New `nomarchy.idle.fingerprint` (default false), mirroring
`nomarchy.hardware.fingerprint.pam` the way `nomarchy.keyboard.layout` mirrors
`services.xserver.xkb.layout`: hyprlock is configured in standalone Home
Manager, which has no `osConfig` to derive the NixOS side from. Opt-in rather
than implied, because with no reader hyprlock advertises a scan that cannot
happen. Documented in the template (SoT for opt-in comments) and README —
option-docs caught the missing row, which is the check doing its job.

Verified V2: nix flake check --no-build; checks.option-docs passes (failed
first with "undocumented option", as it should). V3 pending: Bernardo applies
it and confirms the field reads "password… or scan finger" and both factors
work — the mechanism itself is already proven on his reader via a test config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:58:53 +01:00

424 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 "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.mkEnableOption ''
unlocking the lock screen with a fingerprint as well as the password,
and saying so on the input field.
Mirrors `nomarchy.hardware.fingerprint.pam` in system.nix keep the two
matched, the same way `nomarchy.keyboard.layout` matches
`services.xserver.xkb.layout`. It cannot be derived: hyprlock is
configured here, in standalone Home Manager, which has no `osConfig` to
read the NixOS side from.
Deliberately separate from the PAM flag rather than implied by it:
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. With no reader present it advertises a scan that cannot happen, so
it stays opt-in
'';
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.";
};
};
}