refactor(modules): organize module tree

This commit was merged in pull request #72.
This commit is contained in:
2026-08-28 08:40:15 -04:00
parent f767b613a0
commit abac67afb4
41 changed files with 133 additions and 140 deletions

View File

@@ -18,11 +18,11 @@ One flake builds every machine I own.
### Desktop ### Desktop
- When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/waybar.nix`. - When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/session/waybar.nix`.
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph. The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
Match the mapping on the window class. Match the mapping on the window class.
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session. Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
- Set Hyprland session environment variables in `modules/desktop/hyprland/hyprland.nix` with `env = KEY,VALUE`. - Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
`home.sessionVariables` do **not** reach the Hyprland session because UWSM does not source `hm-session-vars.sh`. `home.sessionVariables` do **not** reach the Hyprland session because UWSM does not source `hm-session-vars.sh`.
Source cursor values from `config.stylix.cursor`. Source cursor values from `config.stylix.cursor`.
Set `XCURSOR_*` and `HYPRCURSOR_*` to the same theme for Bibata. Set `XCURSOR_*` and `HYPRCURSOR_*` to the same theme for Bibata.

View File

@@ -171,7 +171,7 @@ Then populate that file, which needs no existing identity because encrypting onl
$ nix run nixpkgs#sops -- secrets/newhost.yaml $ nix run nixpkgs#sops -- secrets/newhost.yaml
``` ```
A host with `modules.ssh.enable` expects one entry per key type, named `ssh-host-<type>-key`, each holding a private key generated with `ssh-keygen -t <type> -N "" -f /tmp/<type>`. A host with `modules.network.ssh.enable` expects one entry per key type, named `ssh-host-<type>-key`, each holding a private key generated with `ssh-keygen -t <type> -N "" -f /tmp/<type>`.
The build fails at evaluation if a declared secret is absent from the file, so a host that enables the daemon without these will not install. The build fails at evaluation if a declared secret is absent from the file, so a host that enables the daemon without these will not install.
Commit the matching public halves beside the host's configuration in plaintext, since publishing them is their purpose. Commit the matching public halves beside the host's configuration in plaintext, since publishing them is their purpose.

View File

@@ -36,9 +36,9 @@
# So setup can be driven over the network. # So setup can be driven over the network.
# The matching host public keys sit beside this file in plaintext, since # The matching host public keys sit beside this file in plaintext, since
# publishing them is their purpose. # publishing them is their purpose.
modules.ssh.enable = true; modules.network.ssh.enable = true;
modules.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml; modules.network.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
modules.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml; modules.network.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
# The walking-skeleton guest, enabled like any module: proves the guest path # The walking-skeleton guest, enabled like any module: proves the guest path
# end to end through this host's `nix flake check`. # end to end through this host's `nix flake check`.
@@ -60,13 +60,13 @@
tasksMax = 512; tasksMax = 512;
}; };
modules.agents.herdr.enable = true; modules.shell.herdr.enable = true;
modules.agents.tools.gitea-axi.enable = true; modules.agents.tools.gitea-axi.enable = true;
modules.agents.pi.enable = true; modules.agents.pi.enable = true;
modules.desktop.enable = true; modules.desktop.enable = true;
modules.desktop.obsidian.enable = true; modules.desktop.apps.obsidian.enable = true;
modules.desktop.steam.enable = true; modules.desktop.apps.steam.enable = true;
time.timeZone = "America/New_York"; time.timeZone = "America/New_York";
i18n.defaultLocale = "en_GB.UTF-8"; i18n.defaultLocale = "en_GB.UTF-8";

View File

