Phase 2 of the in-flake-state philosophy, mirroring night-light: the
nomarchy-keyboard-watch daemon now remembers per-device layouts in the
git-tracked state file (settings.keyboard.devices) instead of
~/.local/state/nomarchy/keyboard-layouts.
- Reads the LIVE working-tree map (theme-sync get → jq) so a pick this
session is honoured at once; writes instantly with --no-switch (no
rebuild). Writes the whole map back at the dot-free parent path
(read-modify-write) so a device name with a dot can't corrupt the
tool's dotted set-path (Hyprland only sanitises spaces→-).
- Graduation: nomarchy.keyboard.devices now mkDefault-merges entries
from settings.keyboard.devices, so each remembered device becomes a
generated Hyprland device{} block on the next rebuild (reproducible,
hotplug-applied). A hand-written keyboard.devices.<name> still wins;
once baked, the watcher's is_declared check makes it step back.
- theme.nix seeds settings.keyboard.devices = {} (sparse state still
evaluates); options/template descriptions updated; ROADMAP marked done.
Eval-verified (graduation + mkDefault precedence) and the set/get
writer round-trips. Still needs the on-hardware hotplug re-verify (the
picker path isn't testable in CI).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
281 lines
12 KiB
Nix
281 lines
12 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.";
|
||
};
|
||
};
|
||
};
|
||
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.
|
||
'';
|
||
};
|
||
|
||
# ── 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; };
|
||
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; };
|
||
|
||
# ── 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.";
|
||
};
|
||
};
|
||
}
|