All checks were successful
Check / eval (push) Successful in 3m7s
Bernardo, post-reboot: "Use for login" was the wrong question. Whether the
finger works is one decision, not two, and whether login prompts at all is
a different decision that was never in the menu.
System › Fingerprint is now a single Fingerprint (on/off) switch, leading
the menu with enroll/list/verify/delete as the plumbing behind it. It
writes the one settings.fingerprint.pam key, and modules/home/idle.nix now
defaults idle.fingerprint from that same key — so the lock screen and
login/sudo move together instead of drifting apart the way they did until
e2de906. nomarchy-fingerprint does the two rebuilds this needs (sudo
system for PAM, home switch for hyprlock) and refuses to turn on with no
finger enrolled.
System › Auto-login is new (nomarchy-autologin), and it is what decides
whether anything is asked at boot: auto-login on means no prompt whatever
the fingerprint switch says; off means the greeter asks, for a password or
a finger. Installer-seeded ON for LUKS machines — the passphrase already
gates the disk — and off without it, where the greeter is the only thing
between power-on and the desktop.
Both had to become state-owned to be toggleable at all, which surfaced two
real bugs:
* nomarchy.system.greeter.autoLogin defaulted from
`config.nomarchy.settings…` — an attribute that exists ONLY on the Home
Manager side. On NixOS it is absent and `or null` swallowed the error,
so the default silently evaluated to null on every machine ever built.
That is why the installer baked a Nix line: the state path never
worked. Now read via theme-state-read.nix (the hardware.nix/timezone.nix
pattern) and mkDefault'd, so the menu owns it and a hand-set line still
pins it. Two more options read the same phantom bridge — BACKLOG #116.
* `theme-sync get` printed Python's "None" for a JSON null, so every
`case … null)` a caller writes would miss. Now prints "null", as the
comment above it already promised for booleans.
The installer seeds the state instead of emitting the system.nix line,
because that line outranks the state and would strand the toggle.
V1 (V3 pending: HARDWARE-QUEUE). nix flake check --no-build, installer-
safety and option-docs all pass. Proved by eval/build, not assumed: a state
carrying autoLogin yields greetd initial_session {"user":"bernardo"}, the
template state (no autoLogin) yields none, and a hand-set null beats a state
that says otherwise; a state with only fingerprint.pam=true — nothing set by
hand — renders the hyprlock auth.fingerprint block; both new tools pass
bash -n and land in systemPackages (nomarchy-fingerprint only with a
reader); the patcher writes settings.greeter.autoLogin and no system.nix
line; and the get round trip prints null, so the menu reads "Auto-login
(off)" where it would have read "(on)".
The reader itself, the two rebuilds, and the reboot are hardware — queued.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
430 lines
19 KiB
Nix
430 lines
19 KiB
Nix
# 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 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 = 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.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; };
|
||
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.";
|
||
};
|
||
};
|
||
}
|