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

Compare commits

...

41 commits

Author SHA1 Message Date
home-manager-ci[bot]
d6ea5063ae flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15?narHash=sha256-kJ8lIZsiPOmbkJypG%2BB5sReDXSD1KGu2VEPNqhRa/ew%3D' (2025-10-31)
  → 'github:NixOS/nixpkgs/b3d51a0365f6695e7dd5cdf3e180604530ed33b4?narHash=sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw%3D' (2025-11-02)
2025-11-05 04:11:21 +00:00
Martijn Boers
34fe48801d ghostty: Add systemd integration
The systemd unit already exists on the system, this gives the
option to enable it.
2025-11-04 16:56:11 -06:00
heyzec
1c75dd7022 vscode: don't break when profile name has spaces 2025-11-04 14:07:04 -06:00
Ryan Horiguchi
0a5a165aca superfile: add pinnded folder and first use option 2025-11-04 14:06:39 -06:00
Leon Schwarzäugl
c39c07bf31 opkssh: init module 2025-11-04 08:34:45 -06:00
Austin Horstman
aa6936bb63 tests/yazi: fix bash/zsh integration tests
Didn't fail even with incorrect assertion. Multi line string for
assertFileContains didn't properly work. Don't want to manage an entire
zsh config file in assertFileContent so just multi step asserting the
generated file.

Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-03 21:54:12 -06:00
meck
65bf99c579 yazi: update wrappers not to use cat in subshell
If cat is aliased to bat the non piping operation
in the script might include extra text, use builtins
instead
2025-11-03 21:54:12 -06:00
Austin Horstman
6feb368511 news: add vicinae entry
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-03 21:18:48 -06:00
leiserfg
5cdf9ef995 vicinae service: init
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-03 21:18:48 -06:00
Thierry Delafontaine
1342b821db news: add entry for mcp module and integrations
Add news entry documenting the new `programs.mcp` module and MCP
integration support in OpenCode and VSCode modules.
2025-11-03 20:25:16 -06:00
Thierry Delafontaine
9ff9a94fd4 vscode: add mcp module integration 2025-11-03 20:25:16 -06:00
Thierry Delafontaine
c740351870 opencode: add mcp module integration 2025-11-03 20:25:16 -06:00
Thierry Delafontaine
083b20c1a0 mcp: init module 2025-11-03 20:25:16 -06:00
Pierre-Yves Landuré
1f34c2c855 zed-editor: options to generate debug.json
Add `mutableUserDebug` and `userDebug` options to generate `debug.json` file.
The options are heavily inspired by `mutableUserTasks` and `userTasks` options implementation.

Closes #8091
2025-11-03 20:24:19 -06:00
aldur
95d65dddae gpg: fix correctly setting trust for all keys
When passing `gpg.publicKeys` a `source` including _multiple_ keys,
only the first one in `source` will have `trust` set correctly.

This commit fixes the issue and adds a corresponding test
(failing without the patch, fixed with it).
2025-11-03 15:14:46 -06:00
Hoang Nguyen
a5fee07792 rio: reformat 2025-11-03 09:23:19 -06:00
Hoang Nguyen
9d6e28fd32 rio: add support for custom themes 2025-11-03 09:23:19 -06:00
Hoang Nguyen
e22fb25cde rio: use stub package for all test cases 2025-11-03 09:23:19 -06:00
bruce oberg
3c16ac3646 home manager: add test for option subcommand
- added a test to the standalone-basics unit:  the option subcommand queries the `home.username` value and ensures that something sane is returned.

details:

