1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 19:46:05 +01:00

Merge branch 'nix-community:master' into ssh-kex-algo

This commit is contained in:
Oliver Geneser 2025-11-07 09:55:49 +01:00 committed by GitHub
commit a73706f970
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 599 additions and 590 deletions

16
Justfile Normal file
View file

@ -0,0 +1,16 @@
# Common commands
#
# Contributing manual:
# - https://nix-community.github.io/home-manager/#ch-contributing
# List tests matching a pattern `pattern`
list *pattern:
nix run .#tests -- -l {{pattern}}
# Run all tests matching a pattern `pattern`
test *pattern:
nix run .#tests -- {{pattern}}
# Run integration tests
integration_tests:
nix run .#tests -- -t -l

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1761907660,
"narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=",
"lastModified": 1762111121,
"narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15",
"rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4",
"type": "github"
},
"original": {

View file

@ -0,0 +1,14 @@
{ config, pkgs, ... }:
{
time = "2025-11-03T21:56:50+00:00";
condition = config.programs.ghostty.enable && pkgs.stdenv.hostPlatform.isLinux;
message = ''
Ghostty: now enables the user systemd service by default.
Running Ghostty via these systemd units is the recommended way to run
Ghostty. The two most important benefits provided by Ghostty's systemd
integrations are: instantaneous launching and centralized logging.
See https://ghostty.org/docs/linux/systemd for all details
'';
}

View file

@ -0,0 +1,11 @@
{
time = "2025-11-04T13:00:00+00:00";
condition = true;
message = ''
A new module is available: 'programs.opkssh'.
opkssh is a tool which enables ssh to be used with OpenID Connect allowing SSH access to be managed via identities instead of long-lived SSH keys. It does not replace SSH, but instead generates SSH public keys containing PK Tokens and configures sshd to verify them. These PK Tokens contain standard OpenID Connect ID Tokens.
This protocol builds on the OpenPubkey which adds user public keys to OpenID Connect without breaking compatibility with existing OpenID Provider.
'';
}

View file

@ -0,0 +1,9 @@
{
time = "2025-11-06T11:50:53+00:00";
condition = true;
message = ''
A new module is available: 'services.tomat'.
tomat is a Pomodoro timer for status bars and the command line. It is easily configurable and support both desktop and sound notifications.
'';
}

View file

@ -9,175 +9,6 @@ let
cfg = config.systemd.user.tmpfiles;
ruleSubmodule = lib.types.submodule (
{ name, ... }:
{
options.type = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = name;
defaultText = "tmpfiles-type";
example = "d";
description = ''
The type of operation to perform on the file.
The type consists of a single letter and optionally one or more
modifier characters.
Please see the upstream documentation for the available types and
more details: {manpage}`tmpfiles.d(5)`
'';
};
options.mode = lib.mkOption {
type = lib.types.str;
default = "-";
example = "0755";
description = ''
The file access mode to use when creating this file or directory.
'';
};
options.user = lib.mkOption {
type = lib.types.str;
default = "-";
example = "root";
description = ''
The user of the file.
This may either be a numeric ID or a user/group name.
If omitted or when set to `"-"`, the user and group of the user who
invokes systemd-tmpfiles is used.
'';
};
options.group = lib.mkOption {
type = lib.types.str;
default = "-";
example = "root";
description = ''
The group of the file.
This may either be a numeric ID or a user/group name.
If omitted or when set to `"-"`, the user and group of the user who
invokes systemd-tmpfiles is used.
'';
};
options.age = lib.mkOption {
type = lib.types.str;
default = "-";
example = "10d";
description = ''
Delete a file when it reaches a certain age.
If a file or directory is older than the current time minus the age
field, it is deleted.
If set to `"-"`, no automatic clean-up is done.
'';
};
options.argument = lib.mkOption {
type = lib.types.str;
default = "";
example = "";
description = ''
An argument whose meaning depends on the type of operation.
Please see the upstream documentation for the meaning of this
parameter in different situations: {manpage}`tmpfiles.d(5)`
'';
};
}
);
attrsWith' = placeholder: elemType: lib.types.attrsWith { inherit elemType placeholder; };
nonEmptyAttrsWith' =
placeholder: elemType:
let
attrs = lib.types.addCheck (attrsWith' placeholder elemType) (s: s != { });
in
attrs
// {
name = "nonEmptyAttrsOf";
description = "non-empty ${attrs.description}";
emptyValue = { }; # no .value attribute, meaning there is not empty value
substSubModules = m: nonEmptyAttrsWith' placeholder (elemType.substSubModules m);
};
configSubmodule = lib.types.submodule {
options.rules = lib.mkOption {
description = "The rules contained in this configuration.";
example = {
"%C".d = {
mode = "0755";
user = "alice";
group = "alice";
age = "4 weeks";
};
};
type = nonEmptyAttrsWith' "path" (nonEmptyAttrsWith' "tmpfiles-type" ruleSubmodule);
};
options.purgeOnChange = lib.mkOption {
description = ''
Whether the rules that are marked for purging, will automatically
be purged when the set of rules changes.
See {manpage}`systemd-tmpfiles(8)` for details about purging.
'';
type = lib.types.bool;
default = false;
};
};
modulePrefix = [
"systemd"
"user"
"tmpfiles"
"settings"
];
mkFileName = configName: "user-tmpfiles.d/home-manager-${configName}.conf";
mkConfigFile =
name: rules:
{
suffix ? [ name ],
}:
let
escapeArgument = lib.strings.escapeC [
"\t"
"\n"
"\r"
" "
"\\"
];
mkRule = path: rule: ''
'${rule.type}' '${path}' '${rule.mode}' '${rule.user}' '${rule.group}' '${rule.age}' ${escapeArgument rule.argument}
'';
in
{
text = ''
# This file was generated by Home Manager and should not be modified.
# Please change the option '${lib.showAttrPath (modulePrefix ++ suffix)}' instead.
${lib.pipe rules [
(lib.mapAttrs (_path: lib.attrValues))
(lib.mapAttrsToList (path: map (mkRule path)))
lib.flatten
lib.concatStrings
]}
'';
onChange = ''
run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run} '${config.xdg.configHome}/${mkFileName name}'
'';
};
nonPurgedConfigs = lib.filterAttrs (_name: config: !config.purgeOnChange) cfg.settings;
purgedConfigs = lib.filterAttrs (_name: config: config.purgeOnChange) cfg.settings;
# WARNING: When changing this path, the next home-manager generation will
# not find and the rules of the old generation that are subject to purging.
purgedRulesConfigName = "purge-on-change";
in
{
meta.maintainers = with lib.maintainers; [
@ -185,93 +16,42 @@ in
dawidsowa
];
imports = [
(lib.mkRemovedOptionModule [ "systemd" "user" "tmpfiles" "rules" ] ''
It has been replaced by 'systemd.user.tmpfiles.settings'.
'')
];
options.systemd.user.tmpfiles.settings = lib.mkOption {
options.systemd.user.tmpfiles.rules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "L /home/user/Documents - - - - /mnt/data/Documents" ];
description = ''
Declare systemd-tmpfiles rules to create, delete, and clean up volatile
and temporary files and directories.
Even though the service is called `*tmp*files` you can also create
persistent files.
Rules for creating and cleaning up temporary files
automatically. See
{manpage}`tmpfiles.d(5)`
for the exact format.
'';
example = {
cache.rules."%C".d = {
mode = "0755";
user = "alice";
group = "alice";
age = "4 weeks";
};
};
default = { };
type = attrsWith' "config-name" configSubmodule;
};
config = lib.mkMerge [
config = lib.mkIf (cfg.rules != [ ]) {
assertions = [
(lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux)
];
(lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
# The activation script must be enabled unconditionally in order to
# guarantee that the old rules are purged even if the new set of rules
# is empty, i.e. `cfg.rulesToPurgeOnChange == [ ]`.
home.activation.purgeTmpfiles = lib.hm.dag.entryAfter [ "writeBoundary" ] (
let
relativeXdgConfigHome = lib.strings.removePrefix "${config.home.homeDirectory}/" config.xdg.configHome;
configPath = "home-files/${relativeXdgConfigHome}/${purgedRulesConfigName}";
in
''
if [[ -v oldGenPath && -f $oldGenPath/${configPath} ]] &&
diff -q $oldGenPath/${configPath} $newGenPath/${configPath} &>/dev/null; then
verboseEcho "Purge old tmpfiles"
run ${pkgs.systemd}/bin/systemd-tmpfiles --user --purge ''${DRY_RUN:+--dry-run} $oldGenPath/${configPath}
fi
''
);
})
(lib.mkIf (cfg.settings != { }) {
assertions = [
(lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux)
];
warnings = lib.flatten (
lib.mapAttrsToListRecursive (
path: value:
lib.optional
(lib.last path == "argument" && lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' value != null)
''
The '${lib.showAttrPath (modulePrefix ++ path)}' option
appears to contain escape sequences, which will be escaped again.
Unescape them if this is not intended. The assigned string is:
"${value}"
''
) cfg.settings
);
xdg.configFile = {
"systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service";
"systemd/user/systemd-tmpfiles-setup.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service";
"systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer";
"systemd/user/systemd-tmpfiles-clean.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service";
}
// lib.mapAttrs' (
name: config: lib.nameValuePair (mkFileName name) (mkConfigFile name config.rules { })
) nonPurgedConfigs
// lib.optionalAttrs (purgedConfigs != { }) {
${mkFileName purgedRulesConfigName} =
let
purgedConfigsMerged = lib.foldl' lib.recursiveUpdate { } (lib.attrValues purgedConfigs);
in
mkConfigFile purgedRulesConfigName purgedConfigsMerged.rules { suffix = [ ]; };
xdg.configFile = {
"user-tmpfiles.d/home-manager.conf" = {
text = ''
# This file is created automatically and should not be modified.
# Please change the option systemd.user.tmpfiles.rules instead.
${lib.concatStringsSep "\n" cfg.rules}
'';
onChange = ''
run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run}
'';
};
})
];
"systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service";
"systemd/user/systemd-tmpfiles-setup.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service";
"systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer";
"systemd/user/systemd-tmpfiles-clean.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service";
};
};
}