@@ -62,7 +62,7 @@
}; };
networking.useDHCP = false; networking.useDHCP = false;
modules.network.vlans = [ 20 ]; modules.network.vlans.ids = [ 20 ];
boot.zfs.forceImportRoot = false; boot.zfs.forceImportRoot = false;
@@ -74,11 +74,10 @@
registry.address.pikachu.self = "10.23.10.102"; registry.address.pikachu.self = "10.23.10.102";
modules.ssh.enable = true; modules.network.ssh.enable = true;
modules.ssh.hostKeys.sopsFile = ../../secrets/pikachu.yaml; modules.network.ssh.hostKeys.sopsFile = ../../secrets/pikachu.yaml;
modules.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml; modules.network.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml;
modules.git.enable = true;
guests.actualbudget = { guests.actualbudget = {
enable = true; enable = true;
vlan = 20; vlan = 20;

View File

@@ -108,7 +108,7 @@ let
The tagged VLAN this guest lives on. The guest attaches to its host's The tagged VLAN this guest lives on. The guest attaches to its host's
`br-vlan<id>` bridge for that VLAN. Left null, the guest keeps a `br-vlan<id>` bridge for that VLAN. Left null, the guest keeps a
private network with no bridge attachment. The id must be one of the private network with no bridge attachment. The id must be one of the
host's `modules.network.vlans`. host's `modules.network.vlans.ids`.
''; '';
}; };
mac = lib.mkOption { mac = lib.mkOption {
@@ -301,9 +301,9 @@ let
''; '';
} }
{ {
assertion = !networked || lib.elem cfg.vlan config.modules.network.vlans; assertion = !networked || lib.elem cfg.vlan config.modules.network.vlans.ids;
message = '' message = ''
guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans (${lib.concatMapStringsSep ", " toString config.modules.network.vlans}). Declare the VLAN on the host or correct the guest's placement. guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans.ids (${lib.concatMapStringsSep ", " toString config.modules.network.vlans.ids}). Declare the VLAN on the host or correct the guest's placement.
''; '';
} }
]; ];

View File

@@ -116,11 +116,11 @@ in
cleanup = "sudo nix-collect-garbage -d"; cleanup = "sudo nix-collect-garbage -d";
}; };
modules.fish.enable = lib.mkDefault true; modules.shell.fish.enable = lib.mkDefault true;
modules.fish.defaultShell = lib.mkDefault true; modules.shell.fish.defaultShell = lib.mkDefault true;
modules.tmux.enable = lib.mkDefault true; modules.shell.tmux.enable = lib.mkDefault true;
modules.nvim.enable = lib.mkDefault true; modules.shell.nvim.enable = lib.mkDefault true;
modules.git.enable = lib.mkDefault true; modules.shell.git.enable = lib.mkDefault true;
modules.direnv.enable = lib.mkDefault true; modules.shell.direnv.enable = lib.mkDefault true;
}; };
} }

View File

@@ -6,11 +6,11 @@
}: }:
# Alacritty as the desktop terminal. # Alacritty as the desktop terminal.
let let
cfg = config.modules.desktop.terminal; cfg = config.modules.desktop.apps.alacritty;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.desktop.terminal.enable = lib.mkEnableOption "Alacritty as the desktop terminal"; options.modules.desktop.apps.alacritty.enable = lib.mkEnableOption "Alacritty as the desktop terminal";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user}.home.packages = [ pkgs.alacritty ]; home-manager.users.${user}.home.packages = [ pkgs.alacritty ];

View File

@@ -7,12 +7,12 @@
}: }:
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy. # Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
let let
cfg = config.modules.desktop.firefox; cfg = config.modules.desktop.apps.firefox;
user = config.user.name; user = config.user.name;
firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system};
in in
{ {
options.modules.desktop.firefox.enable = lib.mkEnableOption "Firefox as the desktop browser"; options.modules.desktop.apps.firefox.enable = lib.mkEnableOption "Firefox as the desktop browser";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user} = hm: { home-manager.users.${user} = hm: {

View File

@@ -6,11 +6,11 @@
}: }:
# Obsidian as the desktop note-taking vault. # Obsidian as the desktop note-taking vault.
let let
cfg = config.modules.desktop.obsidian; cfg = config.modules.desktop.apps.obsidian;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.desktop.obsidian.enable = lib.mkEnableOption "Obsidian as the desktop note-taking vault"; options.modules.desktop.apps.obsidian.enable = lib.mkEnableOption "Obsidian as the desktop note-taking vault";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user}.home.packages = [ pkgs.obsidian ]; home-manager.users.${user}.home.packages = [ pkgs.obsidian ];

