528 lines
14 KiB
Nix
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
|
|
;
|
|
};
|
|
}
|