View file

@ -99,194 +99,7 @@ in
};
userSettings = mkOption {
type = types.submodule {
freeformType = tomlFormat.type;
options = {
after-startup-command = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
A list of AeroSpace commands to execute immediately after the AeroSpace application starts.
These commands are written to your `aerospace.toml` config file and are run after the `after-login-command` sequence.
A list of all available commands can be found at <https://nikitabobko.github.io/AeroSpace/commands>.
While this module checks for valid command names, using incorrect *arguments* can still cause issues.
If AeroSpace is not behaving correctly after startup, check the logs for errors with `cat /tmp/aerospace.err.log`.
'';
example = [
"exec-and-forget open -n /System/Applications/Utilities/Terminal.app"
"layout tiles accordion"
];
};
enable-normalization-flatten-containers = mkOption {
type = types.bool;
default = true;
description = ''Containers that have only one child are "flattened".'';
};
enable-normalization-opposite-orientation-for-nested-containers = mkOption {
type = types.bool;
default = true;
description = "Containers that nest into each other must have opposite orientations.";
};
accordion-padding = mkOption {
type = types.int;
default = 30;
description = "Padding between windows in an accordion container.";
};
default-root-container-layout = mkOption {
type = types.enum [
"tiles"
"accordion"
];
default = "tiles";
description = "Default layout for the root container.";
};
default-root-container-orientation = mkOption {
type = types.enum [
"horizontal"
"vertical"
"auto"
];
default = "auto";
description = "Default orientation for the root container.";
};
on-window-detected = mkOption {
type = types.listOf (
types.submodule {
options = {
"if" = mkOption {
type = types.submodule {
options = {
app-id = mkOption {
type = with types; nullOr str;
default = null;
description = "The application ID to match (optional).";
};
workspace = mkOption {
type = with types; nullOr str;
default = null;
description = "The workspace name to match (optional).";
};
window-title-regex-substring = mkOption {
type = with types; nullOr str;
default = null;
description = "Substring to match in the window title (optional).";
};
app-name-regex-substring = mkOption {
type = with types; nullOr str;
default = null;
description = "Regex substring to match the app name (optional).";
};
during-aerospace-startup = mkOption {
type = with types; nullOr bool;
default = null;
description = "Whether to match during aerospace startup (optional).";
};
};
};
default = { };
description = "Conditions for detecting a window.";
};
check-further-callbacks = mkOption {
type = with types; nullOr bool;
default = null;
description = "Whether to check further callbacks after this rule (optional).";
};
run = mkOption {
type =
with types;
oneOf [
str
(listOf str)
];
example = [
"move-node-to-workspace m"
"resize-node"
];
description = "Commands to execute when the conditions match (required).";
};
};
}
);
default = [ ];
example = [
{
"if" = {
app-id = "Another.Cool.App";
workspace = "cool-workspace";
window-title-regex-substring = "Title";
app-name-regex-substring = "CoolApp";
during-aerospace-startup = false;
};
check-further-callbacks = false;
run = [
"move-node-to-workspace m"
"resize-node"
];
}
];
description = "Commands to run every time a new window is detected with optional conditions.";
};
workspace-to-monitor-force-assignment = mkOption {
type =
with types;
nullOr (
attrsOf (oneOf [
int
str
(listOf str)
])
);
default = null;
description = ''
Map workspaces to specific monitors.
Left-hand side is the workspace name, and right-hand side is the monitor pattern.
'';
example = {
"1" = 1; # First monitor from left to right.
"2" = "main"; # Main monitor.
"3" = "secondary"; # Secondary monitor (non-main).
"4" = "built-in"; # Built-in display.
"5" = "^built-in retina display$"; # Regex for the built-in retina display.
"6" = [
"secondary"
"dell"
]; # Match first pattern in the list.
};
};
on-focus-changed = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "move-mouse monitor-lazy-center" ];
description = "Commands to run every time focused window or workspace changes.";
};
on-focused-monitor-changed = mkOption {
type = with types; listOf str;
default = [ "move-mouse monitor-lazy-center" ];
description = "Commands to run every time focused monitor changes.";
};
exec-on-workspace-change = mkOption {
type = with types; listOf str;
default = [ ];
example = [
"/bin/bash"
"-c"
"sketchybar --trigger aerospace_workspace_change FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE"
];
description = "Commands to run every time workspace changes.";
};
key-mapping.preset = mkOption {
type = types.enum [
"qwerty"
"dvorak"
"colemak"
];
default = "qwerty";
description = "Keymapping preset.";
};
};
};
inherit (tomlFormat) type;
default = { };
example = lib.literalExpression ''
{

View file

@ -153,6 +153,17 @@ let
};
};
completionModule = types.submodule {
options = {
body = mkOption {
type = types.lines;
description = ''
The completion file's body.
'';
};
};
};
abbrModule = types.submodule {
options = {
expansion = mkOption {
@ -556,6 +567,28 @@ in
<https://fishshell.com/docs/current/cmds/function.html>.
'';
};
programs.fish.completions = mkOption {
type = with types; attrsOf (either lines completionModule);
default = { };
example = literalExpression ''
{
my-prog = '''
complete -c myprog -s o -l output
''';
my-app = {
body = '''
complete -c myapp -s -v
''';
};
}
'';
description = ''
Custom fish completions. For more information see
<https://fishshell.com/docs/current/completions.html>.
'';
};
};
config = mkIf cfg.enable (
@ -734,6 +767,20 @@ in
};
}) cfg.functions;
}
{
xdg.configFile = lib.mapAttrs' (name: def: {
name = "fish/completions/${name}.fish";
value = {
source =
let
body = if isAttrs def then def.body else def;
in
fishIndent "${name}.fish" ''
${lib.strings.removeSuffix "\n" body}
'';
};
}) cfg.completions;
}
# Each plugin gets a corresponding conf.d/plugin-NAME.fish file to load
# in the paths and any initialization scripts.