- the option subcommand was broken by upstream changes to the `nixos-option` command.
- hm had no tests for the option subcommand, so the problem was discovered by users.
- output from nixos-option probably starts with 'Value:\n  "alice"', but we're only looking for 'alice' because the output format is not guaranteed.
2025-11-03 09:12:22 -06:00
bruce oberg
64c49b1aa5 home-manager: fix option subcommand
- calls nix-instatiate instead of nixos-option (using nix-option's underlying nix script).
- loops over options to display since nixos-option can only process a single option.
- passes through the --recursive flag from nixos-option. and includes --help and man page for the flag.

details:

nixos-option was changed from a C++ command to a shell script that feeds a nix script (with arguments) to nix-instatiate. in the process, the --config_expr and --options_expr we once passed to nixos-option were removed.

without changing the nixos-option shell script, we have no may to override the arguments to the nixos-option nix script.

luckily, we can use our modulesExpr as a direct argument to the new nixos-option nix script.

unluckily, the nix script does not accept multiple options per instantiation. so we are also looping through the given options ourselves and feeding them each to nixos-option's nix script.

the nixos-option shell and nix scripts are in different places in the nix store, so we have to search the store for the nix script given the location of the shell script.

also, the nixos-option nix script wants a 'recursive' flag, so we now honor that flag for the home-manager option subcommand.
2025-11-03 09:12:22 -06:00
Benedikt Rips
c93684cd87 tmpfiles: use correct path in the onChange hook 2025-11-03 08:52:18 -06:00
Benedikt Rips
8c824254b1 glab: coerce glab tmpfile rule argument to string 2025-11-03 07:59:26 -06:00
Benedikt Rips
2318e30ea1 tests: hostPlatform -> stdenv.hostPlatform 2025-11-03 07:59:03 -06:00
Benedikt Rips
b5ed4afc22 glab: remove the config file if it is empty or glab disabled 2025-11-02 23:02:10 -06:00
Benedikt Rips
7503ffb0b0 tmpfiles: add maintainer bmrips 2025-11-02 23:02:10 -06:00
Benedikt Rips
b4350d54c2 tmpfiles: add option to purge rules' targets on change 2025-11-02 23:02:10 -06:00
Benedikt Rips
090aa14e5d tmpfiles: migrate to an RFC42-style option 2025-11-02 23:02:10 -06:00
Austin Horstman
32a671dce5 tests/gtk: ubuntu_font_family -> ubuntu-classic
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-02 22:59:59 -06:00
Austin Horstman
ab0d3db1aa tests/darkman: python -> python2
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-02 22:59:59 -06:00
Austin Horstman
9901bb6afc taskwarrior-sync: taskwarrior -> taskwarrior2
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-02 22:59:59 -06:00
Austin Horstman
9f3a82bfd1 taskwarrior: taskwarrior -> taskwarrior2
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-02 22:59:59 -06:00
Austin Horstman
acf7743c89 darwinScrublist: taskwarrior rename
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-02 22:59:59 -06:00
home-manager-ci[bot]
983cbdc75c flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce?narHash=sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c%3D' (2025-10-25)
  → 'github:NixOS/nixpkgs/2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15?narHash=sha256-kJ8lIZsiPOmbkJypG%2BB5sReDXSD1KGu2VEPNqhRa/ew%3D' (2025-10-31)
2025-11-02 22:59:59 -06:00
Austin Horstman
d9cd40d2da
local-ai: string -> str (#8116)
Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-11-03 04:41:08 +00:00
Ilya Savitsky
61f2cc5908
local-ai: init module (#6718) 2025-11-02 22:04:44 -06:00
Alex Ionescu
50a5766d51
kitty: add option mouseBindings (#8111) 2025-11-02 21:55:45 -06:00
Raul Rodrigues de Oliveira
0fe68257a9
fish: added repaint to binds (#8113) 2025-11-02 21:53:28 -06:00
Robert Radestock
371608e69c
rclone: add option to set log-level (#8105)
Add an option to set rclone's log-level per mount:
programs.rclone.remotes.<name>.mounts.<name>.logLevel = "DEBUG";
If no value is set, it'll use rclone's implicit default ("NOTICE")

Previously, the debug log-level got enforced (via "-vv"),
which caused noisy logs, and there was no easy way to change that.

Note: rclone global-flags can't be configured in the config file,
so this uses the environment variable approach.

references:
- https://rclone.org/docs/#logging
- https://rclone.org/docs/#v-vv-verbose

If no value is given, use the implicit default of rclone instead of redefining it through the options
2025-11-02 21:52:56 -06:00
Yechiel Worenklein
43e205606a cbatticon: add package example
After batticonplus was merged into nixpkgs
2025-11-02 06:44:15 -06:00
pancho horrillo
5eaa0072ff gpg-agent: restore empty newlines after commands 2025-11-02 05:52:54 -06:00
pancho horrillo
363797f8a9 gpg-agent: fix syntax-breaking extraneous new line 2025-11-02 05:52:54 -06:00
91 changed files with 2360 additions and 152 deletions

View file

@ -24,7 +24,7 @@
.Cm | generations .Cm | generations
.Cm | help .Cm | help
.Cm | news .Cm | news
.Cm | option Ar option.name .Cm | option Oo Fl -recursive Oc Ar option.name
.Cm | packages .Cm | packages
.Cm | remove-generations Ar ID \&... .Cm | remove-generations Ar ID \&...
.Cm | switch .Cm | switch
@ -138,10 +138,14 @@ Show news entries in a pager.
.RE .RE
.PP .PP
.It Cm option Ar option.name .It Cm option Oo Fl -recursive Oc Ar option.name
.RS 4 .RS 4
Inspect the given option name in the home configuration, like Inspect the given option name in the home configuration, like
\fBnixos-option\fR(8)\&. \fBnixos-option\fR(8)\&.
.sp
If the
.Fl -recursive
option is given, print all the values at or below the option name recursively\&.
.RE .RE
.Pp .Pp

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1761373498, "lastModified": 1762111121,
"narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", "narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -232,7 +232,29 @@ function doInspectOption() {
fi fi
setConfigFile setConfigFile
local extraArgs=("$@") local paths=()
local recursive=false
while (( $# > 0 )); do
local opt="$1"
shift
case $opt in
--recursive)
recursive=true;;
*)
# Remove trailing dot if exists, match the behavior of
# old nixos-option and make shell completions happy
paths+=("${opt%.}")
;;
esac
done
if [[ ${#paths[@]} -eq 0 ]]; then
paths=("")
fi
local extraArgs=()
for p in "${EXTRA_NIX_PATH[@]}"; do for p in "${EXTRA_NIX_PATH[@]}"; do
extraArgs=("${extraArgs[@]}" "-I" "$p") extraArgs=("${extraArgs[@]}" "-I" "$p")
@ -256,11 +278,24 @@ function doInspectOption() {
modulesExpr+=" configuration = if confAttr == \"\" then confPath else (import confPath).\${confAttr};" modulesExpr+=" configuration = if confAttr == \"\" then confPath else (import confPath).\${confAttr};"
modulesExpr+=" pkgs = import <nixpkgs> {}; check = true; })" modulesExpr+=" pkgs = import <nixpkgs> {}; check = true; })"
nixos-option \ local NIXOS_OPTION_CMD NIXOS_OPTION_REAL NIXOS_OPTION_DIR NIXOS_OPTION_NIX
--options_expr "$modulesExpr.options" \ NIXOS_OPTION_CMD=$(command -v nixos-option)
--config_expr "$modulesExpr.config" \ NIXOS_OPTION_REAL=$(realpath "${NIXOS_OPTION_CMD}")
"${extraArgs[@]}" \ NIXOS_OPTION_NIX=$(nix-store -q --references "${NIXOS_OPTION_REAL}" | grep 'nixos-option\.nix$')
"${PASSTHROUGH_OPTS[@]}"
if [[ ! -f "$NIXOS_OPTION_NIX" ]]; then
_iError "nixos-option.nix not found."
exit 1
fi
for path in "${paths[@]}"; do
nix-instantiate --eval --json \
--arg nixos "$modulesExpr" \
--argstr path "$path" \
--arg recursive "$recursive" \
"$NIXOS_OPTION_NIX" \
| jq -r
done
} }
function doInit() { function doInit() {
@ -1063,9 +1098,11 @@ function doHelp() {
echo echo
echo " edit Open the home configuration in \$VISUAL or \$EDITOR" echo " edit Open the home configuration in \$VISUAL or \$EDITOR"
echo echo
echo " option OPTION.NAME" echo " option [--recursive] OPTION.NAME"
echo " Inspect configuration option named OPTION.NAME." echo " Inspect configuration option named OPTION.NAME."
echo echo
echo " --recursive Print all the values at or below the option name recursively."
echo
echo " build Build configuration into result directory" echo " build Build configuration into result directory"
echo echo
echo " init [--switch] [DIR]" echo " init [--switch] [DIR]"
@ -1211,6 +1248,16 @@ while [[ $# -gt 0 ]]; do
;; ;;
esac esac
;; ;;
--recursive)
case $COMMAND in
option)
COMMAND_ARGS+=("$opt")
;;
*)
errTopLevelSubcommandOpt "--recursive" "option"
;;
esac
;;
--option|--arg|--argstr) --option|--arg|--argstr)
[[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt" [[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt"
[[ -v 2 ]] || errMissingOptArg "$opt $1" [[ -v 2 ]] || errMissingOptArg "$opt $1"

View file

@ -0,0 +1,9 @@
{
time = "2025-10-31T12:00:00+00:00";
condition = true;
message = ''
services.local-ai: new module
Added LocalAI, a free, Open Source OpenAI alternative.
'';
}

View file

@ -0,0 +1,25 @@
{
time = "2025-11-03T10:18:15+00:00";
condition = true;
message = ''
A new module 'programs.mcp' is now available for managing Model
Context Protocol (MCP) server configurations.
The 'programs.mcp.servers' option allows you to define MCP servers
in a central location. These configurations can be automatically
integrated into applications that support MCP.
Two modules now support MCP integration:
- 'programs.opencode.enableMcpIntegration': Integrates MCP servers
into OpenCode's configuration.
- 'programs.vscode.profiles.<name>.enableMcpIntegration': Integrates
MCP servers into VSCode profiles.
When integration is enabled, servers from 'programs.mcp.servers' are
merged with application-specific MCP settings, with the latter taking
precedence. This allows you to define MCP servers once and reuse them
across multiple applications.
'';
}

View file

@ -0,0 +1,20 @@
{ pkgs, ... }:
{
time = "2025-11-04T02:33:15+00:00";
condition = pkgs.stdenv.hostPlatform.isLinux;
message = ''
A new program is available: 'programs.vicinae'.
Vicinae is a modern application launcher daemon for Linux with support for
extensions, custom themes, and layer shell integration.
The module provides:
- Systemd service integration with automatic start support
- Extension management with helpers for Vicinae and Raycast extensions
- Theme configuration support
- Declarative settings via 'programs.vicinae.settings'
- Layer shell integration for Wayland compositors
See the module options for more details on configuration.
'';
}

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

@ -9,38 +9,249 @@ let
cfg = config.systemd.user.tmpfiles; 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 in
{ {
meta.maintainers = [ lib.maintainers.dawidsowa ]; text = ''
# This file was generated by Home Manager and should not be modified.
options.systemd.user.tmpfiles.rules = lib.mkOption { # Please change the option '${lib.showAttrPath (modulePrefix ++ suffix)}' instead.
type = lib.types.listOf lib.types.str; ${lib.pipe rules [
default = [ ]; (lib.mapAttrs (_path: lib.attrValues))
example = [ "L /home/user/Documents - - - - /mnt/data/Documents" ]; (lib.mapAttrsToList (path: map (mkRule path)))
description = '' lib.flatten
Rules for creating and cleaning up temporary files lib.concatStrings
automatically. See ]}
{manpage}`tmpfiles.d(5)` '';
for the exact format. onChange = ''
run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run} '${config.xdg.configHome}/${mkFileName name}'
''; '';
}; };
config = lib.mkIf (cfg.rules != [ ]) { 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; [
bmrips
dawidsowa
];
imports = [
(lib.mkRemovedOptionModule [ "systemd" "user" "tmpfiles" "rules" ] ''
It has been replaced by 'systemd.user.tmpfiles.settings'.
'')
];
options.systemd.user.tmpfiles.settings = lib.mkOption {
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.
'';
example = {
cache.rules."%C".d = {
mode = "0755";
user = "alice";
group = "alice";
age = "4 weeks";
};
};
default = { };
type = attrsWith' "config-name" configSubmodule;
};
config = lib.mkMerge [
(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 = [ assertions = [
(lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux) (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 = { 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 = "systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service";
"systemd/user/systemd-tmpfiles-setup.service".source = "systemd/user/systemd-tmpfiles-setup.service".source =
@ -49,6 +260,18 @@ in
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer"; "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer";
"systemd/user/systemd-tmpfiles-clean.service".source = "systemd/user/systemd-tmpfiles-clean.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service"; "${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 = [ ]; };
};
})
];
} }

View file

@ -261,6 +261,7 @@ let
}; };
erase = mkEnableOption "remove bind"; erase = mkEnableOption "remove bind";
silent = mkEnableOption "Operate silently"; silent = mkEnableOption "Operate silently";
repaint = mkEnableOption "redraw prompt after command";
operate = mkOption { operate = mkOption {
description = "Operate on preset bindings or user bindings"; description = "Operate on preset bindings or user bindings";
type = type =
@ -324,6 +325,7 @@ let
{ {
silent, silent,
erase, erase,
repaint,
operate, operate,
mode, mode,
setsMode, setsMode,
@ -344,7 +346,11 @@ let
]; ];
cmdNormal = lib.concatStringsSep " " ( cmdNormal = lib.concatStringsSep " " (
[ "bind" ] ++ opts ++ [ k ] ++ map lib.escapeShellArg (lib.flatten [ command ]) [ "bind" ]
++ opts
++ [ k ]
++ map lib.escapeShellArg (lib.flatten [ command ])
++ lib.optional repaint "repaint"
); );
cmdErase = lib.concatStringsSep " " ( cmdErase = lib.concatStringsSep " " (

View file

@ -114,6 +114,24 @@ in
defaultText = lib.literalMD "`true` if programs.ghostty.package is not null"; 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 ( enableBashIntegration = mkShellIntegrationOption (
lib.hm.shell.mkBashIntegrationOption { inherit config; } 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 { (lib.mkIf cfg.enableBashIntegration {
# Make order 101 to be placed exactly after bash completions, as Ghostty # Make order 101 to be placed exactly after bash completions, as Ghostty
# documentation suggests sourcing the script as soon as possible # documentation suggests sourcing the script as soon as possible

View file

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

View file

@ -125,7 +125,7 @@ let
function importTrust() { function importTrust() {
local keyIds trust local keyIds trust
IFS='\n' read -ra keyIds <<< "$(gpgKeyId "$1")" mapfile -t keyIds <<< "$(gpgKeyId "$1")"
trust="$2" trust="$2"
for id in "''${keyIds[@]}" ; do for id in "''${keyIds[@]}" ; do
{ echo trust; echo "$trust"; (( trust == 5 )) && echo y; echo quit; } \ { echo trust; echo "$trust"; (( trust == 5 )) && echo y; echo quit; } \

View file

@ -42,6 +42,10 @@ let
mkKeyValue = key: command: "map ${key} ${command}"; mkKeyValue = key: command: "map ${key} ${command}";
}; };
toKittyMouseBindings = lib.generators.toKeyValue {
mkKeyValue = key: command: "mouse_map ${key} ${command}";
};
toKittyActionAliases = lib.generators.toKeyValue { toKittyActionAliases = lib.generators.toKeyValue {
mkKeyValue = alias_name: action: "action_alias ${alias_name} ${action}"; mkKeyValue = alias_name: action: "action_alias ${alias_name} ${action}";
}; };
@ -199,6 +203,18 @@ in
''; '';
}; };
mouseBindings = mkOption {
type = types.attrsOf types.str;
default = { };
description = "Mapping of mouse bindings to actions.";
example = literalExpression ''
{
"ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt";
"left click" = "ungrabbed no-op";
};
'';
};
environment = mkOption { environment = mkOption {
type = types.attrsOf types.str; type = types.attrsOf types.str;
default = { }; default = { };
@ -316,7 +332,8 @@ in
(mkOrder 540 (toKittyConfig cfg.settings)) (mkOrder 540 (toKittyConfig cfg.settings))
(mkOrder 550 (toKittyActionAliases cfg.actionAliases)) (mkOrder 550 (toKittyActionAliases cfg.actionAliases))
(mkOrder 560 (toKittyKeybindings cfg.keybindings)) (mkOrder 560 (toKittyKeybindings cfg.keybindings))
(mkOrder 570 (toKittyEnv cfg.environment)) (mkOrder 570 (toKittyMouseBindings cfg.mouseBindings))
(mkOrder 580 (toKittyEnv cfg.environment))
]; ];
xdg.configFile."kitty/kitty.conf" = { xdg.configFile."kitty/kitty.conf" = {

64
modules/programs/mcp.nix Normal file
View file

@ -0,0 +1,64 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
literalExpression
mkEnableOption
mkIf
mkOption
;
cfg = config.programs.mcp;
jsonFormat = pkgs.formats.json { };
in
{
meta.maintainers = with lib.maintainers; [ delafthi ];
options.programs.mcp = {
enable = mkEnableOption "mcp";
servers = mkOption {
inherit (jsonFormat) type;
default = { };
example = literalExpression ''
{
everything = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-everything"
];
};
context7 = {
url = "https://mcp.context7.com/mcp";
headers = {
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
};
};
}
'';
description = ''
MCP server configurations written to
{file}`XDG_CONFIG_HOME/.config/mcp/mcp.json`
'';
};
};
config = mkIf cfg.enable {
xdg.configFile = mkIf (cfg.servers != { }) (
let
mcp-config = {
mcpServers = cfg.servers;
};
in
{
"mcp/mcp.json".source = jsonFormat.generate "mcp.json" mcp-config;
}
);
};
}

View file

@ -16,6 +16,35 @@ let
cfg = config.programs.opencode; cfg = config.programs.opencode;
jsonFormat = pkgs.formats.json { }; jsonFormat = pkgs.formats.json { };
transformMcpServer = name: server: {
name = name;
value = {
enabled = !(server.disabled or false);
}
// (
if server ? url then
{
type = "remote";
url = server.url;
}
// (lib.optionalAttrs (server ? headers) { headers = server.headers; })
else if server ? command then
{
type = "local";
command = [ server.command ] ++ (server.args or [ ]);
}
// (lib.optionalAttrs (server ? env) { environment = server.env; })
else
{ }
);
};
transformedMcpServers =
if cfg.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then
lib.listToAttrs (lib.mapAttrsToList transformMcpServer config.programs.mcp.servers)
else
{ };
in in
{ {
meta.maintainers = with lib.maintainers; [ delafthi ]; meta.maintainers = with lib.maintainers; [ delafthi ];
@ -25,6 +54,20 @@ in
package = mkPackageOption pkgs "opencode" { nullable = true; }; package = mkPackageOption pkgs "opencode" { nullable = true; };
enableMcpIntegration = mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to integrate the MCP servers config from
{option}`programs.mcp.servers` into
{option}`programs.opencode.settings.mcp`.
Note: Settings defined in {option}`programs.mcp.servers` are merged
with {option}`programs.opencode.settings.mcp`, with OpenCode settings
taking precedence.
'';
};
settings = mkOption { settings = mkOption {
inherit (jsonFormat) type; inherit (jsonFormat) type;
default = { }; default = { };
@ -147,7 +190,7 @@ in
Custom themes for opencode. The attribute name becomes the theme Custom themes for opencode. The attribute name becomes the theme
filename, and the value is either: filename, and the value is either:
- An attribute set, that is converted to a json - An attribute set, that is converted to a json
- A path to a file conaining the content - A path to a file containing the content
Themes are stored in {file}`$XDG_CONFIG_HOME/opencode/themes/` directory. Themes are stored in {file}`$XDG_CONFIG_HOME/opencode/themes/` directory.
Set `programs.opencode.settings.theme` to enable the custom theme. Set `programs.opencode.settings.theme` to enable the custom theme.
See <https://opencode.ai/docs/themes/> for the documentation. See <https://opencode.ai/docs/themes/> for the documentation.
@ -159,12 +202,20 @@ in
home.packages = mkIf (cfg.package != null) [ cfg.package ]; home.packages = mkIf (cfg.package != null) [ cfg.package ];
xdg.configFile = { xdg.configFile = {
"opencode/config.json" = mkIf (cfg.settings != { }) { "opencode/config.json" = mkIf (cfg.settings != { } || transformedMcpServers != { }) {
source = jsonFormat.generate "config.json" ( source =
let
# Merge MCP servers: transformed servers + user settings, with user settings taking precedence
mergedMcpServers = transformedMcpServers // (cfg.settings.mcp or { });
# Merge all settings
mergedSettings =
cfg.settings // (lib.optionalAttrs (mergedMcpServers != { }) { mcp = mergedMcpServers; });
in
jsonFormat.generate "config.json" (
{ {
"$schema" = "https://opencode.ai/config.json"; "$schema" = "https://opencode.ai/config.json";
} }
// cfg.settings // mergedSettings
); );
}; };

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

@ -111,6 +111,23 @@ in
options = { options = {
enable = lib.mkEnableOption "this mount"; enable = lib.mkEnableOption "this mount";
logLevel = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"ERROR"
"NOTICE"
"INFO"
"DEBUG"
]
);
default = null;
example = "INFO";
description = ''
Set the log-level.
See: https://rclone.org/docs/#logging
'';
};
mountPoint = lib.mkOption { mountPoint = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = null; default = null;
@ -348,12 +365,13 @@ in
Environment = [ Environment = [
# fusermount/fusermount3 # fusermount/fusermount3
"PATH=/run/wrappers/bin" "PATH=/run/wrappers/bin"
]; ]
++ lib.optional (mount.logLevel != null) "RCLONE_LOG_LEVEL=${mount.logLevel}";
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mount.mountPoint}"; ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mount.mountPoint}";
ExecStart = lib.concatStringsSep " " [ ExecStart = lib.concatStringsSep " " [
(lib.getExe cfg.package) (lib.getExe cfg.package)
"mount" "mount"
"-vv"
(lib.cli.toGNUCommandLineShell { } mount.options) (lib.cli.toGNUCommandLineShell { } mount.options)
"${remote-name}:${mount-path}" "${remote-name}:${mount-path}"
"${mount.mountPoint}" "${mount.mountPoint}"

View file

@ -5,22 +5,34 @@
... ...
}: }:
let let
inherit (lib)
mkEnableOption
mkPackageOption
mkOption
mkIf
mkMerge
types
literalExpression
mapAttrs'
nameValuePair
;
cfg = config.programs.rio; cfg = config.programs.rio;
settingsFormat = pkgs.formats.toml { }; settingsFormat = pkgs.formats.toml { };
in in
{ {
options.programs.rio = { options.programs.rio = {
enable = lib.mkEnableOption null // { enable = mkEnableOption null // {
description = '' description = ''
Enable Rio, a terminal built to run everywhere, as a native desktop applications by Enable Rio, a terminal built to run everywhere, as a native desktop applications by
Rust/WebGPU or even in the browsers powered by WebAssembly/WebGPU. Rust/WebGPU or even in the browsers powered by WebAssembly/WebGPU.
''; '';
}; };
package = lib.mkPackageOption pkgs "rio" { nullable = true; }; package = mkPackageOption pkgs "rio" { nullable = true; };
settings = lib.mkOption { settings = mkOption {
type = settingsFormat.type; type = settingsFormat.type;
default = { }; default = { };
description = '' description = ''
@ -28,20 +40,50 @@ in
<https://raphamorim.io/rio/docs/#configuration-file> for options. <https://raphamorim.io/rio/docs/#configuration-file> for options.
''; '';
}; };
themes = mkOption {
type = with types; attrsOf (either settingsFormat.type path);
default = { };
description = ''
Theme files written to {file}`$XDG_CONFIG_HOME/rio/themes/`. See
<https://rioterm.com/docs/config#building-your-own-theme> for
supported values.
'';
example = literalExpression ''
{
foobar.colors = {
background = "#282a36";
green = "#50fa7b";
dim-green = "#06572f";
};
}
'';
};
}; };
meta.maintainers = [ lib.maintainers.otavio ]; meta.maintainers = [ lib.maintainers.otavio ];
config = lib.mkIf cfg.enable ( config = mkIf cfg.enable (mkMerge [
lib.mkMerge [
{ {
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; home.packages = mkIf (cfg.package != null) [ cfg.package ];
} }
# Only manage configuration if not empty # Only manage configuration if not empty
(lib.mkIf (cfg.settings != { }) { (mkIf (cfg.settings != { }) {
xdg.configFile."rio/config.toml".source = xdg.configFile."rio/config.toml".source =
if lib.isPath cfg.settings then cfg.settings else settingsFormat.generate "rio.toml" cfg.settings; if builtins.isPath cfg.settings then
cfg.settings
else
settingsFormat.generate "rio.toml" cfg.settings;
}) })
]
); (mkIf (cfg.themes != { }) {
xdg.configFile = mapAttrs' (
name: value:
nameValuePair "rio/themes/${name}.toml" {
source =
if builtins.isPath value then value else settingsFormat.generate "rio-theme-${name}.toml" value;
}
) cfg.themes;
})
]);
} }

View file

@ -7,7 +7,10 @@
let let
cfg = config.programs.superfile; cfg = config.programs.superfile;
tomlFormat = pkgs.formats.toml { }; tomlFormat = pkgs.formats.toml { };
jsonFormat = pkgs.formats.json { };
inherit (pkgs.stdenv.hostPlatform) isDarwin; inherit (pkgs.stdenv.hostPlatform) isDarwin;
inherit (lib) inherit (lib)
literalExpression literalExpression
@ -23,6 +26,29 @@ let
types types
hm 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 in
{ {
meta.maintainers = [ hm.maintainers.LucasWagler ]; 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 = config =
let let
enableXdgConfig = !isDarwin || config.xdg.enable; 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 = themeSetting =
if (!(cfg.settings ? theme) && cfg.themes != { }) then if (!(cfg.settings ? theme) && cfg.themes != { }) then
{ {
@ -118,7 +171,6 @@ in
} }
else else
{ }; { };
baseConfigPath = if enableXdgConfig then "superfile" else "Library/Application Support/superfile";
configFile = mkIf (cfg.settings != { }) { configFile = mkIf (cfg.settings != { }) {
"${baseConfigPath}/config.toml".source = tomlFormat.generate "superfile-config.toml" ( "${baseConfigPath}/config.toml".source = tomlFormat.generate "superfile-config.toml" (
recursiveUpdate themeSetting cfg.settings recursiveUpdate themeSetting cfg.settings
@ -139,14 +191,33 @@ in
(tomlFormat.generate "superfile-theme-${name}.toml" value); (tomlFormat.generate "superfile-theme-${name}.toml" value);
} }
) cfg.themes; ) 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 [ configFiles = mkMerge [
configFile configFile
hotkeysFile hotkeysFile
themeFiles themeFiles
]; ];
dataFiles = mkMerge [
firstUseCheckFile
pinnedFile
];
in in
mkIf cfg.enable { mkIf cfg.enable {
home.packages = mkIf (cfg.package != null) ( home = {
packages = mkIf (cfg.package != null) (
[ cfg.package ] [ cfg.package ]
++ optional ( ++ optional (
cfg.metadataPackage != null && cfg.settings ? metadata && cfg.settings.metadata cfg.metadataPackage != null && cfg.settings ? metadata && cfg.settings.metadata
@ -156,7 +227,12 @@ in
) cfg.zoxidePackage ) cfg.zoxidePackage
); );
xdg.configFile = mkIf enableXdgConfig configFiles; file = mkIf (!enableXdgConfig) files;
home.file = mkIf (!enableXdgConfig) configFiles; };
xdg = {
configFile = mkIf enableXdgConfig configFiles;
dataFile = mkIf enableXdgConfig dataFiles;
};
}; };
} }

View file

@ -87,7 +87,7 @@ in
''; '';
}; };
package = lib.mkPackageOption pkgs "taskwarrior" { package = lib.mkPackageOption pkgs "taskwarrior2" {
nullable = true; nullable = true;
example = "pkgs.taskwarrior3"; example = "pkgs.taskwarrior3";
}; };

View file

@ -0,0 +1,244 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.programs.vicinae;
jsonFormat = pkgs.formats.json { };
in
{
meta.maintainers = [ lib.maintainers.leiserfg ];
options.programs.vicinae = {
enable = lib.mkEnableOption "vicinae launcher daemon";
package = lib.mkPackageOption pkgs "vicinae" { nullable = true; };
systemd = {
enable = lib.mkEnableOption "vicinae systemd integration";
autoStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "If the vicinae daemon should be started automatically";
};
target = lib.mkOption {
type = lib.types.str;
default = "graphical-session.target";
example = "sway-session.target";
description = ''
The systemd target that will automatically start the vicinae service.
'';
};
};
useLayerShell = lib.mkOption {
type = lib.types.bool;
default = true;
description = "If vicinae should use the layer shell";
};
extensions = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = ''
List of Vicinae extensions to install.
You can use the `config.lib.vicinae.mkExtension` and `config.lib.vicinae.mkRayCastExtension` functions to create them, like:
```nix
[
(config.lib.vicinae.mkExtension {
name = "test-extension";
src =
pkgs.fetchFromGitHub {
owner = "schromp";
repo = "vicinae-extensions";
rev = "f8be5c89393a336f773d679d22faf82d59631991";
sha256 = "sha256-zk7WIJ19ITzRFnqGSMtX35SgPGq0Z+M+f7hJRbyQugw=";
}
+ "/test-extension";
})
(config.lib.vicinae.mkRayCastExtension {
name = "gif-search";
sha256 = "sha256-G7il8T1L+P/2mXWJsb68n4BCbVKcrrtK8GnBNxzt73Q=";
rev = "4d417c2dfd86a5b2bea202d4a7b48d8eb3dbaeb1";
})
],
```
'';
};
themes = lib.mkOption {
inherit (jsonFormat) type;
default = { };
description = ''
Theme settings to add to the themes folder in `~/.config/vicinae/themes`.
The attribute name of the theme will be the name of theme json file,
e.g. `base16-default-dark` will be `base16-default-dark.json`.
'';
example =
lib.literalExpression # nix
''
{
base16-default-dark = {
version = "1.0.0";
appearance = "dark";
icon = /path/to/icon.png;
name = "base16 default dark";
description = "base16 default dark by Chris Kempson";
palette = {
background = "#181818";
foreground = "#d8d8d8";
blue = "#7cafc2";
green = "#a3be8c";
magenta = "#ba8baf";
orange = "#dc9656";
purple = "#a16946";
red = "#ab4642";
yellow = "#f7ca88";
cyan = "#86c1b9";
};
};
}
'';
};
settings = lib.mkOption {
inherit (jsonFormat) type;
default = { };
description = "Settings written as JSON to `~/.config/vicinae/vicinae.json.";
example = lib.literalExpression ''
{
faviconService = "twenty";
font = {
size = 10;
};
popToRootOnClose = false;
rootSearch = {
searchFiles = false;
};
theme = {
name = "vicinae-dark";
};
window = {
csd = true;
opacity = 0.95;
rounding = 10;
};
}
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
(lib.hm.assertions.assertPlatform "programs.vicinae" pkgs lib.platforms.linux)
{
assertion = cfg.systemd.enable -> cfg.package != null;
message = "{option}programs.vicinae.systemd.enable requires non null {option}programs.vicinae.package";
}
];
lib.vicinae.mkExtension = (
{
name,
src,
}:
(pkgs.buildNpmPackage {
inherit name src;
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r /build/.local/share/vicinae/extensions/${name}/* $out/
runHook postInstall
'';
npmDeps = pkgs.importNpmLock { npmRoot = src; };
npmConfigHook = pkgs.importNpmLock.npmConfigHook;
})
);
lib.vicinae.mkRayCastExtension = (
{
name,
sha256,
rev,
}:
let
src =
pkgs.fetchgit {
inherit rev sha256;
url = "https://github.com/raycast/extensions";
sparseCheckout = [
"/extensions/${name}"
];
}
+ "/extensions/${name}";
in
(pkgs.buildNpmPackage {
inherit name src;
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r /build/.config/raycast/extensions/${name}/* $out/
runHook postInstall
'';
npmDeps = pkgs.importNpmLock { npmRoot = src; };
npmConfigHook = pkgs.importNpmLock.npmConfigHook;
})
);
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
xdg = {
configFile = {
"vicinae/vicinae.json" = lib.mkIf (cfg.settings != { }) {
source = jsonFormat.generate "vicinae-settings" cfg.settings;
};
}
// lib.mapAttrs' (
name: theme:
lib.nameValuePair "vicinae/themes/${name}.json" {
source = jsonFormat.generate "vicinae-${name}-theme" theme;
}
) cfg.themes;
dataFile = builtins.listToAttrs (
builtins.map (item: {
name = "vicinae/extensions/${item.name}";
value.source = item;
}) cfg.extensions
);
};
systemd.user.services.vicinae = lib.mkIf (cfg.systemd.enable && cfg.package != null) {
Unit = {
Description = "Vicinae server daemon";
Documentation = [ "https://docs.vicinae.com" ];
After = [ cfg.systemd.target ];
PartOf = [ cfg.systemd.target ];
BindsTo = [ cfg.systemd.target ];
};
Service = {
EnvironmentFile = pkgs.writeText "vicinae-env" ''
USE_LAYER_SHELL=${if cfg.useLayerShell then builtins.toString 1 else builtins.toString 0}
'';
Type = "simple";
ExecStart = "${lib.getExe' cfg.package "vicinae"} server";
Restart = "always";
RestartSec = 5;
KillMode = "process";
};
Install = lib.mkIf cfg.systemd.autoStart {
WantedBy = [ cfg.systemd.target ];
};
};
};
}

View file

@ -114,6 +114,33 @@ let
isPath = p: builtins.isPath p || lib.isStorePath p; isPath = p: builtins.isPath p || lib.isStorePath p;
transformMcpServerForVscode =
name: server:
let
# Remove the disabled field from the server config
cleanServer = lib.filterAttrs (n: v: n != "disabled") server;
in
{
name = name;
value = {
enabled = !(server.disabled or false);
}
// (
if server ? url then
{
type = "http";
}
// cleanServer
else if server ? command then
{
type = "stdio";
}
// cleanServer
else
{ }
);
};
profileType = types.submodule { profileType = types.submodule {
options = { options = {
userSettings = mkOption { userSettings = mkOption {
@ -154,6 +181,20 @@ let
''; '';
}; };
enableMcpIntegration = mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to integrate the MCP servers config from
{option}`programs.mcp.servers` into
{option}`programs.vscode.profiles.<name>.userMcp`.
Note: Settings defined in {option}`programs.mcp.servers` are merged
with {option}`programs.vscode.profiles.<name>.userMcp`, with VSCode
settings taking precedence.
'';
};
userMcp = mkOption { userMcp = mkOption {
type = types.either types.path jsonFormat.type; type = types.either types.path jsonFormat.type;
default = { }; default = { };
@ -418,7 +459,7 @@ in
existing_profiles=$(jq '.userDataProfiles // [] | map({ (.name): .location }) | add // {}' "$file") existing_profiles=$(jq '.userDataProfiles // [] | map({ (.name): .location }) | add // {}' "$file")
for profile in "''${profiles[@]}"; do 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" file_write="$file_write$([ "$file_write" != "" ] && echo "...")$profile"
fi fi
done done
@ -459,10 +500,31 @@ in
if isPath v.userTasks then v.userTasks else jsonFormat.generate "vscode-user-tasks" v.userTasks; if isPath v.userTasks then v.userTasks else jsonFormat.generate "vscode-user-tasks" v.userTasks;
}) })
(mkIf (v.userMcp != { }) { (mkIf
(
v.userMcp != { }
|| (v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { })
)
{
"${mcpFilePath n}".source = "${mcpFilePath n}".source =
if isPath v.userMcp then v.userMcp else jsonFormat.generate "vscode-user-mcp" v.userMcp; if isPath v.userMcp then
}) v.userMcp
else
let
transformedMcpServers =
if v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { } then
lib.listToAttrs (lib.mapAttrsToList transformMcpServerForVscode config.programs.mcp.servers)
else
{ };
# Merge MCP servers: transformed servers + user servers, with user servers taking precedence
mergedServers = transformedMcpServers // ((v.userMcp.servers or { }));
# Merge all MCP config
mergedMcpConfig =
v.userMcp // (lib.optionalAttrs (mergedServers != { }) { servers = mergedServers; });
in
jsonFormat.generate "vscode-user-mcp" mergedMcpConfig;
}
)
(mkIf (v.keybindings != [ ]) { (mkIf (v.keybindings != [ ]) {
"${keybindingsFilePath n}".source = "${keybindingsFilePath n}".source =

View file

@ -221,7 +221,7 @@ in
function ${cfg.shellWrapperName}() { function ${cfg.shellWrapperName}() {
local tmp="$(mktemp -t "yazi-cwd.XXXXX")" local tmp="$(mktemp -t "yazi-cwd.XXXXX")"
yazi "$@" --cwd-file="$tmp" yazi "$@" --cwd-file="$tmp"
if cwd="$(cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
builtin cd -- "$cwd" builtin cd -- "$cwd"
fi fi
rm -f -- "$tmp" rm -f -- "$tmp"
@ -231,7 +231,7 @@ in
fishIntegration = '' fishIntegration = ''
set -l tmp (mktemp -t "yazi-cwd.XXXXX") set -l tmp (mktemp -t "yazi-cwd.XXXXX")
command yazi $argv --cwd-file="$tmp" command yazi $argv --cwd-file="$tmp"
if set cwd (cat -- "$tmp"); and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ] if read cwd < "$tmp"; and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ]
builtin cd -- "$cwd" builtin cd -- "$cwd"
end end
rm -f -- "$tmp" rm -f -- "$tmp"

View file

@ -81,6 +81,15 @@ in
''; '';
}; };
mutableUserDebug = mkOption {
type = types.bool;
default = true;
example = false;
description = ''
Whether user debug configurations (debug.json) can be updated by zed.
'';
};
userSettings = mkOption { userSettings = mkOption {
type = jsonFormat.type; type = jsonFormat.type;
default = { }; default = { };
@ -140,6 +149,27 @@ in
''; '';
}; };
userDebug = mkOption {
type = jsonFormat.type;
default = [ ];
example = literalExpression ''
[
{
label = "Go (Delve)";
adapter = "Delve";
program = "$ZED_FILE";
request = "launch";
mode = "debug";
}
]
'';
description = ''
Configuration written to Zed's {file}`debug.json`.
Global debug configurations for Zed's [Debugger](https://zed.dev/docs/debugger).
'';
};
extensions = mkOption { extensions = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ ]; default = [ ];
@ -241,6 +271,14 @@ in
(jsonFormat.generate "zed-user-tasks" cfg.userTasks) (jsonFormat.generate "zed-user-tasks" cfg.userTasks)
); );
}) })
(mkIf (cfg.mutableUserDebug && cfg.userDebug != [ ]) {
zedDebugActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] (
impureConfigMerger "[]"
"$dynamic + $static | group_by(.label) | map(reduce .[] as $item ({}; . * $item))"
"${config.xdg.configHome}/zed/debug.json"
(jsonFormat.generate "zed-user-debug" cfg.userDebug)
);
})
]; ];
xdg.configFile = mkMerge [ xdg.configFile = mkMerge [
@ -265,6 +303,9 @@ in
(mkIf (!cfg.mutableUserTasks && cfg.userTasks != [ ]) { (mkIf (!cfg.mutableUserTasks && cfg.userTasks != [ ]) {
"zed/tasks.json".source = jsonFormat.generate "zed-user-tasks" cfg.userTasks; "zed/tasks.json".source = jsonFormat.generate "zed-user-tasks" cfg.userTasks;
}) })
(mkIf (!cfg.mutableUserDebug && cfg.userDebug != [ ]) {
"zed/debug.json".source = jsonFormat.generate "zed-user-debug" cfg.userDebug;
})
]; ];
assertions = [ assertions = [

View file

@ -42,7 +42,13 @@ in
services.cbatticon = { services.cbatticon = {
enable = lib.mkEnableOption "cbatticon"; enable = lib.mkEnableOption "cbatticon";
package = lib.mkPackageOption pkgs "cbatticon" { }; package = lib.mkPackageOption pkgs "cbatticon" {
example = "pkgs.batticonplus";
extraDescription = ''
Use {var}`pkgs.batticonplus`
for wayland support.
'';
};
commandCriticalLevel = mkOption { commandCriticalLevel = mkOption {
type = types.nullOr types.lines; type = types.nullOr types.lines;

View file

@ -19,25 +19,29 @@ let
inherit (config.programs.gpg) homedir; inherit (config.programs.gpg) homedir;
gpgSshSupportStr = '' gpgSshSupportStr = "${gpgPkg}/bin/gpg-connect-agent --quiet updatestartuptty /bye";
${gpgPkg}/bin/gpg-connect-agent --quiet updatestartuptty /bye
'';
gpgBashInitStr = '' gpgBashInitStr = ''
GPG_TTY="$(tty)" GPG_TTY="$(tty)"
export GPG_TTY export GPG_TTY
'' ''
+ optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + optionalString cfg.enableSshSupport ''
${gpgSshSupportStr} > /dev/null
'';
gpgZshInitStr = '' gpgZshInitStr = ''
export GPG_TTY=$TTY export GPG_TTY=$TTY
'' ''
+ optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + optionalString cfg.enableSshSupport ''
${gpgSshSupportStr} > /dev/null
'';
gpgFishInitStr = '' gpgFishInitStr = ''
set -gx GPG_TTY (tty) set -gx GPG_TTY (tty)
'' ''
+ optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + optionalString cfg.enableSshSupport ''
${gpgSshSupportStr} > /dev/null
'';
gpgNushellInitStr = '' gpgNushellInitStr = ''
$env.GPG_TTY = (tty) $env.GPG_TTY = (tty)

View file

@ -0,0 +1,48 @@
{
pkgs,
config,
lib,
...
}:
let
inherit (lib) types;
cfg = config.services.local-ai;
in
{
meta.maintainers = [ lib.maintainers.ipsavitsky ];
options.services.local-ai = {
enable = lib.mkEnableOption "LocalAI is the free, Open Source OpenAI alternative.";
package = lib.mkPackageOption pkgs "local-ai" { };
environment = lib.mkOption {
type = types.attrsOf types.str;
default = { };
description = ''
Additional environment passed to local-ai service. Used to configure local-ai
See <https://localai.io/basics> for available options.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.user.services.local-ai = {
Unit = {
Description = "Server for local large language models";
After = [ "network.target" ];
};
Service = {
ExecStart = lib.getExe cfg.package;
Environment = lib.mapAttrsToList (key: val: "${key}=${val}") cfg.environment;
};
Install = {
WantedBy = [ "default.target" ];
};
};
};
}

View file

@ -19,7 +19,7 @@ in
options.services.taskwarrior-sync = { options.services.taskwarrior-sync = {
enable = lib.mkEnableOption "Taskwarrior periodic sync"; enable = lib.mkEnableOption "Taskwarrior periodic sync";
package = lib.mkPackageOption pkgs "taskwarrior" { example = "pkgs.taskwarrior3"; }; package = lib.mkPackageOption pkgs "taskwarrior2" { example = "pkgs.taskwarrior3"; };
frequency = lib.mkOption { frequency = lib.mkOption {
type = lib.types.str; type = lib.types.str;

View file

@ -157,7 +157,7 @@ let
"spotify-player" "spotify-player"
"starship" "starship"
"superfile" "superfile"
"taskwarrior" "taskwarrior2"
"tealdeer" "tealdeer"
"texlive" "texlive"
"thefuck" "thefuck"

View file

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

View file

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

View file

@ -107,6 +107,12 @@
assert expected in actual, \ assert expected in actual, \
f"expected generations to contain {expected}, but found {actual}" f"expected generations to contain {expected}, but found {actual}"
with subtest("Home Manager option"):
actual = succeed_as_alice("home-manager option home.username")
expected = "alice"
assert expected in actual, \
f"expected generations to contain {expected}, but found {actual}"
with subtest("Home Manager uninstallation"): with subtest("Home Manager uninstallation"):
succeed_as_alice("yes | home-manager uninstall -L") succeed_as_alice("yes | home-manager uninstall -L")

View file

@ -5,7 +5,7 @@
font = { font = {
name = "Ubuntu"; name = "Ubuntu";
size = 12; size = 12;
package = pkgs.ubuntu_font_family; package = pkgs.ubuntu-classic;
}; };
theme = { theme = {
name = "Adwaita-dark"; name = "Adwaita-dark";

View file

@ -0,0 +1,26 @@
{
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

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

View file

@ -0,0 +1,7 @@
{
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

@ -0,0 +1,14 @@
{
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

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

View file

@ -0,0 +1,28 @@
{
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

@ -39,7 +39,7 @@ in
nmt.script = nmt.script =
let let
binPath = binPath =
if pkgs.hostPlatform.isDarwin then if pkgs.stdenv.hostPlatform.isDarwin then
"Applications/${cfg.darwinAppName}.app/Contents/MacOS" "Applications/${cfg.darwinAppName}.app/Contents/MacOS"
else else
"bin"; "bin";

View file

@ -39,7 +39,7 @@ in
nmt.script = nmt.script =
let let
binPath = binPath =
if pkgs.hostPlatform.isDarwin then if pkgs.stdenv.hostPlatform.isDarwin then
"Applications/${cfg.darwinAppName}.app/Contents/MacOS" "Applications/${cfg.darwinAppName}.app/Contents/MacOS"
else else
"bin"; "bin";

View file

@ -37,7 +37,7 @@ in
nmt.script = nmt.script =
let let
binPath = binPath =
if pkgs.hostPlatform.isDarwin then if pkgs.stdenv.hostPlatform.isDarwin then
"Applications/${cfg.darwinAppName}.app/Contents/MacOS" "Applications/${cfg.darwinAppName}.app/Contents/MacOS"
else else
"bin"; "bin";

View file

@ -13,7 +13,7 @@ in
{ {
imports = [ firefoxMockOverlay ]; imports = [ firefoxMockOverlay ];
config = lib.mkIf (config.test.enableBig && !pkgs.hostPlatform.isDarwin) ( config = lib.mkIf (config.test.enableBig && !pkgs.stdenv.hostPlatform.isDarwin) (
{ {
home.stateVersion = "19.09"; home.stateVersion = "19.09";
} }

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
{ {
gpg-immutable-keyfiles = ./immutable-keyfiles.nix; gpg-immutable-keyfiles = ./immutable-keyfiles.nix;
gpg-mutable-keyfiles = ./mutable-keyfiles.nix; gpg-mutable-keyfiles = ./mutable-keyfiles.nix;
gpg-multiple-keys-trust = ./multiple-keys-trust.nix;
gpg-override-defaults = ./override-defaults.nix; gpg-override-defaults = ./override-defaults.nix;
} }

View file

@ -0,0 +1,61 @@
{ realPkgs, ... }:
{
programs.gpg = {
enable = true;
package = realPkgs.gnupg;
mutableKeys = false;
mutableTrust = false;
publicKeys = [
{
# This file contains three public keys
# The bug causes only the first key to have trust set
source = ./test-keys/multiple-keys.asc;
trust = "ultimate"; # trust level 5
}
];
};
nmt.script = ''
assertFileNotRegex activate "^export GNUPGHOME=/home/hm-user/.gnupg$"
assertFileRegex activate \
'^install -m 0700 /nix/store/[0-9a-z]*-gpg-pubring/trustdb.gpg "/home/hm-user/.gnupg/trustdb.gpg"$'
# Setup GPGHOME
export GNUPGHOME=$(mktemp -d)
cp -r $TESTED/home-files/.gnupg/* $GNUPGHOME
TRUSTDB=$(grep -o '/nix/store/[0-9a-z]*-gpg-pubring/trustdb.gpg' $TESTED/activate)
install -m 0700 $TRUSTDB $GNUPGHOME/trustdb.gpg
# Export Trust
export WORKDIR=$(mktemp -d)
${realPkgs.gnupg}/bin/gpg -q --export-ownertrust > $WORKDIR/gpgtrust.txt
echo "=== Trust database contents ==="
cat $WORKDIR/gpgtrust.txt
echo "=== End of trust database ==="
# The test file contains three keys:
# - 13B06D9193E01E0F (Test User One) - fingerprint: B07502E7B7ED0A4AA3BF191913B06D9193E01E0F
# - 42E7B990011430DE (Test User Two) - fingerprint: 6A2A713AE7F93C8EA6D264B642E7B990011430DE
# - DFC825F8209CE742 (Test User Three) - fingerprint: E66D263DC7174345AB102829DFC825F8209CE742
#
# All three keys should have ultimate trust (level 6 in ownertrust format)
# Due to the bug in importTrust function, only the first key gets trust set
# Check that first key has ultimate trust (this works with current code)
assertFileRegex $WORKDIR/gpgtrust.txt \
'^B07502E7B7ED0A4AA3BF191913B06D9193E01E0F:6:$'
# Check that second key has ultimate trust (this FAILS due to bug)
assertFileRegex $WORKDIR/gpgtrust.txt \
'^6A2A713AE7F93C8EA6D264B642E7B990011430DE:6:$'
# Check that third key has ultimate trust (this FAILS due to bug)
assertFileRegex $WORKDIR/gpgtrust.txt \
'^E66D263DC7174345AB102829DFC825F8209CE742:6:$'
'';
}

View file

@ -0,0 +1,45 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGkHy/oBCADC4NT6P4eiOv1f9g8mhdLQlexO4Pefh33EicybD4tnlZZGVzYT
2J75slIGFV9+AOX/TXsws7+0IaZYB94a3p1NKoWeYh4XZy0HQ2HRJjNWeLQ41lFC
dCQ4A0JuqCurMFFdph59Xlh4ko3SXmPwNqXEmNX8LQlIDRNk+RiW+gJ4OC8DV6Do
YexeQHrHxtdGrStFmEygEAB5K1xqLRrzETvPubEmPEcrvhT/7W1+TwCb/haKo+Is
OgFcaJFv7CR6EbYh3DNZa4Zrd/WpNAL8+Kmz89VTdw0qaSYJxV9uR4DdmgX+2tAv
WmLuTuPMabU599p9nRUqk1Pj5fit6octCxX9ABEBAAG0IVRlc3QgVXNlciBPbmUg
PHRlc3QxQGV4YW1wbGUuY29tPokBTwQTAQoAORYhBLB1Aue37QpKo78ZGROwbZGT
4B4PBQJpB8v6AxsvBAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRATsG2Rk+Ae
D54fB/9EN7IjdwARheioFsZlifda5t31l084eYsq9kLzjCrxCXNlDZEIi6QrNBBA
CDZyv5bM+JLrZPbZ/1J1caoB6W9+ARPLiERWMhql7JNWSS/4Yhf/L0aD0C3pJFJf
h3bcSxhAzXBL3857cELR88UeV7NHPNdJsKVX0h7r1xe1D1oGZd19qbyZx3FJLzH8
p01ZkLoKdKAh42x+XN6KrOWGWFyvLX56pXjp9mjero2iDpUlBdIV15CFJ+aoVI3B
KG26z4B7/L8kQVO2eH41k/i39u9SuvuCinYcNQ/5/blpaIc7xqL5jI1gapzE4bBu
GzGOKJoWRgGJDUZzyvTtxbI/nsK6mQENBGkHy/oBCADHGrIJ1uTGWJvSt+2pmqxK
ruXQvVxQva3GbYIgePQa88PzhORYTnuskEdOhNhMTaxKWbxS1bfDXf3Akjis+kHb
xLK692XtKFf88ALV6ts0Rd4YRG6BCcwMPAfFuQhyQRxclNk5XHzaH6IvKvmrSkvG
wilLkrdj9hW32FvVYDyjdiDSbvs05d8EfRr7UF/fMQC5HOJJ6VSC7HJ7tQGWvtNG
eyr/I61OSDxhf6PF5CfuepajO0nzsVHvsXTxoJwYbx+zXSlGxTsHWYxp6r0MdPE/
vCNmvrfpz4PoTiE43Xa3XsYSO2gRCpMYJKQaxl5pCfBGSmKpCF1YDBSTrRYyacyv
ABEBAAG0IVRlc3QgVXNlciBUd28gPHRlc3QyQGV4YW1wbGUuY29tPokBTwQTAQoA
ORYhBGoqcTrn+TyOptJktkLnuZABFDDeBQJpB8v6AxsvBAULCQgHAgYVCgkICwIE
FgIDAQIeAQIXgAAKCRBC57mQARQw3nIGB/9/j1SIk+DxmCeT2fihQmS7lubDoq1I
FUdjb7cAGBs4KAmJh8MVMsYyB+EtaVC8qu4C5EgNNV0+c2H8UishGcZvMm9Qg7LQ
MTSGKLwXikaiIvyw3zlh1FpJn2rYUSvCplVswhF/dfSlenmU81eiPigYsvzVoa8h
xJNn01DLu4cd2VsBhWW/2w3DKSvVHRPdlPTPrqkjzMQRy2ULa2yTWiiuxWJxHuj0
3ocvLGlpyyvIwyoFVG4Lex4r+jSL3RCllEUjADAMgDPfhoTEerfgORCVEqGE/JLR
MVrTl6bMuodGehXgCRalcg9ChUADBHS4fZ0NiH46QhTblwRRFc2K6WbzmQENBGkH
y/oBCADAzZTgBmulUSr29gmBELA1gpMNHZ3J/2R3mTXMFaZAsi84uCZNyLLrDhU4
WaXVRURlwY4eHdvIMc3IM846s0SkLKDy3cIbusQK9NDVS/69LRyKNiZMjEbpODZl
fT5AtQUOL1jAIxy/wVEKzqih0so6mfNCwKFshWyi4p2+E8dFT8apTvhwJkdpptb6
q8Q1ABx+NRE1iSK+lFUw7xD7lLDvUYcHn6glpEMIGjg3/BLF74nVYFe6rCuFKgNt
GHLk1ZjoldbQRmTxdaKkb6vmfPWjbQuZCdNAUT87ljnrpdl3YxRN2ujQ1tHrWkby
C+anhmkdoQnqQPpICaeLe6NwHpPVABEBAAG0I1Rlc3QgVXNlciBUaHJlZSA8dGVz
dDNAZXhhbXBsZS5jb20+iQFPBBMBCgA5FiEE5m0mPccXQ0WrECgp38gl+CCc50IF
AmkHy/oDGy8EBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEN/IJfggnOdC7qEH
/idAjYhb9QNnOOu7lPkgLnPVanLCE20uHoGLeDUNkz2+2VFmkTu9poHKp4P7tW4e
/wMyy6uv4X1kcp6XcwVALx2HRU/PKLy1kNQFEeDocA1fx0wloJTfGfJpbxXWPFUG
oTVx0V2BwjiGK1+MTZCJQ+aqS2mXPLMPRv0ZKw8CQOeGHRJCD3NBEiWxpi5wncFM
DFDnaKrTCgmndRIafdXU3B7L4zZkNwcXRylkxVFjl938W5czbqa0o2LLadd/trJZ
YN/21BNkS/QmrH1Kapcgj5GvJp8ky4OpccrCTxfWLmRVfxtdo/N2woNyK9xvjiwd
TYMaXvrf93dAboJrOmiAtPA=
=tjTO
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -19,5 +19,8 @@ action_alias launch_window launch --cwd=current --type=os-window
map ctrl+c copy_or_interrupt map ctrl+c copy_or_interrupt
map ctrl+f>2 set_font_size 20 map ctrl+f>2 set_font_size 20
mouse_map ctrl+left click ungrabbed mouse_handle_click selection link prompt
mouse_map left click ungrabbed no-op
env LS_COLORS=1 env LS_COLORS=1

View file

@ -25,6 +25,11 @@
"ctrl+f>2" = "set_font_size 20"; "ctrl+f>2" = "set_font_size 20";
}; };
mouseBindings = {
"ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt";
"left click" = "ungrabbed no-op";
};
actionAliases = { actionAliases = {
"launch_tab" = "launch --cwd=current --type=tab"; "launch_tab" = "launch --cwd=current --type=tab";
"launch_window" = "launch --cwd=current --type=os-window"; "launch_window" = "launch --cwd=current --type=os-window";

View file

@ -17,5 +17,8 @@ action_alias launch_window launch --cwd=current --type=os-window
map ctrl+c copy_or_interrupt map ctrl+c copy_or_interrupt
map ctrl+f>2 set_font_size 20 map ctrl+f>2 set_font_size 20
mouse_map ctrl+left click ungrabbed mouse_handle_click selection link prompt
mouse_map left click ungrabbed no-op
env LS_COLORS=1 env LS_COLORS=1

View file

@ -25,6 +25,11 @@
"ctrl+f>2" = "set_font_size 20"; "ctrl+f>2" = "set_font_size 20";
}; };
mouseBindings = {
"ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt";
"left click" = "ungrabbed no-op";
};
actionAliases = { actionAliases = {
"launch_tab" = "launch --cwd=current --type=tab"; "launch_tab" = "launch --cwd=current --type=tab";
"launch_window" = "launch --cwd=current --type=os-window"; "launch_window" = "launch --cwd=current --type=os-window";

View file

@ -0,0 +1,4 @@
{
mcp-servers = ./servers.nix;
mcp-empty-servers = ./empty-servers.nix;
}

View file

@ -0,0 +1,9 @@
{
programs.mcp = {
enable = true;
servers = { };
};
nmt.script = ''
assertPathNotExists home-files/.config/mcp/mcp.json
'';
}

View file

@ -0,0 +1,17 @@
{
"mcpServers": {
"context7": {
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
},
"serverUrl": "https://mcp.context7.com/mcp"
},
"everything": {
"args": [
"-y",
"@modelcontextprotocol/server-everything"
],
"command": "npx"
}
}
}

View file

@ -0,0 +1,25 @@
{
programs.mcp = {
enable = true;
servers = {
everything = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-everything"
];
};
context7 = {
serverUrl = "https://mcp.context7.com/mcp";
headers = {
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
};
};
};
};
nmt.script = ''
assertFileExists home-files/.config/mcp/mcp.json
assertFileContent home-files/.config/mcp/mcp.json \
${./mcp.json}
'';
}

View file

@ -11,4 +11,6 @@
opencode-mixed-content = ./mixed-content.nix; opencode-mixed-content = ./mixed-content.nix;
opencode-themes-inline = ./themes-inline.nix; opencode-themes-inline = ./themes-inline.nix;
opencode-themes-path = ./themes-path.nix; opencode-themes-path = ./themes-path.nix;
opencode-mcp-integration = ./mcp-integration.nix;
opencode-mcp-integration-with-override = ./mcp-integration-with-override.nix;
} }

View file

@ -0,0 +1,27 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"context7": {
"enabled": true,
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
},
"type": "remote",
"url": "https://mcp.context7.com/mcp"
},
"custom-server": {
"enabled": true,
"type": "remote",
"url": "https://example.com"
},
"everything": {
"command": [
"custom-command"
],
"enabled": false,
"type": "local"
}
},
"model": "anthropic/claude-sonnet-4-20250514",
"theme": "opencode"
}

View file

@ -0,0 +1,48 @@
{
programs.mcp = {
enable = true;
servers = {
everything = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-everything"
];
};
context7 = {
url = "https://mcp.context7.com/mcp";
headers = {
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
};
};
};
};
programs.opencode = {
enable = true;
enableMcpIntegration = true;
settings = {
theme = "opencode";
model = "anthropic/claude-sonnet-4-20250514";
# User's custom MCP settings should override generated ones
mcp = {
everything = {
enabled = false; # Override to disable
command = [ "custom-command" ];
type = "local";
};
custom-server = {
enabled = true;
type = "remote";
url = "https://example.com";
};
};
};
};
nmt.script = ''
assertFileExists home-files/.config/opencode/config.json
assertFileContent home-files/.config/opencode/config.json \
${./mcp-integration-with-override.json}
'';
}

View file

@ -0,0 +1,30 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"context7": {
"enabled": true,
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
},
"type": "remote",
"url": "https://mcp.context7.com/mcp"
},
"disabled-server": {
"command": [
"echo",
"test"
],
"enabled": false,
"type": "local"
},
"everything": {
"command": [
"npx",
"-y",
"@modelcontextprotocol/server-everything"
],
"enabled": true,
"type": "local"
}
}
}

View file

@ -0,0 +1,36 @@
{
programs.mcp = {
enable = true;
servers = {
everything = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-everything"
];
};
context7 = {
url = "https://mcp.context7.com/mcp";
headers = {
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
};
};
disabled-server = {
command = "echo";
args = [ "test" ];
disabled = true;
};
};
};
programs.opencode = {
enable = true;
enableMcpIntegration = true;
};
nmt.script = ''
assertFileExists home-files/.config/opencode/config.json
assertFileContent home-files/.config/opencode/config.json \
${./mcp-integration.json}
'';
}

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

@ -1,4 +1,5 @@
{ {
rio-example-settings = ./example-settings.nix; rio-example-settings = ./example-settings.nix;
rio-empty-settings = ./empty-settings.nix; rio-empty-settings = ./empty-settings.nix;
rio-themes = ./example-themes.nix;
} }

View file

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

View file

@ -0,0 +1,30 @@
{ config, ... }:
{
programs.rio = {
enable = true;
package = config.lib.test.mkStubPackage { };
themes = {
foobar.colors = {
cyan = "#8be9fd";
green = "#50fa7b";
background = "#282a36";
};
foobar2 = ./foobar.toml;
};
};
nmt.script = ''
assertFileExists home-files/.config/rio/themes/foobar.toml
assertFileExists home-files/.config/rio/themes/foobar2.toml
assertFileContent \
home-files/.config/rio/themes/foobar.toml \
${./foobar.toml}
assertFileContent \
home-files/.config/rio/themes/foobar2.toml \
${./foobar.toml}
'';
}

View file

@ -0,0 +1,4 @@
[colors]
background = "#282a36"
cyan = "#8be9fd"
green = "#50fa7b"

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 = nmt.script =
@ -60,6 +67,10 @@
configSubPath = configSubPath =
if !pkgs.stdenv.isDarwin then ".config/superfile" else "Library/Application Support/superfile"; if !pkgs.stdenv.isDarwin then ".config/superfile" else "Library/Application Support/superfile";
configBasePath = "home-files/" + configSubPath; configBasePath = "home-files/" + configSubPath;
dataSubPath =
if !pkgs.stdenv.isDarwin then ".local/share/superfile" else "Library/Application Support/superfile";
dataBasePath = "home-files/" + dataSubPath;
in in
'' ''
assertFileExists "${configBasePath}/config.toml" assertFileExists "${configBasePath}/config.toml"
@ -82,5 +93,10 @@
assertFileContent \ assertFileContent \
"${configBasePath}/theme/test2.toml" \ "${configBasePath}/theme/test2.toml" \
${./example-theme2-expected.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,4 @@
{ lib, pkgs, ... }:
lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux) {
vicinae-example-settings = ./example-settings.nix;
}

View file

@ -0,0 +1,77 @@
{
pkgs,
config,
...
}:
{
programs.vicinae = {
enable = true;
systemd.enable = true;
settings = {
faviconService = "twenty";
font = {
size = 10;
};
popToRootOnClose = false;
rootSearch = {
searchFiles = false;
};
theme = {
name = "vicinae-dark";
};
window = {
csd = true;
opacity = 0.95;
rounding = 10;
};
};
themes = {
base16-default-dark = {
version = "1.0.0";
appearance = "dark";
name = "base16 default dark";
description = "base16 default dark by Chris Kempson";
palette = {
background = "#181818";
foreground = "#d8d8d8";
blue = "#7cafc2";
green = "#a3be8c";
magenta = "#ba8baf";
orange = "#dc9656";
purple = "#a16946";
red = "#ab4642";
yellow = "#f7ca88";
cyan = "#86c1b9";
};
};
};
extensions = [
(config.lib.vicinae.mkRayCastExtension {
name = "gif-search";
sha256 = "sha256-G7il8T1L+P/2mXWJsb68n4BCbVKcrrtK8GnBNxzt73Q=";
rev = "4d417c2dfd86a5b2bea202d4a7b48d8eb3dbaeb1";
})
(config.lib.vicinae.mkExtension {
name = "test-extension";
src =
pkgs.fetchFromGitHub {
owner = "schromp";
repo = "vicinae-extensions";
rev = "f8be5c89393a336f773d679d22faf82d59631991";
sha256 = "sha256-zk7WIJ19ITzRFnqGSMtX35SgPGq0Z+M+f7hJRbyQugw=";
}
+ "/test-extension";
})
];
};
nmt.script = ''
assertFileExists "home-files/.config/vicinae/vicinae.json"
assertFileExists "home-files/.config/systemd/user/vicinae.service"
assertFileExists "home-files/.local/share/vicinae/extensions/gif-search/package.json"
assertFileExists "home-files/.local/share/vicinae/extensions/test-extension/package.json"
'';
}

View file

@ -24,6 +24,8 @@ let
keybindings = import ./keybindings.nix; keybindings = import ./keybindings.nix;
tasks = import ./tasks.nix; tasks = import ./tasks.nix;
mcp = import ./mcp.nix; mcp = import ./mcp.nix;
mcp-integration = import ./mcp-integration.nix;
mcp-integration-with-override = import ./mcp-integration-with-override.nix;
update-checks = import ./update-checks.nix; update-checks = import ./update-checks.nix;
snippets = import ./snippets.nix; snippets = import ./snippets.nix;
}; };

View file

@ -0,0 +1,26 @@
{
"servers": {
"context7": {
"enabled": true,
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
},
"type": "http",
"url": "https://mcp.context7.com/mcp"
},
"disabled-server": {
"command": "echo",
"enabled": false,
"type": "stdio"
},
"everything": {
"args": [
"-y",
"@modelcontextprotocol/server-everything"
],
"command": "npx",
"enabled": true,
"type": "stdio"
}
}
}

View file

@ -0,0 +1,7 @@
{
"servers": {
"Github": {
"url": "https://api.githubcopilot.com/mcp/"
}
}
}

View file

@ -0,0 +1,25 @@
{
"servers": {
"CustomServer": {
"type": "http",
"url": "https://example.com/mcp"
},
"context7": {
"enabled": true,
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
},
"type": "http",
"url": "https://mcp.context7.com/mcp"
},
"everything": {
"args": [
"-y",
"@modelcontextprotocol/server-everything"
],
"command": "custom-npx",
"enabled": false,
"type": "stdio"
}
}
}

View file

@ -0,0 +1,79 @@
package:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.programs.vscode;
willUseIfd = package.pname != "vscode";
mcpFilePath =
name:
if pkgs.stdenv.hostPlatform.isDarwin then
"Library/Application Support/${cfg.nameShort}/User/${
lib.optionalString (name != "default") "profiles/${name}/"
}mcp.json"
else
".config/${cfg.nameShort}/User/${
lib.optionalString (name != "default") "profiles/${name}/"
}mcp.json";
in
lib.mkIf (willUseIfd -> config.test.enableLegacyIfd) {
programs.mcp = {
enable = true;
servers = {
everything = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-everything"
];
};
context7 = {
url = "https://mcp.context7.com/mcp";
headers = {
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
};
};
};
};
programs.vscode = {
enable = true;
inherit package;
profiles = {
default = {
enableMcpIntegration = true;
# User MCP settings should override generated ones
userMcp = {
servers = {
everything = {
command = "custom-npx";
args = [
"-y"
"@modelcontextprotocol/server-everything"
];
enabled = false;
type = "stdio";
};
CustomServer = {
type = "http";
url = "https://example.com/mcp";
};
};
};
};
};
};
nmt.script = ''
assertFileExists "home-files/${mcpFilePath "default"}"
assertFileContent "home-files/${mcpFilePath "default"}" ${./mcp-integration-with-override.json}
'';
}

View file

@ -0,0 +1,73 @@
package:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.programs.vscode;
willUseIfd = package.pname != "vscode";
mcpFilePath =
name:
if pkgs.stdenv.hostPlatform.isDarwin then
"Library/Application Support/${cfg.nameShort}/User/${
lib.optionalString (name != "default") "profiles/${name}/"
}mcp.json"
else
".config/${cfg.nameShort}/User/${
lib.optionalString (name != "default") "profiles/${name}/"
}mcp.json";
in
lib.mkIf (willUseIfd -> config.test.enableLegacyIfd) {
programs.mcp = {
enable = true;
servers = {
everything = {
command = "npx";
args = [
"-y"
"@modelcontextprotocol/server-everything"
];
};
context7 = {
url = "https://mcp.context7.com/mcp";
headers = {
CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}";
};
};
disabled-server = {
command = "echo";
disabled = true;
};
};
};
programs.vscode = {
enable = true;
inherit package;
profiles = {
default.enableMcpIntegration = true;
test.userMcp = {
servers = {
Github = {
url = "https://api.githubcopilot.com/mcp/";
};
};
};
};
};
nmt.script = ''
assertFileExists "home-files/${mcpFilePath "default"}"
assertFileContent "home-files/${mcpFilePath "default"}" ${./mcp-integration-default.json}
assertFileExists "home-files/${mcpFilePath "test"}"
assertFileContent "home-files/${mcpFilePath "test"}" ${./mcp-integration-test.json}
'';
}

View file

@ -1,15 +1,3 @@
let
shellIntegration = ''
function yy() {
local tmp="$(mktemp -t "yazi-cwd.XXXXX")"
yazi "$@" --cwd-file="$tmp"
if cwd="$(cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
builtin cd -- "$cwd"
fi
rm -f -- "$tmp"
}
'';
in
{ {
programs.bash.enable = true; programs.bash.enable = true;
@ -19,6 +7,12 @@ in
}; };
nmt.script = '' nmt.script = ''
assertFileContains home-files/.bashrc '${shellIntegration}' assertFileExists home-files/.bashrc
assertFileContains home-files/.bashrc 'function yy() {'
assertFileContains home-files/.bashrc 'local tmp="$(mktemp -t "yazi-cwd.XXXXX")"'
assertFileContains home-files/.bashrc 'yazi "$@" --cwd-file="$tmp"'
assertFileContains home-files/.bashrc 'if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then'
assertFileContains home-files/.bashrc 'builtin cd -- "$cwd"'
assertFileContains home-files/.bashrc 'rm -f -- "$tmp"'
''; '';
} }

View file

@ -1,7 +1,7 @@
function yy function yy
set -l tmp (mktemp -t "yazi-cwd.XXXXX") set -l tmp (mktemp -t "yazi-cwd.XXXXX")
command yazi $argv --cwd-file="$tmp" command yazi $argv --cwd-file="$tmp"
if set cwd (cat -- "$tmp"); and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ] if read cwd <"$tmp"; and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ]
builtin cd -- "$cwd" builtin cd -- "$cwd"
end end
rm -f -- "$tmp" rm -f -- "$tmp"

View file

@ -1,24 +1,18 @@
let
shellIntegration = ''
function yy() {
local tmp="$(mktemp -t "yazi-cwd.XXXXX")"
yazi "$@" --cwd-file="$tmp"
if cwd="$(cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
builtin cd -- "$cwd"
fi
rm -f -- "$tmp"
}
'';
in
{ {
programs.zsh.enable = true; programs.zsh.enable = true;
programs.yazi = { programs.yazi = {
enable = true; enable = true;
enableBashIntegration = true; enableZshIntegration = true;
}; };
nmt.script = '' nmt.script = ''
assertFileContains home-files/.zshrc '${shellIntegration}' assertFileExists home-files/.zshrc
assertFileContains home-files/.zshrc 'function yy() {'
assertFileContains home-files/.zshrc 'local tmp="$(mktemp -t "yazi-cwd.XXXXX")"'
assertFileContains home-files/.zshrc 'yazi "$@" --cwd-file="$tmp"'
assertFileContains home-files/.zshrc 'if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then'
assertFileContains home-files/.zshrc 'builtin cd -- "$cwd"'
assertFileContains home-files/.zshrc 'rm -f -- "$tmp"'
''; '';
} }

View file

@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
{
programs.zed-editor = {
enable = true;
package = config.lib.test.mkStubPackage { };
userDebug = [
{
label = "PHP: Listen to Xdebug";
adapter = "Xdebug";
request = "launch";
port = 9003;
}
{
label = "PHP: Debug this test";
adapter = "Xdebug";
request = "launch";
program = "vendor/bin/phpunit";
args = [
"--filter"
"$ZED_SYMBOL"
];
}
];
};
home.homeDirectory = lib.mkForce "/@TMPDIR@/hm-user";
nmt.script =
let
preexistingDebug = builtins.toFile "preexisting.json" "";
expectedContent = builtins.toFile "expected.json" ''
[
{
"adapter": "Xdebug",
"args": [
"--filter",
"$ZED_SYMBOL"
],
"label": "PHP: Debug this test",
"program": "vendor/bin/phpunit",
"request": "launch"
},
{
"adapter": "Xdebug",
"label": "PHP: Listen to Xdebug",
"port": 9003,
"request": "launch"
}
]
'';
debugPath = ".config/zed/debug.json";
activationScript = pkgs.writeScript "activation" config.home.activation.zedDebugActivation.data;
in
''
export HOME=$TMPDIR/hm-user
# Simulate preexisting debug
mkdir -p $HOME/.config/zed
cat ${preexistingDebug} > $HOME/${debugPath}
# Run the activation script
substitute ${activationScript} $TMPDIR/activate --subst-var TMPDIR
chmod +x $TMPDIR/activate
$TMPDIR/activate
# Validate the merged debug
assertFileExists "$HOME/${debugPath}"
assertFileContent "$HOME/${debugPath}" "${expectedContent}"
# Test idempotency
$TMPDIR/activate
assertFileExists "$HOME/${debugPath}"
assertFileContent "$HOME/${debugPath}" "${expectedContent}"
'';
}

View file

@ -0,0 +1,58 @@
# Test custom keymap functionality
{ config, ... }:
{
programs.zed-editor = {
enable = true;
package = config.lib.test.mkStubPackage { };
mutableUserDebug = false;
userDebug = [
{
label = "PHP: Listen to Xdebug";
adapter = "Xdebug";
request = "launch";
port = 9003;
}
{
label = "PHP: Debug this test";
adapter = "Xdebug";
request = "launch";
program = "vendor/bin/phpunit";
args = [
"--filter"
"$ZED_SYMBOL"
];
}
];
};
nmt.script =
let
expectedContent = builtins.toFile "expected.json" ''
[
{
"adapter": "Xdebug",
"label": "PHP: Listen to Xdebug",
"port": 9003,
"request": "launch"
},
{
"adapter": "Xdebug",
"args": [
"--filter",
"$ZED_SYMBOL"
],
"label": "PHP: Debug this test",
"program": "vendor/bin/phpunit",
"request": "launch"
}
]
'';
settingsPath = ".config/zed/debug.json";
in
''
assertFileExists "home-files/${settingsPath}"
assertFileContent "home-files/${settingsPath}" "${expectedContent}"
'';
}

View file

@ -0,0 +1,100 @@
{
config,
lib,
pkgs,
...
}:
{
programs.zed-editor = {
enable = true;
package = config.lib.test.mkStubPackage { };
userDebug = [
{
label = "PHP: Listen to Xdebug";
adapter = "Xdebug";
request = "launch";
port = 9003;
}
{
label = "PHP: Debug this test";
adapter = "Xdebug";
request = "launch";
program = "vendor/bin/phpunit";
args = [
"--filter"
"$ZED_SYMBOL"
];
}
];
};
home.homeDirectory = lib.mkForce "/@TMPDIR@/hm-user";
nmt.script =
let
preexistingDebug = builtins.toFile "preexisting.json" ''
[
{
"label": "Debug active Python file",
"adapter": "Debugpy",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
]
'';
expectedContent = builtins.toFile "expected.json" ''
[
{
"label": "Debug active Python file",
"adapter": "Debugpy",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"adapter": "Xdebug",
"args": [
"--filter",
"$ZED_SYMBOL"
],
"label": "PHP: Debug this test",
"program": "vendor/bin/phpunit",
"request": "launch"
},
{
"adapter": "Xdebug",
"label": "PHP: Listen to Xdebug",
"port": 9003,
"request": "launch"
}
]
'';
debugPath = ".config/zed/debug.json";
activationScript = pkgs.writeScript "activation" config.home.activation.zedDebugActivation.data;
in
''
export HOME=$TMPDIR/hm-user
# Simulate preexisting debug
mkdir -p $HOME/.config/zed
cat ${preexistingDebug} > $HOME/${debugPath}
# Run the activation script
substitute ${activationScript} $TMPDIR/activate --subst-var TMPDIR
chmod +x $TMPDIR/activate
$TMPDIR/activate
# Validate the merged debug
assertFileExists "$HOME/${debugPath}"
assertFileContent "$HOME/${debugPath}" "${expectedContent}"
# Test idempotency
$TMPDIR/activate
assertFileExists "$HOME/${debugPath}"
assertFileContent "$HOME/${debugPath}" "${expectedContent}"
'';
}

View file

@ -11,5 +11,8 @@
zed-tasks = ./tasks.nix; zed-tasks = ./tasks.nix;
zed-tasks-immutable = ./tasks-immutable.nix; zed-tasks-immutable = ./tasks-immutable.nix;
zed-tasks-empty = ./tasks-empty.nix; zed-tasks-empty = ./tasks-empty.nix;
zed-debug = ./debug.nix;
zed-debug-immutable = ./debug-immutable.nix;
zed-debug-empty = ./debug-empty.nix;
zed-themes = ./themes; zed-themes = ./themes;
} }

View file

@ -17,7 +17,7 @@
''; '';
lightModeScripts.color-scheme-light = pkgs.writeScript "my-python-script" '' lightModeScripts.color-scheme-light = pkgs.writeScript "my-python-script" ''
#!${pkgs.python}/bin/python #!${pkgs.python2}/bin/python
print('Do something!') print('Do something!')
''; '';

View file

@ -0,0 +1,6 @@
{ lib, pkgs, ... }:
lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
local-ai-enabled = ./enabled.nix;
local-ai-enabled-with-environment = ./enabled-with-environment.nix;
}

View file

@ -0,0 +1,18 @@
{
services.local-ai = {
enable = true;
environment = {
MODELS_PATH = "/tmp/models";
PRELOAD_MODELS = "[{ \"url\": \"https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf\", \"name\": \"mistral-7b-instruct-v0.2.Q4_K_M.gguf\" }]";
};
};
nmt.script = ''
assertFileContains \
home-files/.config/systemd/user/local-ai.service \
"Environment=MODELS_PATH=/tmp/models"
assertFileContains \
home-files/.config/systemd/user/local-ai.service \
"Environment=PRELOAD_MODELS=[{ \"url\": \"https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf\", \"name\": \"mistral-7b-instruct-v0.2.Q4_K_M.gguf\" }]"
'';
}

View file

@ -0,0 +1,7 @@
{
services.local-ai.enable = true;
nmt.script = ''
assertFileExists home-files/.config/systemd/user/local-ai.service
'';
}