View File

@@ -6,10 +6,10 @@
}: }:
# Steam game launcher and runtime integration. # Steam game launcher and runtime integration.
let let
cfg = config.modules.desktop.steam; cfg = config.modules.desktop.apps.steam;
in in
{ {
options.modules.desktop.steam.enable = lib.mkEnableOption "Steam game launcher"; options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
programs.steam = { programs.steam = {

View File

@@ -0,0 +1,30 @@
{ config, lib, ... }:
# The desktop aggregator: one flag brings up the whole graphical session.
let
cfg = config.modules.desktop;
in
{
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
# Each piece is turned on at default priority, so a host can still override
# any one of them while the single flag above enables the whole desktop.
config = lib.mkIf cfg.enable {
modules.desktop.apps.alacritty.enable = lib.mkDefault true;
modules.desktop.apps.firefox.enable = lib.mkDefault true;
modules.desktop.session.hyprland.enable = lib.mkDefault true;
modules.desktop.session.hyprland.hypridle.enable = lib.mkDefault true;
modules.desktop.session.hyprland.hyprlock.enable = lib.mkDefault true;
modules.desktop.session.login.enable = lib.mkDefault true;
modules.desktop.session.mako.enable = lib.mkDefault true;
modules.desktop.session.portals.enable = lib.mkDefault true;
modules.desktop.session.rofi.enable = lib.mkDefault true;
modules.desktop.session.waybar.enable = lib.mkDefault true;
modules.desktop.utilities.audio.enable = lib.mkDefault true;
modules.desktop.utilities.clipboard.enable = lib.mkDefault true;
modules.desktop.utilities.osd.enable = lib.mkDefault true;
modules.desktop.utilities.recording.enable = lib.mkDefault true;
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
modules.desktop.utilities.theming.enable = lib.mkDefault true;
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
};
}

View File

@@ -1,30 +0,0 @@
{ config, lib, ... }:
# The desktop aggregator: one flag brings up the whole graphical session.
let
cfg = config.modules.desktop;
in
{
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
# Each piece is turned on at default priority, so a host can still override
# any one of them while the single flag above enables the whole desktop.
config = lib.mkIf cfg.enable {
modules.desktop.clipboard.enable = lib.mkDefault true;
modules.desktop.firefox.enable = lib.mkDefault true;
modules.desktop.hyprland.enable = lib.mkDefault true;
modules.desktop.hyprland.hyprlock.enable = lib.mkDefault true;
modules.desktop.hyprland.hypridle.enable = lib.mkDefault true;
modules.desktop.login.enable = lib.mkDefault true;
modules.desktop.mako.enable = lib.mkDefault true;
modules.desktop.portals.enable = lib.mkDefault true;
modules.desktop.recording.enable = lib.mkDefault true;
modules.desktop.rofi.enable = lib.mkDefault true;
modules.desktop.screenshot.enable = lib.mkDefault true;
modules.desktop.terminal.enable = lib.mkDefault true;
modules.desktop.userdirs.enable = lib.mkDefault true;
modules.desktop.theming.enable = lib.mkDefault true;
modules.desktop.waybar.enable = lib.mkDefault true;
modules.desktop.audio.enable = lib.mkDefault true;
modules.desktop.osd.enable = lib.mkDefault true;
};
}

View File

@@ -6,7 +6,7 @@
}: }:
# The Hyprland compositor. # The Hyprland compositor.
let let
cfg = config.modules.desktop.hyprland; cfg = config.modules.desktop.session.hyprland;
user = config.user.name; user = config.user.name;
cursor = config.stylix.cursor; cursor = config.stylix.cursor;
@@ -27,7 +27,7 @@ let
''; '';
in in
{ {
options.modules.desktop.hyprland = { options.modules.desktop.session.hyprland = {
enable = lib.mkEnableOption "the Hyprland compositor"; enable = lib.mkEnableOption "the Hyprland compositor";
blur = lib.mkEnableOption '' blur = lib.mkEnableOption ''

View File

@@ -6,7 +6,7 @@
}: }:
# hypridle: idle-triggered locking and display power management. # hypridle: idle-triggered locking and display power management.
let let
cfg = config.modules.desktop.hyprland.hypridle; cfg = config.modules.desktop.session.hyprland.hypridle;
user = config.user.name; user = config.user.name;
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl"; hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
@@ -17,7 +17,7 @@ let
lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"; lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}";
in in
{ {
options.modules.desktop.hyprland.hypridle = { options.modules.desktop.session.hyprland.hypridle = {
enable = lib.mkEnableOption "hypridle idle management"; enable = lib.mkEnableOption "hypridle idle management";
lockTimeout = lib.mkOption { lockTimeout = lib.mkOption {

View File

@@ -6,7 +6,7 @@
}: }:
# The lock screen: a hyprlock session-lock surface the compositor owns. # The lock screen: a hyprlock session-lock surface the compositor owns.
let let
cfg = config.modules.desktop.hyprland.hyprlock; cfg = config.modules.desktop.session.hyprland.hyprlock;
user = config.user.name; user = config.user.name;
# The hyprlock this module installs, used by the lock keybind below. # The hyprlock this module installs, used by the lock keybind below.
@@ -45,7 +45,7 @@ let
''; '';
in in
{ {
options.modules.desktop.hyprland.hyprlock.enable = lib.mkEnableOption "the hyprlock lock screen"; options.modules.desktop.session.hyprland.hyprlock.enable = lib.mkEnableOption "the hyprlock lock screen";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user} = { home-manager.users.${user} = {

View File

@@ -7,10 +7,10 @@
# Text login: greetd running the tuigreet greeter, which starts the session # Text login: greetd running the tuigreet greeter, which starts the session
# through the universal Wayland session manager. # through the universal Wayland session manager.
let let
cfg = config.modules.desktop.login; cfg = config.modules.desktop.session.login;
in in
{ {
options.modules.desktop.login.enable = lib.mkEnableOption "greetd with the tuigreet text greeter"; options.modules.desktop.session.login.enable = lib.mkEnableOption "greetd with the tuigreet text greeter";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.greetd = { services.greetd = {

View File

@@ -6,13 +6,13 @@
}: }:
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall. # mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
let let
cfg = config.modules.desktop.mako; cfg = config.modules.desktop.session.mako;
user = config.user.name; user = config.user.name;
makoctl = "${pkgs.mako}/bin/makoctl"; makoctl = "${pkgs.mako}/bin/makoctl";
in in
{ {
options.modules.desktop.mako.enable = lib.mkEnableOption "the mako notification daemon"; options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user} = { home-manager.users.${user} = {

View File

@@ -1,10 +1,10 @@
{ config, lib, ... }: { config, lib, ... }:
# XDG desktop portal routing for in-app screen sharing and file dialogs. # XDG desktop portal routing for in-app screen sharing and file dialogs.
let let
cfg = config.modules.desktop.portals; cfg = config.modules.desktop.session.portals;
in in
{ {
options.modules.desktop.portals.enable = options.modules.desktop.session.portals.enable =
lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs"; lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {

View File

@@ -6,7 +6,7 @@
}: }:
# rofi as the desktop launcher and dmenu-style menu frontend. # rofi as the desktop launcher and dmenu-style menu frontend.
let let
cfg = config.modules.desktop.rofi; cfg = config.modules.desktop.session.rofi;
user = config.user.name; user = config.user.name;
# A session menu built on the same themed rofi, so the power actions look # A session menu built on the same themed rofi, so the power actions look
@@ -24,7 +24,7 @@ let
''; '';
in in
{ {
options.modules.desktop.rofi.enable = lib.mkEnableOption "the rofi launcher"; options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user} = { home-manager.users.${user} = {

View File

@@ -6,7 +6,7 @@
}: }:
# Waybar: a top status bar reading system state at a glance. # Waybar: a top status bar reading system state at a glance.
let let
cfg = config.modules.desktop.waybar; cfg = config.modules.desktop.session.waybar;
user = config.user.name; user = config.user.name;
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded # A Nerd Font Private-Use-Area codepoint as a literal character, decoded
@@ -49,7 +49,7 @@ let
''; '';
in in
{ {
options.modules.desktop.waybar.enable = lib.mkEnableOption "the Waybar status bar"; options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# A symbols font backs the module glyphs, so pango falls back to it for the # A symbols font backs the module glyphs, so pango falls back to it for the

View File

@@ -1,10 +1,10 @@
{ config, lib, ... }: { config, lib, ... }:
# PipeWire as the desktop audio server. # PipeWire as the desktop audio server.
let let
cfg = config.modules.desktop.audio; cfg = config.modules.desktop.utilities.audio;
in in
{ {
options.modules.desktop.audio.enable = lib.mkEnableOption "the PipeWire audio server"; options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# Realtime scheduling for the audio threads, so playback survives load. # Realtime scheduling for the audio threads, so playback survives load.

View File

@@ -6,7 +6,7 @@
}: }:
# Clipboard history: cliphist records every copy, picked back through rofi. # Clipboard history: cliphist records every copy, picked back through rofi.
let let
cfg = config.modules.desktop.clipboard; cfg = config.modules.desktop.utilities.clipboard;
user = config.user.name; user = config.user.name;
cliphist = "${pkgs.cliphist}/bin/cliphist"; cliphist = "${pkgs.cliphist}/bin/cliphist";
@@ -25,7 +25,7 @@ let
''; '';
in in
{ {
options.modules.desktop.clipboard.enable = lib.mkEnableOption "cliphist clipboard history"; options.modules.desktop.utilities.clipboard.enable = lib.mkEnableOption "cliphist clipboard history";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user} = { home-manager.users.${user} = {

View File

@@ -6,11 +6,11 @@
}: }:
# SwayOSD: a transient on-screen popup for volume and brightness changes. # SwayOSD: a transient on-screen popup for volume and brightness changes.
let let
cfg = config.modules.desktop.osd; cfg = config.modules.desktop.utilities.osd;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.desktop.osd.enable = lib.mkEnableOption "the SwayOSD volume and brightness popup"; options.modules.desktop.utilities.osd.enable = lib.mkEnableOption "the SwayOSD volume and brightness popup";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.swayosd ]; environment.systemPackages = [ pkgs.swayosd ];

View File

@@ -6,7 +6,7 @@
}: }:
# Keybound screen recording: pick a region, then toggle a video-only capture. # Keybound screen recording: pick a region, then toggle a video-only capture.
let let
cfg = config.modules.desktop.recording; cfg = config.modules.desktop.utilities.recording;
user = config.user.name; user = config.user.name;
slurp = "${pkgs.slurp}/bin/slurp"; slurp = "${pkgs.slurp}/bin/slurp";
@@ -41,7 +41,7 @@ let
''; '';
in in
{ {
options.modules.desktop.recording.enable = lib.mkEnableOption "wf-recorder screen recording"; options.modules.desktop.utilities.recording.enable = lib.mkEnableOption "wf-recorder screen recording";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user} = { home-manager.users.${user} = {

View File

@@ -7,7 +7,7 @@
# Keyboard-driven screenshots that open in satty for annotation, then land in # Keyboard-driven screenshots that open in satty for annotation, then land in
# both the clipboard and a dated file. # both the clipboard and a dated file.
let let
cfg = config.modules.desktop.screenshot; cfg = config.modules.desktop.utilities.screenshot;
user = config.user.name; user = config.user.name;
grimblast = "${pkgs.grimblast}/bin/grimblast"; grimblast = "${pkgs.grimblast}/bin/grimblast";
@@ -32,7 +32,7 @@ let
''; '';
in in
{ {
options.modules.desktop.screenshot.enable = lib.mkEnableOption "screenshots via grimblast and satty"; options.modules.desktop.utilities.screenshot.enable = lib.mkEnableOption "screenshots via grimblast and satty";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home-manager.users.${user} = { home-manager.users.${user} = {

View File

@@ -6,7 +6,7 @@
}: }:
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix. # Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
let let
cfg = config.modules.desktop.theming; cfg = config.modules.desktop.utilities.theming;
user = config.user.name; user = config.user.name;
# A single static Nord wallpaper, drawn as a vertical gradient across the # A single static Nord wallpaper, drawn as a vertical gradient across the
@@ -18,7 +18,7 @@ let
''; '';
in in
{ {
options.modules.desktop.theming.enable = options.modules.desktop.utilities.theming.enable =
lib.mkEnableOption "Nord theming of the graphical layer via Stylix"; lib.mkEnableOption "Nord theming of the graphical layer via Stylix";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {

View File

@@ -2,11 +2,11 @@
# Declares the XDG user directories, so a tool that queries them lands in a # Declares the XDG user directories, so a tool that queries them lands in a
# well-known folder a host can relocate from one place. # well-known folder a host can relocate from one place.
let let
cfg = config.modules.desktop.userdirs; cfg = config.modules.desktop.utilities.userdirs;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.desktop.userdirs.enable = lib.mkEnableOption "XDG user directories"; options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# Write ~/.config/user-dirs.dirs from the option defaults. # Write ~/.config/user-dirs.dirs from the option defaults.

View File

@@ -8,7 +8,7 @@ in
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
system.stateVersion = "26.05"; system.stateVersion = "26.05";
modules.ssh.enable = lib.mkDefault true; modules.network.ssh.enable = lib.mkDefault true;
modules.ssh.hostKeys.restore = lib.mkDefault false; modules.network.ssh.hostKeys.restore = lib.mkDefault false;
}; };
} }

View File

@@ -6,7 +6,7 @@
}: }:
# SSH on this machine, in both directions. # SSH on this machine, in both directions.
let let
cfg = config.modules.ssh; cfg = config.modules.network.ssh;
user = config.user.name; user = config.user.name;
inherit (lib) inherit (lib)
@@ -153,7 +153,7 @@ let
); );
in in
{ {
options.modules.ssh = { options.modules.network.ssh = {
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets"; enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
hosts = { hosts = {
@@ -226,7 +226,7 @@ in
default = null; default = null;
description = '' description = ''
Complete override for client public keys admitted by this host. Complete override for client public keys admitted by this host.
Leave null to derive access from `modules.ssh.hosts` and `modules.ssh.targets`. Leave null to derive access from `modules.network.ssh.hosts` and `modules.network.ssh.targets`.
''; '';
}; };
@@ -304,7 +304,7 @@ in
assertions = [ assertions = [
{ {
assertion = cfg.hostKeys.sopsFile != null; assertion = cfg.hostKeys.sopsFile != null;
message = "modules.ssh.hostKeys.restore requires modules.ssh.hostKeys.sopsFile to name the encrypted host keys."; message = "modules.network.ssh.hostKeys.restore requires modules.network.ssh.hostKeys.sopsFile to name the encrypted host keys.";
} }
]; ];

View File

@@ -6,30 +6,26 @@
}: }:
# The host networking foundation: per-VLAN bridges over a tagged trunk. # The host networking foundation: per-VLAN bridges over a tagged trunk.
let let
cfg = config.modules.network; cfg = config.modules.network.vlans;
# Each tagged VLAN materializes as a bridge named for its id, by the shared
# convention.
inherit (my) bridgeName; inherit (my) bridgeName;
# The tagged sub-interface stacked on the trunk that feeds one bridge.
vlanName = id: "${cfg.trunk}.${toString id}"; vlanName = id: "${cfg.trunk}.${toString id}";
in in
{ {
options.modules.network = { options.modules.network.vlans = {
enable = enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk";
lib.mkEnableOption "the host trunk, its tagged-VLAN bridges, and the host management address";
trunk = lib.mkOption { trunk = lib.mkOption {
type = lib.types.str; type = lib.types.str;
example = "enp1s0"; example = "enp1s0";
description = '' description = ''
The physical interface carrying 802.1Q-tagged traffic for every VLAN. Physical interface that carries 802.1Q-tagged traffic for every VLAN.
Each declared VLAN is stacked on it and enslaved to its own bridge. Each declared VLAN attaches to its own bridge.
''; '';
}; };
vlans = lib.mkOption { ids = lib.mkOption {
type = lib.types.listOf (lib.types.ints.between 1 4094); type = lib.types.listOf (lib.types.ints.between 1 4094);
default = [ ]; default = [ ];
example = [ example = [
@@ -38,8 +34,8 @@ in
30 30
]; ];
description = '' description = ''
The tagged VLAN ids to materialize. Each id N emits exactly one bridge Tagged VLAN ids to materialize.
named `br-vlanN`, and guests on VLAN N attach to it. Each id N creates one bridge named `br-vlanN`.
''; '';
}; };
@@ -48,9 +44,9 @@ in
type = lib.types.nullOr (lib.types.ints.between 1 4094); type = lib.types.nullOr (lib.types.ints.between 1 4094);
default = null; default = null;
description = '' description = ''
The VLAN whose bridge carries the host's own management address; must VLAN whose bridge carries the host's own management address.
be one of `vlans`. Left null, the host takes no address on any bridge The value must be one of `ids`.
and the bridges serve guests alone. Left null, the host takes no address on any bridge.
''; '';
}; };
@@ -59,8 +55,8 @@ in
default = null; default = null;
example = "10.0.10.2/24"; example = "10.0.10.2/24";
description = '' description = ''
The host's static management address, in CIDR form, on the management Host static management address in CIDR form.
VLAN's bridge. Left null, the host takes its address there by DHCP. Left null, the host takes its management address by DHCP.
''; '';
}; };
}; };
@@ -69,21 +65,20 @@ in
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
assertions = [ assertions = [
{ {
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.vlans; assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids;
message = "modules.network.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans."; message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids.";
} }
{ {
assertion = cfg.management.address == null || cfg.management.vlan != null; assertion = cfg.management.address == null || cfg.management.vlan != null;
message = "modules.network.management.address needs modules.network.management.vlan to say which bridge carries it."; message = "modules.network.vlans.management.address needs modules.network.vlans.management.vlan to say which bridge carries it.";
} }
]; ];
# networkd owns the trunk, its VLAN sub-interfaces, and the bridges, so a host # networkd owns these interfaces so NetworkManager does not contend for them.
# also running NetworkManager leaves them to networkd rather than contending.
networking.networkmanager.unmanaged = networking.networkmanager.unmanaged =
[ "interface-name:${cfg.trunk}" ] [ "interface-name:${cfg.trunk}" ]
++ map (id: "interface-name:${bridgeName id}") cfg.vlans ++ map (id: "interface-name:${bridgeName id}") cfg.ids
++ map (id: "interface-name:${vlanName id}") cfg.vlans; ++ map (id: "interface-name:${vlanName id}") cfg.ids;
systemd.network = { systemd.network = {
enable = true; enable = true;
@@ -103,19 +98,18 @@ in
}; };
vlanConfig.Id = id; vlanConfig.Id = id;
}; };
}) cfg.vlans }) cfg.ids
); );
networks = lib.mkMerge ( networks = lib.mkMerge (
[ [
# The trunk carries only tagged frames up to the sub-interfaces and # The trunk carries only tagged frames and takes no address.
# takes no address of its own.
{ {
"30-${cfg.trunk}" = { "30-${cfg.trunk}" = {
matchConfig.Name = cfg.trunk; matchConfig.Name = cfg.trunk;
networkConfig.LinkLocalAddressing = "no"; networkConfig.LinkLocalAddressing = "no";
linkConfig.RequiredForOnline = "no"; linkConfig.RequiredForOnline = "no";
vlan = map vlanName cfg.vlans; vlan = map vlanName cfg.ids;
}; };
} }
] ]
@@ -126,8 +120,8 @@ in
networkConfig.Bridge = bridgeName id; networkConfig.Bridge = bridgeName id;
linkConfig.RequiredForOnline = "no"; linkConfig.RequiredForOnline = "no";
}; };
# The management VLAN's bridge carries the host address; every other # The management bridge carries the host address.
# bridge is a plain L2 segment for guests. # Other bridges provide L2 segments for guests.
"40-${bridgeName id}" = lib.mkMerge [ "40-${bridgeName id}" = lib.mkMerge [
{ {
matchConfig.Name = bridgeName id; matchConfig.Name = bridgeName id;
@@ -140,7 +134,7 @@ in
{ address = [ cfg.management.address ]; } { address = [ cfg.management.address ]; }
)) ))
]; ];
}) cfg.vlans }) cfg.ids
); );
}; };
}; };