View file

@ -114,6 +114,24 @@ in
defaultText = lib.literalMD "`true` if programs.ghostty.package is not null";
};
systemd = lib.mkOption {
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "the Ghostty systemd user service" // {
default = pkgs.stdenv.hostPlatform.isLinux;
defaultText = lib.literalMD "`true` on Linux, `false` otherwise";
};
};
};
default = { };
description = ''
Configuration for Ghostty's systemd integration.
This enables additional speed and features.
See <https://ghostty.org/docs/linux/systemd> for more information.
'';
};
enableBashIntegration = mkShellIntegrationOption (
lib.hm.shell.mkBashIntegrationOption { inherit config; }
);
@ -195,6 +213,22 @@ in
};
})
(lib.mkIf cfg.systemd.enable {
assertions = [
{
assertion = cfg.systemd.enable -> cfg.package != null;
message = "programs.ghostty.systemd.enable cannot be true when programs.ghostty.package is null";
}
{
assertion = cfg.systemd.enable -> pkgs.stdenv.hostPlatform.isLinux;
message = "Ghostty systemd integration cannot be enabled for non-linux platforms";
}
];
xdg.configFile."systemd/user/app-com.mitchellh.ghostty.service".source =
"${cfg.package}/share/systemd/user/app-com.mitchellh.ghostty.service";
dbus.packages = [ cfg.package ];
})
(lib.mkIf cfg.enableBashIntegration {
# Make order 101 to be placed exactly after bash completions, as Ghostty
# documentation suggests sourcing the script as soon as possible

View file

@ -36,12 +36,14 @@ in
# Use `systemd-tmpfiles` since glab requires its configuration file to have
# mode 0600.
systemd.user.tmpfiles.settings.glab = lib.mkIf (cfg.settings != { }) {
rules."${config.xdg.configHome}/glab-cli/config.yml" = {
"C+$".argument = "${yaml.generate "glab-config" cfg.settings}";
z.mode = "0600";
};
};
systemd.user.tmpfiles.rules =
let
target = "${config.xdg.configHome}/glab-cli/config.yml";
in
lib.mkIf (cfg.settings != { }) [
"C+ ${target} - - - - ${yaml.generate "glab-config" cfg.settings}"
"z ${target} 0600"
];
xdg.configFile."glab-cli/aliases.yml" = lib.mkIf (cfg.aliases != { }) {
source = yaml.generate "glab-aliases" cfg.aliases;

View file

@ -103,13 +103,11 @@ in
'';
fishIntegration = ''
function ${cfg.shellWrapperName}
set -x LAZYGIT_NEW_DIR_FILE ${lazygitNewDirFilePath}
command lazygit $argv
if test -f $LAZYGIT_NEW_DIR_FILE
cd (cat $LAZYGIT_NEW_DIR_FILE)
rm -f $LAZYGIT_NEW_DIR_FILE
end
set -x LAZYGIT_NEW_DIR_FILE ${lazygitNewDirFilePath}
command lazygit $argv
if test -f $LAZYGIT_NEW_DIR_FILE
cd (cat $LAZYGIT_NEW_DIR_FILE)
rm -f $LAZYGIT_NEW_DIR_FILE
end
'';

View file

@ -0,0 +1,59 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.programs.opkssh;
yamlFormat = pkgs.formats.yaml { };
in
{
meta.maintainers = [ lib.maintainers.swarsel ];
options.programs.opkssh = {
enable = lib.mkEnableOption "enable the OpenPubkey SSH client";
package = lib.mkPackageOption pkgs "opkssh" { nullable = true; };
settings = lib.mkOption {
inherit (yamlFormat) type;
default = { };
example = lib.literalExpression ''
{
default_provider = "kanidm";
providers = [
{
alias = "kanidm";
issuer = "https://idm.example.com/oauth2/openid/opkssh";
client_id = "opkssh";
scopes = "openid email profile";
redirect_uris = [
"http://localhost:3000/login-callback"
"http://localhost:10001/login-callback"
"http://localhost:11110/login-callback"
];
};
];
}
'';
description = ''
Configuration written to {file}`$HOME/.opk/config.yml`.
See <https://github.com/openpubkey/opkssh/blob/main/docs/config.md#client-config-opkconfigyml>.
'';
};
};
config = lib.mkIf cfg.enable {
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
home.file."${config.home.homeDirectory}/.opk/config.yml" = lib.mkIf (cfg.settings != { }) {
source = yamlFormat.generate "opkssh-config-${config.home.username}.yml" cfg.settings;
};
};
}

View file

@ -7,7 +7,10 @@
let
cfg = config.programs.superfile;
tomlFormat = pkgs.formats.toml { };
jsonFormat = pkgs.formats.json { };
inherit (pkgs.stdenv.hostPlatform) isDarwin;
inherit (lib)
literalExpression
@ -23,6 +26,29 @@ let
types
hm
;
pinnedFolderModule = types.submodule {
freeformType = jsonFormat.type;
options = {
name = mkOption {
type = types.nullOr types.str;
default = null;
example = "Nix Store";
description = ''
Name that will be shown.
'';
};
location = mkOption {
type = types.path;
example = "/nix/store";
description = ''
Location of the pinned entry.
'';
};
};
};
in
{
meta.maintainers = [ hm.maintainers.LucasWagler ];
@ -106,11 +132,38 @@ in
};
'';
};
firstUseCheck = mkOption {
type = types.bool;
default = true;
description = ''
Enables the first time use popup.
'';
};
pinnedFolders = mkOption {
type = types.listOf pinnedFolderModule;
default = [ ];
example = literalExpression ''
[
{
name = "Nix Store";
location = "/nix/store";
}
];
'';
description = ''
Entries that get added to the pinned panel.
'';
};
};
config =
let
enableXdgConfig = !isDarwin || config.xdg.enable;
baseConfigPath = if enableXdgConfig then "superfile" else "Library/Application Support/superfile";
baseDataPath = if enableXdgConfig then "superfile" else "Library/Application Support/superfile";
themeSetting =
if (!(cfg.settings ? theme) && cfg.themes != { }) then
{
@ -118,7 +171,6 @@ in
}
else
{ };
baseConfigPath = if enableXdgConfig then "superfile" else "Library/Application Support/superfile";
configFile = mkIf (cfg.settings != { }) {
"${baseConfigPath}/config.toml".source = tomlFormat.generate "superfile-config.toml" (
recursiveUpdate themeSetting cfg.settings
@ -139,24 +191,48 @@ in
(tomlFormat.generate "superfile-theme-${name}.toml" value);
}
) cfg.themes;
firstUseCheckFile = mkIf (!cfg.firstUseCheck) { "${baseDataPath}/firstUseCheck".text = ""; };
pinnedFile = mkIf (cfg.pinnedFolders != [ ]) {
"${baseDataPath}/pinned.json".source = jsonFormat.generate "pinned.json" cfg.pinnedFolders;
};
files = mkMerge [
configFile
hotkeysFile
themeFiles
firstUseCheckFile
pinnedFile
];
configFiles = mkMerge [
configFile
hotkeysFile
themeFiles
];
dataFiles = mkMerge [
firstUseCheckFile
pinnedFile
];
in
mkIf cfg.enable {
home.packages = mkIf (cfg.package != null) (
[ cfg.package ]
++ optional (
cfg.metadataPackage != null && cfg.settings ? metadata && cfg.settings.metadata
) cfg.metadataPackage
++ optional (
cfg.zoxidePackage != null && cfg.settings ? zoxide_support && cfg.settings.zoxide_support
) cfg.zoxidePackage
);
home = {
packages = mkIf (cfg.package != null) (
[ cfg.package ]
++ optional (
cfg.metadataPackage != null && cfg.settings ? metadata && cfg.settings.metadata
) cfg.metadataPackage
++ optional (
cfg.zoxidePackage != null && cfg.settings ? zoxide_support && cfg.settings.zoxide_support
) cfg.zoxidePackage
);
xdg.configFile = mkIf enableXdgConfig configFiles;
home.file = mkIf (!enableXdgConfig) configFiles;
file = mkIf (!enableXdgConfig) files;
};
xdg = {
configFile = mkIf enableXdgConfig configFiles;
dataFile = mkIf enableXdgConfig dataFiles;
};
};
}

