Files
lux/modules/lib.nix
T

528 lines
14 KiB
Nix

{
config,
lib,
...
}:
let
mkHostUser =
{
account,
needsPassword ? false,
homeImports ? [ ],
stateVersion ? null,
}:
{
inherit
account
homeImports
needsPassword
stateVersion
;
};
mkHost =
{
name,
displays ? { },
input ? { },
sourceControl ? { },
users ? { },
imports ? [ ],
stateVersion ? "24.05",
}:
{
config,
pkgs,
...
}:
let
hostUserSpecs = lib.mapAttrs (_: spec: mkHostUser spec) users;
hostUsers = lib.mapAttrs (_: spec: spec.account) hostUserSpecs;
userAssertions = lib.mapAttrsToList (userName: spec: {
assertion = userName == spec.account.name;
message = "Host `${name}` declares user `${userName}` with mismatched account name `${spec.account.name}`.";
}) hostUserSpecs;
passwordUserSpecs = lib.filterAttrs (_: spec: spec.needsPassword) hostUserSpecs;
in
{
assertions = userAssertions;
meta.host = {
inherit
displays
input
name
sourceControl
;
users = hostUsers;
};
inherit imports;
networking.hostName = name;
system.stateVersion = stateVersion;
programs.zsh.enable = true;
sops.secrets = lib.mapAttrs' (
userName: _:
lib.nameValuePair "hashed-password-${userName}" {
neededForUsers = true;
}
) passwordUserSpecs;
users.users = lib.mapAttrs (
userName: spec:
{
name = spec.account.name;
home = spec.account.homeDirectory;
isNormalUser = true;
shell = pkgs.zsh;
extraGroups = [
"wheel"
"networkmanager"
];
}
// lib.optionalAttrs spec.needsPassword {
hashedPasswordFile = config.sops.secrets."hashed-password-${userName}".path;
}
) hostUserSpecs;
home-manager.users = lib.mapAttrs (
_: spec:
{
imports = spec.homeImports;
meta = {
host = config.meta.host;
user = spec.account;
};
home = {
username = spec.account.name;
homeDirectory = spec.account.homeDirectory;
stateVersion = if spec.stateVersion == null then stateVersion else spec.stateVersion;
};
}
) hostUserSpecs;
};
mkCaddyReverseProxy =
{
domain,
port,
extraHeaders ? [ ],
extraConfigText ? "",
}:
let
headerLines = map (header: " header_up ${header.name} ${header.value}") extraHeaders;
extraConfigLines = map (line: " ${line}") (
lib.filter (line: line != "") (lib.splitString "\n" extraConfigText)
);
bodyLines = headerLines ++ extraConfigLines;
body = lib.concatStringsSep "\n" bodyLines;
in
{
services.caddy.virtualHosts.${domain}.extraConfig =
if body == "" then
"reverse_proxy :${toString port}"
else
''
reverse_proxy :${toString port} {
${body}
}
'';
};
resolvePackagePath =
{
pkgs,
path,
}:
lib.attrByPath path null pkgs;
repo = {
contact.email = "mail@jelles.net";
services = {
actual = {
domain = "finance.jelles.net";
host = "127.0.0.1";
port = 3000;
url = "https://finance.jelles.net";
};
gitea = {
domain = "git.jelles.net";
host = "127.0.0.1";
port = 3001;
url = "https://git.jelles.net/";
};
radicale = {
domain = "radicale.jelles.net";
host = "127.0.0.1";
port = 5232;
url = "https://radicale.jelles.net/";
};
vaultwarden = {
domain = "vault.jelles.net";
host = "127.0.0.1";
port = 8100;
url = "https://vault.jelles.net";
};
};
theme = {
cursor = {
name = "phinger-cursors-light";
packagePath = [ "phinger-cursors" ];
size = 24;
};
kanagawa = {
displayName = "Kanagawa Wave";
name = "kanagawa-wave";
gtkThemeName = "Kanagawa-BL-LB";
iconThemeName = "Kanagawa";
owner = "Fausto-Korpsvart";
repo = "Kanagawa-GKT-Theme";
rev = "55ca4ba249eba21f861b9866b71ab41bb8930318";
hash = "sha256-UdMoMx2DoovcxSp/zBZ3PRv/Qpj+prd0uPm1gmdak2E=";
version = "unstable-2025-10-23";
palette = {
background = "#1F1F28";
foreground = "#DCD7BA";
secondaryBackground = "#16161D";
border = "#2A2A37";
selectionBackground = "#2D4F67";
selectionForeground = "#C8C093";
url = "#72A7BC";
cursor = "#C8C093";
muted = "#727169";
accents = {
blue = "#7E9CD8";
green = "#98BB6C";
magenta = "#D27E99";
orange = "#FFA066";
purple = "#957FB8";
red = "#E82424";
yellow = "#E6C384";
cyan = "#7AA89F";
};
niri.border = {
active = "#7E9CD8";
inactive = "#54546D";
urgent = "#E82424";
};
terminal = {
color0 = "#16161D";
color1 = "#C34043";
color2 = "#76946A";
color3 = "#C0A36E";
color4 = "#7E9CD8";
color5 = "#957FB8";
color6 = "#6A9589";
color7 = "#C8C093";
color8 = "#727169";
color9 = "#E82424";
color10 = "#98BB6C";
color11 = "#E6C384";
color12 = "#7FB4CA";
color13 = "#938AA9";
color14 = "#7AA89F";
color15 = "#DCD7BA";
color16 = "#FFA066";
color17 = "#FF5D62";
};
};
};
};
};
resolveUserTerminal =
{
pkgs,
user,
}:
let
package = resolvePackagePath {
inherit pkgs;
path = user.terminalPackagePath;
};
hasPackage = package != null;
hasMainProgram = hasPackage && package ? meta.mainProgram;
mainProgram = if hasMainProgram then package.meta.mainProgram else null;
desktopId = if mainProgram == null then null else "${mainProgram}.desktop";
in
{
inherit
desktopId
hasMainProgram
hasPackage
mainProgram
package
;
hasDesktopEntry =
desktopId != null && builtins.pathExists "${package}/share/applications/${desktopId}";
hasTerminfo = hasPackage && lib.elem "terminfo" package.outputs;
};
mkTerminalAssertions =
{
terminal,
user,
requireDesktopEntry ? false,
requireKitty ? false,
requireMainProgram ? true,
requireTerminfo ? false,
}:
lib.flatten [
[
{
assertion = terminal.hasPackage;
message = "Unknown terminal package `${lib.showAttrPath user.terminalPackagePath}` for user `${user.name}`.";
}
]
(lib.optional requireMainProgram {
assertion = terminal.hasMainProgram;
message = "Terminal package `${lib.showAttrPath user.terminalPackagePath}` must define `meta.mainProgram`.";
})
(lib.optional requireDesktopEntry {
assertion = terminal.hasDesktopEntry;
message = "Terminal package `${lib.showAttrPath user.terminalPackagePath}` must provide `${terminal.desktopId}`.";
})
(lib.optional requireKitty {
assertion = terminal.hasMainProgram && terminal.mainProgram == "kitty";
message = "The terminal feature currently only supports kitty-specific Home Manager configuration.";
})
(lib.optional requireTerminfo {
assertion = terminal.hasTerminfo;
message = "Terminal package `${lib.showAttrPath user.terminalPackagePath}` must provide a `terminfo` output.";
})
];
mapConfiguredAttrs =
{
settings,
schema,
}:
lib.filterAttrs (_: value: value != null) (
lib.mapAttrs (
_: spec:
let
value = lib.getAttrFromPath spec.path settings;
in
if value == null then
null
else if spec ? values then
spec.values.${value}
else if spec ? transform then
spec.transform value
else
value
) schema
);
mkInputProfiles =
input:
let
scrollMethodValues = {
edge = {
libinput = "edge";
niri = "edge";
};
"no-scroll" = {
libinput = "none";
niri = "no-scroll";
};
"on-button-down" = {
libinput = "button";
niri = "on-button-down";
};
"two-finger" = {
libinput = "twofinger";
niri = "two-finger";
};
};
mouse = input.mouse;
touchpad = input.touchpad;
libinputMouse = mapConfiguredAttrs {
settings = mouse;
schema = {
accelProfile.path = [ "accelProfile" ];
accelSpeed = {
path = [ "accelSpeed" ];
transform = toString;
};
leftHanded.path = [ "leftHanded" ];
middleEmulation.path = [ "middleEmulation" ];
naturalScrolling.path = [ "naturalScrolling" ];
scrollMethod = {
path = [ "scrollMethod" ];
values = lib.mapAttrs (_: value: value.libinput) scrollMethodValues;
};
};
};
libinputTouchpad = mapConfiguredAttrs {
settings = touchpad;
schema = {
accelProfile.path = [ "accelProfile" ];
accelSpeed = {
path = [ "accelSpeed" ];
transform = toString;
};
clickMethod = {
path = [ "clickMethod" ];
values = {
"button-areas" = "buttonareas";
clickfinger = "clickfinger";
};
};
disableWhileTyping.path = [ "disableWhileTyping" ];
leftHanded.path = [ "leftHanded" ];
middleEmulation.path = [ "middleEmulation" ];
naturalScrolling.path = [ "naturalScrolling" ];
scrollMethod = {
path = [ "scrollMethod" ];
values = lib.mapAttrs (_: value: value.libinput) scrollMethodValues;
};
tapping.path = [ "tapping" ];
};
};
niriMouse = mapConfiguredAttrs {
settings = mouse;
schema = {
"accel-profile".path = [ "accelProfile" ];
"accel-speed".path = [ "accelSpeed" ];
"left-handed".path = [ "leftHanded" ];
"middle-emulation".path = [ "middleEmulation" ];
"natural-scroll".path = [ "naturalScrolling" ];
"scroll-method" = {
path = [ "scrollMethod" ];
values = lib.mapAttrs (_: value: value.niri) scrollMethodValues;
};
};
};
niriTouchpad = mapConfiguredAttrs {
settings = touchpad;
schema = {
"accel-profile".path = [ "accelProfile" ];
"accel-speed".path = [ "accelSpeed" ];
"click-method".path = [ "clickMethod" ];
dwt.path = [ "disableWhileTyping" ];
"left-handed".path = [ "leftHanded" ];
"middle-emulation".path = [ "middleEmulation" ];
"natural-scroll".path = [ "naturalScrolling" ];
"scroll-method" = {
path = [ "scrollMethod" ];
values = lib.mapAttrs (_: value: value.niri) scrollMethodValues;
};
tap.path = [ "tapping" ];
};
};
in
{
hasPointerConfig = libinputMouse != { } || libinputTouchpad != { };
libinput = {
mouse = libinputMouse;
touchpad = libinputTouchpad;
};
niri = {
mouse = niriMouse;
touchpad = niriTouchpad;
};
};
in
{
options.meta.lib.mkHost = lib.mkOption {
type = lib.types.raw;
description = "Internal host constructor shared between flake-parts modules.";
internal = true;
readOnly = true;
};
options.meta.lib.mkHostUser = lib.mkOption {
type = lib.types.raw;
description = "Internal host user constructor shared between flake-parts modules.";
internal = true;
readOnly = true;
};
options.meta.lib.mkCaddyReverseProxy = lib.mkOption {
type = lib.types.raw;
description = "Internal Caddy reverse proxy helper shared between flake-parts modules.";
internal = true;
readOnly = true;
};
options.meta.lib.resolvePackagePath = lib.mkOption {
type = lib.types.raw;
description = "Internal helper to resolve package attr paths against the local pkgs set.";
internal = true;
readOnly = true;
};
options.meta.lib.resolveUserTerminal = lib.mkOption {
type = lib.types.raw;
description = "Internal helper to resolve and validate user terminal metadata.";
internal = true;
readOnly = true;
};
options.meta.lib.mkTerminalAssertions = lib.mkOption {
type = lib.types.raw;
description = "Internal helper for terminal-related assertions shared across modules.";
internal = true;
readOnly = true;
};
options.meta.lib.mkInputProfiles = lib.mkOption {
type = lib.types.raw;
description = "Internal helper to normalize host input metadata for libinput and Niri.";
internal = true;
readOnly = true;
};
options.meta.lib.users = lib.mkOption {
type = lib.types.attrs;
description = "Canonical user attrsets shared by host definitions.";
internal = true;
readOnly = true;
};
options.meta.lib.repo = lib.mkOption {
type = lib.types.attrs;
description = "Internal shared repository metadata.";
internal = true;
readOnly = true;
};
config.meta.lib = {
inherit
mkInputProfiles
mkCaddyReverseProxy
mkTerminalAssertions
mkHost
mkHostUser
repo
resolvePackagePath
resolveUserTerminal
;
};
}