View File

@@ -5,11 +5,11 @@
}: }:
# direnv for the primary user, configured through home-manager. # direnv for the primary user, configured through home-manager.
let let
cfg = config.modules.direnv; cfg = config.modules.shell.direnv;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.direnv.enable = options.modules.shell.direnv.enable =
lib.mkEnableOption "direnv for the primary user, with nix-direnv shell caching"; lib.mkEnableOption "direnv for the primary user, with nix-direnv shell caching";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {

View File

@@ -6,13 +6,13 @@
}: }:
# fish for the primary user, configured through home-manager. # fish for the primary user, configured through home-manager.
let let
cfg = config.modules.fish; cfg = config.modules.shell.fish;
user = config.user.name; user = config.user.name;
shortcuts = config.registry.shell-shortcut.${config.networking.hostName}; shortcuts = config.registry.shell-shortcut.${config.networking.hostName};
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts; commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
in in
{ {
options.modules.fish = { options.modules.shell.fish = {
enable = lib.mkEnableOption "fish as the user's shell, configured via home-manager"; enable = lib.mkEnableOption "fish as the user's shell, configured via home-manager";
defaultShell = lib.mkOption { defaultShell = lib.mkOption {

View File

@@ -5,11 +5,11 @@
}: }:
# git for the primary user, configured through home-manager. # git for the primary user, configured through home-manager.
let let
cfg = config.modules.git; cfg = config.modules.shell.git;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.git.enable = options.modules.shell.git.enable =
lib.mkEnableOption "git for the primary user, carrying the operator's commit identity"; lib.mkEnableOption "git for the primary user, carrying the operator's commit identity";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {

View File

@@ -6,11 +6,11 @@
}: }:
# Herdr, a terminal multiplexer for coding agents. # Herdr, a terminal multiplexer for coding agents.
let let
cfg = config.modules.agents.herdr; cfg = config.modules.shell.herdr;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.agents.herdr.enable = lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents"; options.modules.shell.herdr.enable = lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session"; registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";

View File

@@ -7,11 +7,11 @@
}: }:
# Neovim for the primary user, configured declaratively through nixvim. # Neovim for the primary user, configured declaratively through nixvim.
let let
cfg = config.modules.nvim; cfg = config.modules.shell.nvim;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.nvim.enable = lib.mkEnableOption "Neovim, configured declaratively via nixvim"; options.modules.shell.nvim.enable = lib.mkEnableOption "Neovim, configured declaratively via nixvim";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
registry.shell-shortcut.${config.networking.hostName} = { registry.shell-shortcut.${config.networking.hostName} = {

View File

@@ -6,11 +6,11 @@
# tmux for the primary user, configured through home-manager. # tmux for the primary user, configured through home-manager.
# Settings without a home-manager option are read from ./extra.conf. # Settings without a home-manager option are read from ./extra.conf.
let let
cfg = config.modules.tmux; cfg = config.modules.shell.tmux;
user = config.user.name; user = config.user.name;
in in
{ {
options.modules.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager"; options.modules.shell.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s"; registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s";