View file

@ -224,7 +224,6 @@ in
Documentation = [ "https://docs.vicinae.com" ];
After = [ cfg.systemd.target ];
PartOf = [ cfg.systemd.target ];
BindsTo = [ cfg.systemd.target ];
};
Service = {
EnvironmentFile = pkgs.writeText "vicinae-env" ''

View file

@ -459,7 +459,7 @@ in
existing_profiles=$(jq '.userDataProfiles // [] | map({ (.name): .location }) | add // {}' "$file")
for profile in "''${profiles[@]}"; do
if [[ "$(echo $existing_profiles | jq --arg profile $profile 'has ($profile)')" != "true" ]] || [[ "$(echo $existing_profiles | jq --arg profile $profile 'has ($profile)')" == "true" && "$(echo $existing_profiles | jq --arg profile $profile '.[$profile]')" != "\"$profile\"" ]]; then
if [[ "$(echo $existing_profiles | jq --arg profile "$profile" 'has ($profile)')" != "true" ]] || [[ "$(echo $existing_profiles | jq --arg profile "$profile" 'has ($profile)')" == "true" && "$(echo $existing_profiles | jq --arg profile "$profile" '.[$profile]')" != "\"$profile\"" ]]; then
file_write="$file_write$([ "$file_write" != "" ] && echo "...")$profile"
fi
done

View file

@ -0,0 +1,74 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.tomat;
tomlFormat = pkgs.formats.toml { };
in
{
meta.maintainers = with lib.maintainers; [ jolars ];
options.services.tomat = {
enable = lib.mkEnableOption "Tomat Pomodoro server";
package = lib.mkPackageOption pkgs "tomat" { };
settings = lib.mkOption {
type = lib.types.submodule { freeformType = tomlFormat.type; };
default = { };
example = {
timer = {
work = 25;
break = 5;
auto_advance = false;
};
sound = {
enabled = true;
};
notification = {
enabled = true;
};
};
description = ''
Tomat configuration.
See <https://github.com/jolars/tomat/blob/main/docs/configuration.md> for supported values.
'';
};
};
config = lib.mkIf cfg.enable {
home.packages = [ cfg.package ];
xdg.configFile = {
"tomat/config.toml" = lib.mkIf (cfg.settings != { }) {
source = tomlFormat.generate "tomat-config.toml" cfg.settings;
};
};
systemd.user.services.tomat = {
Unit = {
Description = "Tomat Pomodoro server";
After = [ "graphical.target" ];
};
Service = {
ExecStart = "${lib.getExe cfg.package} daemon run";
Restart = "always";
RestartSec = 5;
};
Install = {
WantedBy = [ "default.target" ];
};
};
};
}

View file

@ -203,7 +203,6 @@ import nmtSrc {
./modules/misc/numlock
./modules/misc/pam
./modules/misc/qt
./modules/misc/tmpfiles
./modules/misc/xdg/linux.nix
./modules/misc/xsession
./modules/systemd

View file

@ -28,12 +28,9 @@ in
uid = 1000;
};
systemd.tmpfiles.settings.age.rules."/home/alice/age-key".f = {
mode = "400";
user = "alice";
group = "users";
argument = ageKey;
};
systemd.tmpfiles.rules = [
"f /home/alice/age-key 400 alice users - ${ageKey}"
];
home-manager.users.alice =
{ config, ... }:

View file

@ -1,26 +0,0 @@
{
imports = [ ./common-stubs.nix ];
systemd.user.tmpfiles.settings = {
cache.rules."%C".d.age = "4 weeks";
myTool.rules."%h/.config/myTool.conf"."f+" = {
mode = "0644";
user = "alice";
group = "users";
argument = "my unescaped config";
};
};
nmt.script = ''
assertPathNotExists home-files/.config/user-tmpfiles.d/home-manager-purge-on-change.conf
cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf
assertFileExists $cacheRulesFile
assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $"
myToolRulesFile=home-files/.config/user-tmpfiles.d/home-manager-myTool.conf
assertFileExists $myToolRulesFile
assertFileRegex $myToolRulesFile \
"^'f+' '%h/.config/myTool.conf' '0644' 'alice' 'users' '-' my\\\\x20unescaped\\\\x20config$"
'';
}

View file

@ -1,3 +0,0 @@
{
test.stubs.systemd.outPath = null;
}

View file

@ -1,7 +0,0 @@
{
tmpfiles-no-rules = ./no-rules.nix;
tmpfiles-basic-rules = ./basic-rules.nix;
tmpfiles-rules-with-purging = ./rules-with-purging.nix;
tmpfiles-escaped-argument-warning = ./escaped-argument-warning.nix;
}

View file

@ -1,14 +0,0 @@
{
imports = [ ./common-stubs.nix ];
systemd.user.tmpfiles.settings.foo.rules.path.f.argument = "my\\x20unescaped\\x20config";
test.asserts.warnings.expected = [
''
The 'systemd.user.tmpfiles.settings.foo.rules.path.f.argument' option
appears to contain escape sequences, which will be escaped again.
Unescape them if this is not intended. The assigned string is:
"my\x20unescaped\x20config"
''
];
}

View file

@ -1,9 +0,0 @@
{
imports = [ ./common-stubs.nix ];
systemd.user.tmpfiles.settings = { };
nmt.script = ''
assertPathNotExists home-files/.config/user-tmpfiles.d/
'';
}

View file

@ -1,28 +0,0 @@
{
imports = [ ./common-stubs.nix ];
systemd.user.tmpfiles.settings = {
cache.rules."%C".d.age = "4 weeks";
myTool = {
rules = {
"%h/.config/myTool.conf"."f+".argument = "my_config";
"%h/.config/myToolPurged.conf"."f+$".argument = "my_config_purged";
};
purgeOnChange = true;
};
};
nmt.script = ''
cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf
assertFileExists $cacheRulesFile
assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $"
assertPathNotExists home-files/.config/user-tmpfiles.d/home-manager-myTool.conf
myToolRulesFile=home-files/.config/user-tmpfiles.d/home-manager-purge-on-change.conf
assertFileExists $myToolRulesFile
assertFileRegex $myToolRulesFile \
"^'f+' '%h/.config/myTool.conf' '-' '-' '-' '-' my_config$"
assertFileRegex $myToolRulesFile \
"^'f+$' '%h/.config/myToolPurged.conf' '-' '-' '-' '-' my_config_purged$"
'';
}

View file

@ -1,14 +1,3 @@
accordion-padding = 30
after-startup-command = []
default-root-container-layout = "tiles"
default-root-container-orientation = "auto"
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true
exec-on-workspace-change = []
on-focus-changed = []
on-focused-monitor-changed = ["move-mouse monitor-lazy-center"]
on-window-detected = []
[gaps.outer]
bottom = 8
left = 8

View file

@ -1,14 +1,4 @@
accordion-padding = 30
after-login-command = []
after-startup-command = []
default-root-container-layout = "tiles"
default-root-container-orientation = "auto"
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true
exec-on-workspace-change = []
on-focus-changed = []
on-focused-monitor-changed = ["move-mouse monitor-lazy-center"]
on-window-detected = []
start-at-login = false
[gaps.outer]
@ -17,9 +7,6 @@ left = 8
right = 8
top = 8
[key-mapping]
preset = "qwerty"
[mode.main.binding]
alt-h = "focus left"
alt-j = "focus down"

View file

@ -0,0 +1,45 @@
{ lib, pkgs, ... }:
let
myProg = pkgs.writeText "my-prog.fish" ''
complete -c myprog -s o -l output
'';
myApp = pkgs.writeText "my-app.fish" ''
complete -c myapp -s -v
'';
in
{
config = {
programs.fish = {
enable = true;
completions = {
my-prog = ''
complete -c myprog -s o -l output
'';
my-app = {
body = ''
complete -c myapp -s -v
'';
};
};
};
xdg.dataFile."fish/home-manager_generated_completions".source = lib.mkForce (
builtins.toFile "empty" ""
);
nmt = {
description = "if fish.completions is set, check file exists and contents match";
script = ''
assertFileExists home-files/.config/fish/completions/my-prog.fish
echo ${myProg}
assertFileContent home-files/.config/fish/completions/my-prog.fish ${myProg}
assertFileExists home-files/.config/fish/completions/my-app.fish
echo ${myApp}
assertFileContent home-files/.config/fish/completions/my-app.fish ${myApp}
'';
};
};
}

View file

@ -2,6 +2,7 @@
fish-abbrs = ./abbrs.nix;
fish-format-scripts = ./format-scripts.nix;
fish-functions = ./functions.nix;
fish-completions = ./completions.nix;
fish-no-functions = ./no-functions.nix;
fish-plugins = ./plugins.nix;
fish-manpage = ./manpage.nix;

View file

@ -1,5 +1,9 @@
{ config, ... }:
{
programs.ghostty.enable = true;
programs.ghostty = {
enable = true;
package = config.lib.test.mkStubPackage { outPath = null; };
};
nmt.script = ''
assertPathNotExists home-files/.config/ghostty/config

View file

@ -2,7 +2,7 @@
{
programs.ghostty = {
enable = true;
package = config.lib.test.mkStubPackage { };
package = config.lib.test.mkStubPackage { outPath = null; };
settings = {
theme = "catppuccin-mocha";

View file

@ -1,6 +1,8 @@
{ config, ... }:
{
programs.ghostty = {
enable = true;
package = config.lib.test.mkStubPackage { outPath = null; };
themes = {
catppuccin-mocha = {

View file

@ -0,0 +1,3 @@
{
lazygit-fish-integration-enabled = ./fish-integration-enabled.nix;
}

View file

@ -0,0 +1,18 @@
{ config, ... }:
{
programs.fish.enable = true;
home.preferXdgDirectories = false;
programs.lazygit = {
enable = true;
shellWrapperName = "lg";
enableFishIntegration = true;
};
nmt.script = ''
assertFileContent home-files/.config/fish/functions/${config.programs.lazygit.shellWrapperName}.fish \
${./fish-integration-expected.fish}
'';
}

View file

@ -0,0 +1,8 @@
function lg
set -x LAZYGIT_NEW_DIR_FILE ~/.lazygit/newdir
command lazygit $argv
if test -f $LAZYGIT_NEW_DIR_FILE
cd (cat $LAZYGIT_NEW_DIR_FILE)
rm -f $LAZYGIT_NEW_DIR_FILE
end
end

View file

@ -0,0 +1,3 @@
{
opkssh-basic-config = ./opkssh-basic-config.nix;
}

View file

@ -0,0 +1,42 @@
_: {
programs.opkssh = {
enable = true;
settings = {
default_provider = "test-provider";
providers = [
{
alias = "test-provider";
issuer = "https://test.domain/oauth2/openid/opkssh";
client_id = "opkssh";
scopes = "openid email profile";
redirect_uris = [
"http://localhost:3000/login-callback"
"http://localhost:10001/login-callback"
"http://localhost:11110/login-callback"
];
}
];
};
};
nmt.script = ''
configFile=home-files/.opk/config.yml
assertFileExists "$configFile"
configFileNormalized="$(normalizeStorePaths "$configFile")"
assertFileContent "$configFileNormalized" ${builtins.toFile "expected.service" ''
default_provider: test-provider
providers:
- alias: test-provider
client_id: opkssh
issuer: https://test.domain/oauth2/openid/opkssh
redirect_uris:
- http://localhost:3000/login-callback
- http://localhost:10001/login-callback
- http://localhost:11110/login-callback
scopes: openid email profile
''}
'';
}

View file

@ -0,0 +1,6 @@
[
{
"location": "/nix/store",
"name": "Nix Store"
}
]

View file

@ -53,6 +53,13 @@
];
};
};
firstUseCheck = false;
pinnedFolders = [
{
name = "Nix Store";
location = "/nix/store";
}
];
};
nmt.script =
@ -60,6 +67,10 @@
configSubPath =
if !pkgs.stdenv.isDarwin then ".config/superfile" else "Library/Application Support/superfile";
configBasePath = "home-files/" + configSubPath;
dataSubPath =
if !pkgs.stdenv.isDarwin then ".local/share/superfile" else "Library/Application Support/superfile";
dataBasePath = "home-files/" + dataSubPath;
in
''
assertFileExists "${configBasePath}/config.toml"
@ -82,5 +93,10 @@
assertFileContent \
"${configBasePath}/theme/test2.toml" \
${./example-theme2-expected.toml}
assertFileExists "${dataBasePath}/firstUseCheck"
assertFileExists "${dataBasePath}/pinned.json"
assertFileContent \
"${dataBasePath}/pinned.json" \
${./example-pinned-folders.json}
'';
}

View file

@ -0,0 +1,5 @@
{ lib, pkgs, ... }:
lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
tomat-service = ./tomat.nix;
}

View file

@ -0,0 +1,3 @@
[timer]
break = 10
work = 60

View file

@ -0,0 +1,11 @@
[Install]
WantedBy=default.target
[Service]
ExecStart=@tomat@/bin/tomat daemon run
Restart=always
RestartSec=5
[Unit]
After=graphical.target
Description=Tomat Pomodoro server

View file

@ -0,0 +1,25 @@
{
services.tomat = {
enable = true;
settings = {
timer = {
break = 10;
work = 60;
};
};
};
nmt.script =
let
serviceFile = "home-files/.config/systemd/user/tomat.service";
configFile = "home-files/.config/tomat/config.toml";
in
''
assertFileExists "${serviceFile}"
assertFileExists "${configFile}"
assertFileContent "${serviceFile}" ${./expected.service}
assertFileContent "${configFile}" ${./expected-config.toml}
'';
}