refactor: simplify host composition and shared feature config

This commit is contained in:
2026-04-22 03:29:19 +02:00
parent 5eec5689f4
commit 503c1fe9bc
16 changed files with 327 additions and 238 deletions
-4
View File
@@ -3,12 +3,10 @@
flake.modules.homeManager.git =
{
config,
lib,
...
}:
let
user = config.meta.user;
usesScopedIdentity = user != null && user.sourceControl.profiles != { };
in
{
programs.git = {
@@ -20,8 +18,6 @@
];
settings = {
init.defaultBranch = "main";
}
// lib.optionalAttrs (!usesScopedIdentity) {
user = {
name = user.realName;
email = user.primaryEmail.address;
+54 -19
View File
@@ -1,5 +1,7 @@
{ lib, ... }:
let
nonEmptyStrType = lib.types.addCheck lib.types.str (value: lib.stringLength value > 0);
mkNullableOption =
type:
lib.mkOption {
@@ -10,9 +12,26 @@ let
hasSinglePrimaryEmail =
user: builtins.length (lib.filter (email: email.primary) (builtins.attrValues user.emails)) == 1;
scopeEmailCount =
scope: user:
builtins.length (lib.filter (email: email.scope == scope) (builtins.attrValues user.emails));
hasAtMostOneScopedEmail = scope: user: scopeEmailCount scope user <= 1;
requiredSourceControlScopes =
user:
lib.unique [
"personal"
user.sourceControl.projectScope
];
hasRequiredScopedEmail =
scope: user: scopeEmailCount scope user == 1;
primaryEmailFallback = {
address = "";
primary = false;
scope = null;
type = "";
};
@@ -53,6 +72,8 @@ let
type = lib.mkOption {
type = lib.types.str;
};
scope = mkNullableOption sourceControlScopeType;
};
}
);
@@ -62,13 +83,11 @@ let
{
options = {
publicKey = lib.mkOption {
type = lib.types.str;
};
privateKeyPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
type = lib.types.nullOr nonEmptyStrType;
default = null;
};
privateKeyPath = mkNullableOption nonEmptyStrType;
};
}
);
@@ -93,22 +112,10 @@ let
}
);
sourceControlProfileType = lib.types.submodule (
{ ... }:
{
options = { };
}
);
sourceControlType = lib.types.submodule (
{ ... }:
{
options = {
profiles = lib.mkOption {
type = lib.types.attrsOf sourceControlProfileType;
default = { };
};
projectScope = lib.mkOption {
type = sourceControlScopeType;
default = "personal";
@@ -305,7 +312,22 @@ in
config.assertions = lib.mapAttrsToList (userName: user: {
assertion = hasSinglePrimaryEmail user;
message = "User `${userName}` must define exactly one primary email entry.";
}) config.meta.host.users;
}) config.meta.host.users
++ lib.flatten (
lib.mapAttrsToList (userName: user:
(map (scope: {
assertion = hasAtMostOneScopedEmail scope user;
message = "User `${userName}` may define at most one `${scope}` scoped email entry.";
}) [
"personal"
"work"
])
++ map (scope: {
assertion = hasRequiredScopedEmail scope user;
message = "User `${userName}` must define exactly one `${scope}` scoped email entry.";
}) (requiredSourceControlScopes user)
) config.meta.host.users
);
};
flake.modules.homeManager.meta =
@@ -326,6 +348,19 @@ in
config.assertions = lib.optional (config.meta.user != null) {
assertion = hasSinglePrimaryEmail config.meta.user;
message = "User `${config.meta.user.name}` must define exactly one primary email entry.";
};
}
++ lib.optionals (config.meta.user != null) (
(map (scope: {
assertion = hasAtMostOneScopedEmail scope config.meta.user;
message = "User `${config.meta.user.name}` may define at most one `${scope}` scoped email entry.";
}) [
"personal"
"work"
])
++ map (scope: {
assertion = hasRequiredScopedEmail scope config.meta.user;
message = "User `${config.meta.user.name}` must define exactly one `${scope}` scoped email entry.";
}) (requiredSourceControlScopes config.meta.user)
);
};
}
@@ -0,0 +1,45 @@
{ themeName }:
''
require("kanagawa").setup({
colors = {
theme = {
all = {
ui = {
bg_gutter = "none"
}
}
}
},
overrides = function(colors)
local theme = colors.theme
local makeDiagnosticColor = function(color)
local c = require("kanagawa.lib.color")
return { fg = color, bg = c(color):blend(theme.ui.bg, 0.95):to_hex() }
end
return {
TelescopeTitle = { fg = theme.ui.special, bold = true },
TelescopePromptNormal = { bg = theme.ui.bg_p1 },
TelescopePromptBorder = { fg = theme.ui.bg_p1, bg = theme.ui.bg_p1 },
TelescopeResultsNormal = { fg = theme.ui.fg_dim, bg = theme.ui.bg_m1 },
TelescopeResultsBorder = { fg = theme.ui.bg_m1, bg = theme.ui.bg_m1 },
TelescopePreviewNormal = { bg = theme.ui.bg_dim },
TelescopePreviewBorder = { bg = theme.ui.bg_dim, fg = theme.ui.bg_dim },
Pmenu = { fg = theme.ui.shade0, bg = theme.ui.bg_p1 }, -- add `blend = vim.o.pumblend` to enable transparency
PmenuSel = { fg = "NONE", bg = theme.ui.bg_p2 },
PmenuSbar = { bg = theme.ui.bg_m1 },
PmenuThumb = { bg = theme.ui.bg_p2 },
DiagnosticVirtualTextHint = makeDiagnosticColor(theme.diag.hint),
DiagnosticVirtualTextInfo = makeDiagnosticColor(theme.diag.info),
DiagnosticVirtualTextWarn = makeDiagnosticColor(theme.diag.warning),
DiagnosticVirtualTextError = makeDiagnosticColor(theme.diag.error),
}
end,
})
vim.cmd.colorscheme("${themeName}")
''
+9 -48
View File
@@ -1,3 +1,7 @@
{ config, ... }:
let
repoTheme = config.meta.lib.repo.theme.kanagawa;
in
{
flake.modules.homeManager.neovim =
{
@@ -109,56 +113,13 @@
# Hostname/ConfigDir needed for nixd
nixdExtras = {
nixpkgs = "import ${pkgs.path} {}";
nixos_options = ''(builtins.getFlake "path://${config.home.homeDirectory}/.config/nixos").nixosConfigurations.${config.meta.host.name}.options'';
home_manager_options = ''(builtins.getFlake "path://${config.home.homeDirectory}/.config/nixos").nixosConfigurations.${config.meta.host.name}.options.home-manager.users.type.getSubOptions []'';
nixos_options = ''(builtins.getFlake "path://${config.meta.user.nixosConfigurationPath}").nixosConfigurations.${config.meta.host.name}.options'';
home_manager_options = ''(builtins.getFlake "path://${config.meta.user.nixosConfigurationPath}").nixosConfigurations.${config.meta.host.name}.options.home-manager.users.type.getSubOptions []'';
};
# TODO: Put in separate theme file
themeSetup = # lua
''
require("kanagawa").setup({
colors = {
theme = {
all = {
ui = {
bg_gutter = "none"
}
}
}
},
overrides = function(colors)
local theme = colors.theme
local makeDiagnosticColor = function(color)
local c = require("kanagawa.lib.color")
return { fg = color, bg = c(color):blend(theme.ui.bg, 0.95):to_hex() }
end
return {
TelescopeTitle = { fg = theme.ui.special, bold = true },
TelescopePromptNormal = { bg = theme.ui.bg_p1 },
TelescopePromptBorder = { fg = theme.ui.bg_p1, bg = theme.ui.bg_p1 },
TelescopeResultsNormal = { fg = theme.ui.fg_dim, bg = theme.ui.bg_m1 },
TelescopeResultsBorder = { fg = theme.ui.bg_m1, bg = theme.ui.bg_m1 },
TelescopePreviewNormal = { bg = theme.ui.bg_dim },
TelescopePreviewBorder = { bg = theme.ui.bg_dim, fg = theme.ui.bg_dim },
Pmenu = { fg = theme.ui.shade0, bg = theme.ui.bg_p1 }, -- add `blend = vim.o.pumblend` to enable transparency
PmenuSel = { fg = "NONE", bg = theme.ui.bg_p2 },
PmenuSbar = { bg = theme.ui.bg_m1 },
PmenuThumb = { bg = theme.ui.bg_p2 },
DiagnosticVirtualTextHint = makeDiagnosticColor(theme.diag.hint),
DiagnosticVirtualTextInfo = makeDiagnosticColor(theme.diag.info),
DiagnosticVirtualTextWarn = makeDiagnosticColor(theme.diag.warning),
DiagnosticVirtualTextError = makeDiagnosticColor(theme.diag.error),
}
end,
})
vim.cmd.colorscheme("kanagawa-wave")
'';
themeSetup = import ./_kanagawa-theme.nix {
themeName = repoTheme.name;
};
};
+6 -6
View File
@@ -30,6 +30,7 @@ in
...
}:
let
repoTheme = metaLib.repo.theme.kanagawa;
outputs = lib.mapAttrs (
_: display:
{
@@ -41,10 +42,10 @@ in
// lib.optionalAttrs (display.primary or false) {
"focus-at-startup" = true;
}
// lib.optionalAttrs (display ? scale) {
// lib.optionalAttrs (display.scale != null) {
inherit (display) scale;
}
// lib.optionalAttrs (display ? mode) {
// lib.optionalAttrs (display.mode != null) {
inherit (display) mode;
}
) config.meta.host.displays;
@@ -81,7 +82,6 @@ in
spawn-at-startup = [
{ command = [ "xwayland-satellite" ]; }
{ command = [ "noctalia-shell" ]; }
{ command = [ "qbittorrent" ]; }
];
prefer-no-csd = true;
hotkey-overlay.skip-at-startup = true;
@@ -106,9 +106,9 @@ in
border = {
enable = true;
width = 3;
active.color = "#7E9CD8";
inactive.color = "#54546D";
urgent.color = "#E82424";
active.color = repoTheme.palette.niri.border.active;
inactive.color = repoTheme.palette.niri.border.inactive;
urgent.color = repoTheme.palette.niri.border.urgent;
};
};
+9 -1
View File
@@ -7,8 +7,16 @@
};
flake.modules.homeManager.qbittorrent-client =
{ pkgs, ... }:
{
lib,
pkgs,
...
}:
{
home.packages = [ pkgs.qbittorrent ];
programs.niri.settings.spawn-at-startup = lib.mkAfter [
{ command = [ "qbittorrent" ]; }
];
};
}
+26 -55
View File
@@ -14,40 +14,11 @@ in
user = config.meta.user;
sourceControl = user.sourceControl;
hostSourceControlUsers = host.sourceControl.users;
hostUserSourceControl =
if lib.hasAttr user.name hostSourceControlUsers then hostSourceControlUsers.${user.name} else { };
profileNames = builtins.attrNames sourceControl.profiles;
parsedProfiles = map (
name:
let
matches = builtins.match "(github|gitlab)-(personal|work)" name;
in
{
inherit matches name;
isValid = matches != null;
scope = if matches == null then null else builtins.elemAt matches 1;
}
) profileNames;
validProfiles = builtins.filter (profile: profile.isValid) parsedProfiles;
invalidProfileNames = map (profile: profile.name) (
builtins.filter (profile: !profile.isValid) parsedProfiles
);
emailNamesForScope = {
personal = [
"personal"
"main"
];
work = [ "work" ];
};
hostUserSourceControl = hostSourceControlUsers.${user.name} or { };
scopeEmails =
scope:
map (name: user.emails.${name}) (
builtins.filter (name: lib.hasAttr name user.emails) emailNamesForScope.${scope}
);
lib.filter (email: email.scope == scope) (builtins.attrValues user.emails);
emailForScope =
scope:
@@ -56,8 +27,14 @@ in
in
if builtins.length emails == 1 then (builtins.head emails).address else null;
scopeConfig =
scope: if lib.hasAttr scope hostUserSourceControl then hostUserSourceControl.${scope} else null;
scopeConfig = scope: hostUserSourceControl.${scope} or null;
scopeHasSigningKey =
scope:
let
keyConfig = scopeConfig scope;
in
keyConfig != null && keyConfig.publicKey != null;
privateKeyPathForScope =
scope:
@@ -69,7 +46,7 @@ in
else
keyConfig.privateKeyPath;
scopePublicKey =
publicKeyForScope =
scope:
let
keyConfig = scopeConfig scope;
@@ -81,23 +58,25 @@ in
"personal"
sourceControl.projectScope
]
++ map (profile: profile.scope) validProfiles
);
missingKeyScopes = builtins.filter (scope: scopePublicKey scope == null) scopesInUse;
invalidEmailScopes = builtins.filter (scope: emailForScope scope == null) scopesInUse;
allowedSignersLines = map (scope: "${emailForScope scope} ${scopePublicKey scope}") (
builtins.filter (scope: emailForScope scope != null && scopePublicKey scope != null) scopesInUse
allowedSignersLines = map (scope: "${emailForScope scope} ${publicKeyForScope scope}") (
builtins.filter (scope: emailForScope scope != null && scopeHasSigningKey scope) scopesInUse
);
gitConfigForScope = scope: {
gpg.ssh.allowedSignersFile = "${config.xdg.configHome}/git/allowed_signers";
user = {
name = user.realName;
email = emailForScope scope;
signingKey = "${privateKeyPathForScope scope}.pub";
};
};
gitConfigForScope =
scope:
lib.recursiveUpdate {
user = {
name = user.realName;
email = emailForScope scope;
};
}
(lib.optionalAttrs (scopeHasSigningKey scope) {
gpg.ssh.allowedSignersFile = "${config.xdg.configHome}/git/allowed_signers";
user.signingKey = "${privateKeyPathForScope scope}.pub";
});
gitRoots = [
{
@@ -114,17 +93,9 @@ in
imports = [ homeModules.git ];
assertions = [
{
assertion = invalidProfileNames == [ ];
message = "Invalid source control profiles for `${user.name}`: ${lib.concatStringsSep ", " invalidProfileNames}. Expected `<service>-<scope>` using github/gitlab and personal/work.";
}
{
assertion = missingKeyScopes == [ ];
message = "Missing source control keys for `${user.name}` scopes: ${lib.concatStringsSep ", " missingKeyScopes}.";
}
{
assertion = invalidEmailScopes == [ ];
message = "Expected exactly one email selected by name for `${user.name}` scopes: ${lib.concatStringsSep ", " invalidEmailScopes}. Personal uses `personal` or `main`; work uses `work`.";
message = "Expected exactly one scoped email for `${user.name}` source-control scopes: ${lib.concatStringsSep ", " invalidEmailScopes}.";
}
];
+31 -29
View File
@@ -11,6 +11,8 @@ in
...
}:
let
repoTheme = metaLib.repo.theme.kanagawa;
palette = repoTheme.palette;
terminal = metaLib.resolveUserTerminal {
inherit pkgs;
user = config.meta.user;
@@ -44,43 +46,43 @@ in
update_check_interval = 0;
};
extraConfig = ''
## name: Kanagawa
## name: ${repoTheme.displayName}
## license: MIT
## author: Tommaso Laurenzi
## upstream: https://github.com/rebelot/kanagawa.nvim/
background #1F1F28
foreground #DCD7BA
selection_background #2D4F67
selection_foreground #C8C093
url_color #72A7BC
cursor #C8C093
background ${palette.background}
foreground ${palette.foreground}
selection_background ${palette.selectionBackground}
selection_foreground ${palette.selectionForeground}
url_color ${palette.url}
cursor ${palette.cursor}
active_tab_background #1F1F28
active_tab_foreground #C8C093
inactive_tab_background #1F1F28
inactive_tab_foreground #727169
active_tab_background ${palette.background}
active_tab_foreground ${palette.selectionForeground}
inactive_tab_background ${palette.background}
inactive_tab_foreground ${palette.muted}
color0 #16161D
color1 #C34043
color2 #76946A
color3 #C0A36E
color4 #7E9CD8
color5 #957FB8
color6 #6A9589
color7 #C8C093
color0 ${palette.terminal.color0}
color1 ${palette.terminal.color1}
color2 ${palette.terminal.color2}
color3 ${palette.terminal.color3}
color4 ${palette.terminal.color4}
color5 ${palette.terminal.color5}
color6 ${palette.terminal.color6}
color7 ${palette.terminal.color7}
color8 #727169
color9 #E82424
color10 #98BB6C
color11 #E6C384
color12 #7FB4CA
color13 #938AA9
color14 #7AA89F
color15 #DCD7BA
color8 ${palette.terminal.color8}
color9 ${palette.terminal.color9}
color10 ${palette.terminal.color10}
color11 ${palette.terminal.color11}
color12 ${palette.terminal.color12}
color13 ${palette.terminal.color13}
color14 ${palette.terminal.color14}
color15 ${palette.terminal.color15}
color16 #FFA066
color17 #FF5D62
color16 ${palette.terminal.color16}
color17 ${palette.terminal.color17}
'';
};
};
+32 -18
View File
@@ -1,44 +1,58 @@
{ config, ... }:
let
metaLib = config.meta.lib;
in
{
flake.modules.homeManager.vicinae =
{ pkgs, inputs, ... }:
{
pkgs,
inputs,
...
}:
let
repoTheme = metaLib.repo.theme.kanagawa;
palette = repoTheme.palette;
in
{
programs.vicinae = {
enable = true;
systemd.enable = true;
themes.kanagawa-wave = {
themes.${repoTheme.name} = {
meta = {
version = 1;
name = "Kanagawa Wave";
name = repoTheme.displayName;
description = "A dark theme inspired by the colors of the famous painting by Katsushika Hokusai.";
variant = "dark";
inherits = "vicinae-dark";
};
colors = {
core = {
background = "#1F1F28";
foreground = "#DCD7BA";
secondary_background = "#16161D";
border = "#2A2A37";
accent = "#7E9CD8";
background = palette.background;
foreground = palette.foreground;
secondary_background = palette.secondaryBackground;
border = palette.border;
accent = palette.accents.blue;
};
accents = {
blue = "#7E9CD8";
green = "#98BB6C";
magenta = "#D27E99";
orange = "#FFA066";
purple = "#957FB8";
red = "#E82424";
yellow = "#E6C384";
cyan = "#7AA89F";
inherit (palette.accents)
blue
cyan
green
magenta
orange
purple
red
yellow
;
};
input.border_focus = "colors.core.accent";
};
};
settings.theme = {
light.name = "kanagawa-wave";
dark.name = "kanagawa-wave";
light.name = repoTheme.name;
dark.name = repoTheme.name;
};
extensions = with inputs.vicinae-extensions.packages.${pkgs.stdenv.hostPlatform.system}; [
-1
View File
@@ -15,7 +15,6 @@ in
nixosModules.networking
nixosModules.niri
nixosModules.printing
nixosModules.qbittorrent-client
nixosModules.sddm
nixosModules.sops-admin-key-file
nixosModules.standard-boot