diff --git a/docs/home-manager.1 b/docs/home-manager.1 index 068a41aad..3cd386369 100644 --- a/docs/home-manager.1 +++ b/docs/home-manager.1 @@ -24,7 +24,7 @@ .Cm | generations .Cm | help .Cm | news -.Cm | option Ar option.name +.Cm | option Oo Fl -recursive Oc Ar option.name .Cm | packages .Cm | remove-generations Ar ID \&... .Cm | switch @@ -138,10 +138,14 @@ Show news entries in a pager. .RE .PP -.It Cm option Ar option.name +.It Cm option Oo Fl -recursive Oc Ar option.name .RS 4 Inspect the given option name in the home configuration, like \fBnixos-option\fR(8)\&. +.sp +If the +.Fl -recursive +option is given, print all the values at or below the option name recursively\&. .RE .Pp diff --git a/flake.lock b/flake.lock index 94bff35ca..35822786c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1761373498, - "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", + "lastModified": 1762111121, + "narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", + "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", "type": "github" }, "original": { diff --git a/home-manager/home-manager b/home-manager/home-manager index e72151d28..7b4bcf490 100644 --- a/home-manager/home-manager +++ b/home-manager/home-manager @@ -232,7 +232,29 @@ function doInspectOption() { fi 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 extraArgs=("${extraArgs[@]}" "-I" "$p") @@ -256,11 +278,24 @@ function doInspectOption() { modulesExpr+=" configuration = if confAttr == \"\" then confPath else (import confPath).\${confAttr};" modulesExpr+=" pkgs = import {}; check = true; })" - nixos-option \ - --options_expr "$modulesExpr.options" \ - --config_expr "$modulesExpr.config" \ - "${extraArgs[@]}" \ - "${PASSTHROUGH_OPTS[@]}" + local NIXOS_OPTION_CMD NIXOS_OPTION_REAL NIXOS_OPTION_DIR NIXOS_OPTION_NIX + NIXOS_OPTION_CMD=$(command -v nixos-option) + NIXOS_OPTION_REAL=$(realpath "${NIXOS_OPTION_CMD}") + NIXOS_OPTION_NIX=$(nix-store -q --references "${NIXOS_OPTION_REAL}" | grep 'nixos-option\.nix$') + + 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() { @@ -1063,9 +1098,11 @@ function doHelp() { echo echo " edit Open the home configuration in \$VISUAL or \$EDITOR" echo - echo " option OPTION.NAME" + echo " option [--recursive] OPTION.NAME" echo " Inspect configuration option named OPTION.NAME." echo + echo " --recursive Print all the values at or below the option name recursively." + echo echo " build Build configuration into result directory" echo echo " init [--switch] [DIR]" @@ -1211,6 +1248,16 @@ while [[ $# -gt 0 ]]; do ;; esac ;; + --recursive) + case $COMMAND in + option) + COMMAND_ARGS+=("$opt") + ;; + *) + errTopLevelSubcommandOpt "--recursive" "option" + ;; + esac + ;; --option|--arg|--argstr) [[ -v 1 && $1 != -* ]] || errMissingOptArg "$opt" [[ -v 2 ]] || errMissingOptArg "$opt $1" diff --git a/modules/misc/news/2025/10/2025-10-31_12-00-00.nix b/modules/misc/news/2025/10/2025-10-31_12-00-00.nix new file mode 100644 index 000000000..179586a35 --- /dev/null +++ b/modules/misc/news/2025/10/2025-10-31_12-00-00.nix @@ -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. + ''; +} diff --git a/modules/misc/news/2025/11/2025-11-03_11-18-15.nix b/modules/misc/news/2025/11/2025-11-03_11-18-15.nix new file mode 100644 index 000000000..82d9dd5bb --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-03_11-18-15.nix @@ -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..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. + ''; +} diff --git a/modules/misc/news/2025/11/2025-11-03_20-33-15.nix b/modules/misc/news/2025/11/2025-11-03_20-33-15.nix new file mode 100644 index 000000000..b63b6d6e2 --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-03_20-33-15.nix @@ -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. + ''; +} diff --git a/modules/misc/news/2025/11/2025-11-03_22-56-50.nix b/modules/misc/news/2025/11/2025-11-03_22-56-50.nix new file mode 100644 index 000000000..d78de2292 --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-03_22-56-50.nix @@ -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 + ''; +} diff --git a/modules/misc/news/2025/11/2025-11-04_13-00-00.nix b/modules/misc/news/2025/11/2025-11-04_13-00-00.nix new file mode 100644 index 000000000..8ad7e13dd --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-04_13-00-00.nix @@ -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. + ''; +} diff --git a/modules/misc/tmpfiles.nix b/modules/misc/tmpfiles.nix index b0d4c681e..531e3cd87 100644 --- a/modules/misc/tmpfiles.nix +++ b/modules/misc/tmpfiles.nix @@ -9,46 +9,269 @@ let cfg = config.systemd.user.tmpfiles; -in -{ - meta.maintainers = [ lib.maintainers.dawidsowa ]; + 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. - options.systemd.user.tmpfiles.rules = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "L /home/user/Documents - - - - /mnt/data/Documents" ]; - description = '' - Rules for creating and cleaning up temporary files - automatically. See - {manpage}`tmpfiles.d(5)` - for the exact format. - ''; - }; + The type consists of a single letter and optionally one or more + modifier characters. - config = lib.mkIf (cfg.rules != [ ]) { - assertions = [ - (lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux) - ]; - - 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} + Please see the upstream documentation for the available types and + more details: {manpage}`tmpfiles.d(5)` ''; }; - "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"; + 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; [ + 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 = [ + (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 = [ ]; }; + }; + }) + + ]; } diff --git a/modules/programs/fish.nix b/modules/programs/fish.nix index 006fb53d1..49922ef50 100644 --- a/modules/programs/fish.nix +++ b/modules/programs/fish.nix @@ -261,6 +261,7 @@ let }; erase = mkEnableOption "remove bind"; silent = mkEnableOption "Operate silently"; + repaint = mkEnableOption "redraw prompt after command"; operate = mkOption { description = "Operate on preset bindings or user bindings"; type = @@ -324,6 +325,7 @@ let { silent, erase, + repaint, operate, mode, setsMode, @@ -344,7 +346,11 @@ let ]; 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 " " ( diff --git a/modules/programs/ghostty.nix b/modules/programs/ghostty.nix index 43f3d99fe..38cfadf1a 100644 --- a/modules/programs/ghostty.nix +++ b/modules/programs/ghostty.nix @@ -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 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 diff --git a/modules/programs/glab.nix b/modules/programs/glab.nix index 525456dc4..84d1817ea 100644 --- a/modules/programs/glab.nix +++ b/modules/programs/glab.nix @@ -36,14 +36,12 @@ in # Use `systemd-tmpfiles` since glab requires its configuration file to have # 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" - ]; + 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"; + }; + }; xdg.configFile."glab-cli/aliases.yml" = lib.mkIf (cfg.aliases != { }) { source = yaml.generate "glab-aliases" cfg.aliases; diff --git a/modules/programs/gpg.nix b/modules/programs/gpg.nix index e3a7a8e87..1978a1b68 100644 --- a/modules/programs/gpg.nix +++ b/modules/programs/gpg.nix @@ -125,7 +125,7 @@ let function importTrust() { local keyIds trust - IFS='\n' read -ra keyIds <<< "$(gpgKeyId "$1")" + mapfile -t keyIds <<< "$(gpgKeyId "$1")" trust="$2" for id in "''${keyIds[@]}" ; do { echo trust; echo "$trust"; (( trust == 5 )) && echo y; echo quit; } \ diff --git a/modules/programs/kitty.nix b/modules/programs/kitty.nix index b8f0157a0..96087ae78 100644 --- a/modules/programs/kitty.nix +++ b/modules/programs/kitty.nix @@ -42,6 +42,10 @@ let mkKeyValue = key: command: "map ${key} ${command}"; }; + toKittyMouseBindings = lib.generators.toKeyValue { + mkKeyValue = key: command: "mouse_map ${key} ${command}"; + }; + toKittyActionAliases = lib.generators.toKeyValue { 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 { type = types.attrsOf types.str; default = { }; @@ -316,7 +332,8 @@ in (mkOrder 540 (toKittyConfig cfg.settings)) (mkOrder 550 (toKittyActionAliases cfg.actionAliases)) (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" = { diff --git a/modules/programs/mcp.nix b/modules/programs/mcp.nix new file mode 100644 index 000000000..9fedbdf2e --- /dev/null +++ b/modules/programs/mcp.nix @@ -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; + } + ); + }; +} diff --git a/modules/programs/opencode.nix b/modules/programs/opencode.nix index 538cff2a3..8e74ca872 100644 --- a/modules/programs/opencode.nix +++ b/modules/programs/opencode.nix @@ -16,6 +16,35 @@ let cfg = config.programs.opencode; 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 { meta.maintainers = with lib.maintainers; [ delafthi ]; @@ -25,6 +54,20 @@ in 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 { inherit (jsonFormat) type; default = { }; @@ -147,7 +190,7 @@ in Custom themes for opencode. The attribute name becomes the theme filename, and the value is either: - 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. Set `programs.opencode.settings.theme` to enable the custom theme. See for the documentation. @@ -159,13 +202,21 @@ in home.packages = mkIf (cfg.package != null) [ cfg.package ]; xdg.configFile = { - "opencode/config.json" = mkIf (cfg.settings != { }) { - source = jsonFormat.generate "config.json" ( - { - "$schema" = "https://opencode.ai/config.json"; - } - // cfg.settings - ); + "opencode/config.json" = mkIf (cfg.settings != { } || transformedMcpServers != { }) { + 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"; + } + // mergedSettings + ); }; "opencode/AGENTS.md" = ( diff --git a/modules/programs/opkssh.nix b/modules/programs/opkssh.nix new file mode 100644 index 000000000..a9d43ea73 --- /dev/null +++ b/modules/programs/opkssh.nix @@ -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 . + ''; + }; + }; + + 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; + }; + + }; +} diff --git a/modules/programs/rclone.nix b/modules/programs/rclone.nix index 9e92609bb..40fd5790f 100644 --- a/modules/programs/rclone.nix +++ b/modules/programs/rclone.nix @@ -111,6 +111,23 @@ in options = { 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 { type = lib.types.str; default = null; @@ -348,12 +365,13 @@ in Environment = [ # fusermount/fusermount3 "PATH=/run/wrappers/bin" - ]; + ] + ++ lib.optional (mount.logLevel != null) "RCLONE_LOG_LEVEL=${mount.logLevel}"; + ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mount.mountPoint}"; ExecStart = lib.concatStringsSep " " [ (lib.getExe cfg.package) "mount" - "-vv" (lib.cli.toGNUCommandLineShell { } mount.options) "${remote-name}:${mount-path}" "${mount.mountPoint}" diff --git a/modules/programs/rio.nix b/modules/programs/rio.nix index 491727f85..b3a45f2d3 100644 --- a/modules/programs/rio.nix +++ b/modules/programs/rio.nix @@ -5,22 +5,34 @@ ... }: let + inherit (lib) + mkEnableOption + mkPackageOption + mkOption + mkIf + mkMerge + types + literalExpression + mapAttrs' + nameValuePair + ; + cfg = config.programs.rio; settingsFormat = pkgs.formats.toml { }; in { options.programs.rio = { - enable = lib.mkEnableOption null // { + enable = mkEnableOption null // { description = '' 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. ''; }; - package = lib.mkPackageOption pkgs "rio" { nullable = true; }; + package = mkPackageOption pkgs "rio" { nullable = true; }; - settings = lib.mkOption { + settings = mkOption { type = settingsFormat.type; default = { }; description = '' @@ -28,20 +40,50 @@ in 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 + for + supported values. + ''; + example = literalExpression '' + { + foobar.colors = { + background = "#282a36"; + green = "#50fa7b"; + dim-green = "#06572f"; + }; + } + ''; + }; }; meta.maintainers = [ lib.maintainers.otavio ]; - config = lib.mkIf cfg.enable ( - lib.mkMerge [ - { - home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; - } + config = mkIf cfg.enable (mkMerge [ + { + home.packages = mkIf (cfg.package != null) [ cfg.package ]; + } - # Only manage configuration if not empty - (lib.mkIf (cfg.settings != { }) { - xdg.configFile."rio/config.toml".source = - if lib.isPath cfg.settings then cfg.settings else settingsFormat.generate "rio.toml" cfg.settings; - }) - ] - ); + # Only manage configuration if not empty + (mkIf (cfg.settings != { }) { + xdg.configFile."rio/config.toml".source = + 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; + }) + ]); } diff --git a/modules/programs/superfile.nix b/modules/programs/superfile.nix index 4e7765c68..3eb21cfb3 100644 --- a/modules/programs/superfile.nix +++ b/modules/programs/superfile.nix @@ -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; + }; }; } diff --git a/modules/programs/taskwarrior.nix b/modules/programs/taskwarrior.nix index 9b5bdaa27..c7b8de1d2 100644 --- a/modules/programs/taskwarrior.nix +++ b/modules/programs/taskwarrior.nix @@ -87,7 +87,7 @@ in ''; }; - package = lib.mkPackageOption pkgs "taskwarrior" { + package = lib.mkPackageOption pkgs "taskwarrior2" { nullable = true; example = "pkgs.taskwarrior3"; }; diff --git a/modules/programs/vicinae.nix b/modules/programs/vicinae.nix new file mode 100644 index 000000000..bde52d4eb --- /dev/null +++ b/modules/programs/vicinae.nix @@ -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 ]; + }; + }; + }; +} diff --git a/modules/programs/vscode/default.nix b/modules/programs/vscode/default.nix index 4eba83784..fb3064beb 100644 --- a/modules/programs/vscode/default.nix +++ b/modules/programs/vscode/default.nix @@ -114,6 +114,33 @@ let 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 { options = { 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..userMcp`. + + Note: Settings defined in {option}`programs.mcp.servers` are merged + with {option}`programs.vscode.profiles..userMcp`, with VSCode + settings taking precedence. + ''; + }; + userMcp = mkOption { type = types.either types.path jsonFormat.type; default = { }; @@ -418,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 @@ -459,10 +500,31 @@ in if isPath v.userTasks then v.userTasks else jsonFormat.generate "vscode-user-tasks" v.userTasks; }) - (mkIf (v.userMcp != { }) { - "${mcpFilePath n}".source = - if isPath v.userMcp then v.userMcp else jsonFormat.generate "vscode-user-mcp" v.userMcp; - }) + (mkIf + ( + v.userMcp != { } + || (v.enableMcpIntegration && config.programs.mcp.enable && config.programs.mcp.servers != { }) + ) + { + "${mcpFilePath n}".source = + 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 != [ ]) { "${keybindingsFilePath n}".source = diff --git a/modules/programs/yazi.nix b/modules/programs/yazi.nix index c945447cd..6d8dbe0fb 100644 --- a/modules/programs/yazi.nix +++ b/modules/programs/yazi.nix @@ -221,7 +221,7 @@ in function ${cfg.shellWrapperName}() { local tmp="$(mktemp -t "yazi-cwd.XXXXX")" yazi "$@" --cwd-file="$tmp" - if cwd="$(cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then + if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then builtin cd -- "$cwd" fi rm -f -- "$tmp" @@ -231,7 +231,7 @@ in fishIntegration = '' set -l tmp (mktemp -t "yazi-cwd.XXXXX") 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" end rm -f -- "$tmp" diff --git a/modules/programs/zed-editor.nix b/modules/programs/zed-editor.nix index 5b6fc4ff3..d95f59e88 100644 --- a/modules/programs/zed-editor.nix +++ b/modules/programs/zed-editor.nix @@ -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 { type = jsonFormat.type; 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 { type = types.listOf types.str; default = [ ]; @@ -241,6 +271,14 @@ in (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 [ @@ -265,6 +303,9 @@ in (mkIf (!cfg.mutableUserTasks && 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 = [ diff --git a/modules/services/cbatticon.nix b/modules/services/cbatticon.nix index c441f1799..70daa9fc3 100644 --- a/modules/services/cbatticon.nix +++ b/modules/services/cbatticon.nix @@ -42,7 +42,13 @@ in services.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 { type = types.nullOr types.lines; diff --git a/modules/services/gpg-agent.nix b/modules/services/gpg-agent.nix index d4c27e91f..e2ef852f2 100644 --- a/modules/services/gpg-agent.nix +++ b/modules/services/gpg-agent.nix @@ -19,25 +19,29 @@ let inherit (config.programs.gpg) homedir; - gpgSshSupportStr = '' - ${gpgPkg}/bin/gpg-connect-agent --quiet updatestartuptty /bye - ''; + gpgSshSupportStr = "${gpgPkg}/bin/gpg-connect-agent --quiet updatestartuptty /bye"; gpgBashInitStr = '' GPG_TTY="$(tty)" export GPG_TTY '' - + optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + + optionalString cfg.enableSshSupport '' + ${gpgSshSupportStr} > /dev/null + ''; gpgZshInitStr = '' export GPG_TTY=$TTY '' - + optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + + optionalString cfg.enableSshSupport '' + ${gpgSshSupportStr} > /dev/null + ''; gpgFishInitStr = '' set -gx GPG_TTY (tty) '' - + optionalString cfg.enableSshSupport "${gpgSshSupportStr} > /dev/null"; + + optionalString cfg.enableSshSupport '' + ${gpgSshSupportStr} > /dev/null + ''; gpgNushellInitStr = '' $env.GPG_TTY = (tty) diff --git a/modules/services/local-ai.nix b/modules/services/local-ai.nix new file mode 100644 index 000000000..a95cfdfbd --- /dev/null +++ b/modules/services/local-ai.nix @@ -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 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" ]; + }; + }; + }; +} diff --git a/modules/services/taskwarrior-sync.nix b/modules/services/taskwarrior-sync.nix index 959019b87..ab4fc440c 100644 --- a/modules/services/taskwarrior-sync.nix +++ b/modules/services/taskwarrior-sync.nix @@ -19,7 +19,7 @@ in options.services.taskwarrior-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 { type = lib.types.str; diff --git a/tests/darwinScrublist.nix b/tests/darwinScrublist.nix index 1287f6812..ff23a728f 100644 --- a/tests/darwinScrublist.nix +++ b/tests/darwinScrublist.nix @@ -157,7 +157,7 @@ let "spotify-player" "starship" "superfile" - "taskwarrior" + "taskwarrior2" "tealdeer" "texlive" "thefuck" diff --git a/tests/default.nix b/tests/default.nix index 88ff40ece..735e1f246 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -203,6 +203,7 @@ import nmtSrc { ./modules/misc/numlock ./modules/misc/pam ./modules/misc/qt + ./modules/misc/tmpfiles ./modules/misc/xdg/linux.nix ./modules/misc/xsession ./modules/systemd diff --git a/tests/integration/standalone/rclone/sops-nix.nix b/tests/integration/standalone/rclone/sops-nix.nix index 71283edf5..fc676505e 100644 --- a/tests/integration/standalone/rclone/sops-nix.nix +++ b/tests/integration/standalone/rclone/sops-nix.nix @@ -28,9 +28,12 @@ in uid = 1000; }; - systemd.tmpfiles.rules = [ - "f /home/alice/age-key 400 alice users - ${ageKey}" - ]; + systemd.tmpfiles.settings.age.rules."/home/alice/age-key".f = { + mode = "400"; + user = "alice"; + group = "users"; + argument = ageKey; + }; home-manager.users.alice = { config, ... }: diff --git a/tests/integration/standalone/standard-basics.nix b/tests/integration/standalone/standard-basics.nix index ed565e18c..1a47aad5e 100644 --- a/tests/integration/standalone/standard-basics.nix +++ b/tests/integration/standalone/standard-basics.nix @@ -107,6 +107,12 @@ assert expected in 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"): succeed_as_alice("yes | home-manager uninstall -L") diff --git a/tests/modules/misc/gtk/gtk-global-inheritance.nix b/tests/modules/misc/gtk/gtk-global-inheritance.nix index 3d01f4843..d4c88af9c 100644 --- a/tests/modules/misc/gtk/gtk-global-inheritance.nix +++ b/tests/modules/misc/gtk/gtk-global-inheritance.nix @@ -5,7 +5,7 @@ font = { name = "Ubuntu"; size = 12; - package = pkgs.ubuntu_font_family; + package = pkgs.ubuntu-classic; }; theme = { name = "Adwaita-dark"; diff --git a/tests/modules/misc/tmpfiles/basic-rules.nix b/tests/modules/misc/tmpfiles/basic-rules.nix new file mode 100644 index 000000000..68b00f87b --- /dev/null +++ b/tests/modules/misc/tmpfiles/basic-rules.nix @@ -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$" + ''; +} diff --git a/tests/modules/misc/tmpfiles/common-stubs.nix b/tests/modules/misc/tmpfiles/common-stubs.nix new file mode 100644 index 000000000..abc4c732d --- /dev/null +++ b/tests/modules/misc/tmpfiles/common-stubs.nix @@ -0,0 +1,3 @@ +{ + test.stubs.systemd.outPath = null; +} diff --git a/tests/modules/misc/tmpfiles/default.nix b/tests/modules/misc/tmpfiles/default.nix new file mode 100644 index 000000000..608796fe9 --- /dev/null +++ b/tests/modules/misc/tmpfiles/default.nix @@ -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; +} diff --git a/tests/modules/misc/tmpfiles/escaped-argument-warning.nix b/tests/modules/misc/tmpfiles/escaped-argument-warning.nix new file mode 100644 index 000000000..0a7441b7e --- /dev/null +++ b/tests/modules/misc/tmpfiles/escaped-argument-warning.nix @@ -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" + '' + ]; +} diff --git a/tests/modules/misc/tmpfiles/no-rules.nix b/tests/modules/misc/tmpfiles/no-rules.nix new file mode 100644 index 000000000..8c8f0e77e --- /dev/null +++ b/tests/modules/misc/tmpfiles/no-rules.nix @@ -0,0 +1,9 @@ +{ + imports = [ ./common-stubs.nix ]; + + systemd.user.tmpfiles.settings = { }; + + nmt.script = '' + assertPathNotExists home-files/.config/user-tmpfiles.d/ + ''; +} diff --git a/tests/modules/misc/tmpfiles/rules-with-purging.nix b/tests/modules/misc/tmpfiles/rules-with-purging.nix new file mode 100644 index 000000000..a453bfcb6 --- /dev/null +++ b/tests/modules/misc/tmpfiles/rules-with-purging.nix @@ -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$" + ''; +} diff --git a/tests/modules/programs/firefox/profiles/overwrite/default.nix b/tests/modules/programs/firefox/profiles/overwrite/default.nix index 54014bea2..1dcfa35f9 100644 --- a/tests/modules/programs/firefox/profiles/overwrite/default.nix +++ b/tests/modules/programs/firefox/profiles/overwrite/default.nix @@ -39,7 +39,7 @@ in nmt.script = let binPath = - if pkgs.hostPlatform.isDarwin then + if pkgs.stdenv.hostPlatform.isDarwin then "Applications/${cfg.darwinAppName}.app/Contents/MacOS" else "bin"; diff --git a/tests/modules/programs/firefox/profiles/settings/default.nix b/tests/modules/programs/firefox/profiles/settings/default.nix index 9aa8af6ac..e48fa14f1 100644 --- a/tests/modules/programs/firefox/profiles/settings/default.nix +++ b/tests/modules/programs/firefox/profiles/settings/default.nix @@ -39,7 +39,7 @@ in nmt.script = let binPath = - if pkgs.hostPlatform.isDarwin then + if pkgs.stdenv.hostPlatform.isDarwin then "Applications/${cfg.darwinAppName}.app/Contents/MacOS" else "bin"; diff --git a/tests/modules/programs/firefox/profiles/userchrome/default.nix b/tests/modules/programs/firefox/profiles/userchrome/default.nix index 17ff8ddc8..f1b01c642 100644 --- a/tests/modules/programs/firefox/profiles/userchrome/default.nix +++ b/tests/modules/programs/firefox/profiles/userchrome/default.nix @@ -37,7 +37,7 @@ in nmt.script = let binPath = - if pkgs.hostPlatform.isDarwin then + if pkgs.stdenv.hostPlatform.isDarwin then "Applications/${cfg.darwinAppName}.app/Contents/MacOS" else "bin"; diff --git a/tests/modules/programs/firefox/state-version-19_09.nix b/tests/modules/programs/firefox/state-version-19_09.nix index cabb8c284..3da8f1374 100644 --- a/tests/modules/programs/firefox/state-version-19_09.nix +++ b/tests/modules/programs/firefox/state-version-19_09.nix @@ -13,7 +13,7 @@ in { 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"; } diff --git a/tests/modules/programs/ghostty/empty-settings.nix b/tests/modules/programs/ghostty/empty-settings.nix index 1ea025eb5..d74972a4a 100644 --- a/tests/modules/programs/ghostty/empty-settings.nix +++ b/tests/modules/programs/ghostty/empty-settings.nix @@ -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 diff --git a/tests/modules/programs/ghostty/example-settings.nix b/tests/modules/programs/ghostty/example-settings.nix index 18b64b4fe..b45094d2f 100644 --- a/tests/modules/programs/ghostty/example-settings.nix +++ b/tests/modules/programs/ghostty/example-settings.nix @@ -2,7 +2,7 @@ { programs.ghostty = { enable = true; - package = config.lib.test.mkStubPackage { }; + package = config.lib.test.mkStubPackage { outPath = null; }; settings = { theme = "catppuccin-mocha"; diff --git a/tests/modules/programs/ghostty/example-theme.nix b/tests/modules/programs/ghostty/example-theme.nix index 29f40a897..320b073a1 100644 --- a/tests/modules/programs/ghostty/example-theme.nix +++ b/tests/modules/programs/ghostty/example-theme.nix @@ -1,6 +1,8 @@ +{ config, ... }: { programs.ghostty = { enable = true; + package = config.lib.test.mkStubPackage { outPath = null; }; themes = { catppuccin-mocha = { diff --git a/tests/modules/programs/gpg/default.nix b/tests/modules/programs/gpg/default.nix index a3949b186..40ed739ec 100644 --- a/tests/modules/programs/gpg/default.nix +++ b/tests/modules/programs/gpg/default.nix @@ -1,5 +1,6 @@ { gpg-immutable-keyfiles = ./immutable-keyfiles.nix; gpg-mutable-keyfiles = ./mutable-keyfiles.nix; + gpg-multiple-keys-trust = ./multiple-keys-trust.nix; gpg-override-defaults = ./override-defaults.nix; } diff --git a/tests/modules/programs/gpg/multiple-keys-trust.nix b/tests/modules/programs/gpg/multiple-keys-trust.nix new file mode 100644 index 000000000..966443cf6 --- /dev/null +++ b/tests/modules/programs/gpg/multiple-keys-trust.nix @@ -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:$' + ''; +} diff --git a/tests/modules/programs/gpg/test-keys/multiple-keys.asc b/tests/modules/programs/gpg/test-keys/multiple-keys.asc new file mode 100644 index 000000000..c8b9c6937 --- /dev/null +++ b/tests/modules/programs/gpg/test-keys/multiple-keys.asc @@ -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----- diff --git a/tests/modules/programs/kitty/example-mkOrder-expected.conf b/tests/modules/programs/kitty/example-mkOrder-expected.conf index 76ee85246..111b93aaa 100644 --- a/tests/modules/programs/kitty/example-mkOrder-expected.conf +++ b/tests/modules/programs/kitty/example-mkOrder-expected.conf @@ -19,5 +19,8 @@ action_alias launch_window launch --cwd=current --type=os-window map ctrl+c copy_or_interrupt 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 diff --git a/tests/modules/programs/kitty/example-mkOrder.nix b/tests/modules/programs/kitty/example-mkOrder.nix index acc58287f..53dbd468b 100644 --- a/tests/modules/programs/kitty/example-mkOrder.nix +++ b/tests/modules/programs/kitty/example-mkOrder.nix @@ -25,6 +25,11 @@ "ctrl+f>2" = "set_font_size 20"; }; + mouseBindings = { + "ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt"; + "left click" = "ungrabbed no-op"; + }; + actionAliases = { "launch_tab" = "launch --cwd=current --type=tab"; "launch_window" = "launch --cwd=current --type=os-window"; diff --git a/tests/modules/programs/kitty/example-settings-expected.conf b/tests/modules/programs/kitty/example-settings-expected.conf index 0a6afc926..0a039ac82 100644 --- a/tests/modules/programs/kitty/example-settings-expected.conf +++ b/tests/modules/programs/kitty/example-settings-expected.conf @@ -17,5 +17,8 @@ action_alias launch_window launch --cwd=current --type=os-window map ctrl+c copy_or_interrupt 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 diff --git a/tests/modules/programs/kitty/example-settings.nix b/tests/modules/programs/kitty/example-settings.nix index 484dacda6..5383f2525 100644 --- a/tests/modules/programs/kitty/example-settings.nix +++ b/tests/modules/programs/kitty/example-settings.nix @@ -25,6 +25,11 @@ "ctrl+f>2" = "set_font_size 20"; }; + mouseBindings = { + "ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt"; + "left click" = "ungrabbed no-op"; + }; + actionAliases = { "launch_tab" = "launch --cwd=current --type=tab"; "launch_window" = "launch --cwd=current --type=os-window"; diff --git a/tests/modules/programs/kitty/null-shellIntegration-expected.conf b/tests/modules/programs/kitty/null-shellIntegration-expected.conf index fedba1157..c1bcc986a 100644 --- a/tests/modules/programs/kitty/null-shellIntegration-expected.conf +++ b/tests/modules/programs/kitty/null-shellIntegration-expected.conf @@ -4,3 +4,4 @@ + diff --git a/tests/modules/programs/mcp/default.nix b/tests/modules/programs/mcp/default.nix new file mode 100644 index 000000000..2a10a18b8 --- /dev/null +++ b/tests/modules/programs/mcp/default.nix @@ -0,0 +1,4 @@ +{ + mcp-servers = ./servers.nix; + mcp-empty-servers = ./empty-servers.nix; +} diff --git a/tests/modules/programs/mcp/empty-servers.nix b/tests/modules/programs/mcp/empty-servers.nix new file mode 100644 index 000000000..63c72c5c0 --- /dev/null +++ b/tests/modules/programs/mcp/empty-servers.nix @@ -0,0 +1,9 @@ +{ + programs.mcp = { + enable = true; + servers = { }; + }; + nmt.script = '' + assertPathNotExists home-files/.config/mcp/mcp.json + ''; +} diff --git a/tests/modules/programs/mcp/mcp.json b/tests/modules/programs/mcp/mcp.json new file mode 100644 index 000000000..9e2571d5e --- /dev/null +++ b/tests/modules/programs/mcp/mcp.json @@ -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" + } + } +} diff --git a/tests/modules/programs/mcp/servers.nix b/tests/modules/programs/mcp/servers.nix new file mode 100644 index 000000000..a63e0f4ae --- /dev/null +++ b/tests/modules/programs/mcp/servers.nix @@ -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} + ''; +} diff --git a/tests/modules/programs/opencode/default.nix b/tests/modules/programs/opencode/default.nix index 62d780d5d..74bfc6a8b 100644 --- a/tests/modules/programs/opencode/default.nix +++ b/tests/modules/programs/opencode/default.nix @@ -11,4 +11,6 @@ opencode-mixed-content = ./mixed-content.nix; opencode-themes-inline = ./themes-inline.nix; opencode-themes-path = ./themes-path.nix; + opencode-mcp-integration = ./mcp-integration.nix; + opencode-mcp-integration-with-override = ./mcp-integration-with-override.nix; } diff --git a/tests/modules/programs/opencode/mcp-integration-with-override.json b/tests/modules/programs/opencode/mcp-integration-with-override.json new file mode 100644 index 000000000..46df26ad1 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration-with-override.json @@ -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" +} diff --git a/tests/modules/programs/opencode/mcp-integration-with-override.nix b/tests/modules/programs/opencode/mcp-integration-with-override.nix new file mode 100644 index 000000000..c66133b83 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration-with-override.nix @@ -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} + ''; +} diff --git a/tests/modules/programs/opencode/mcp-integration.json b/tests/modules/programs/opencode/mcp-integration.json new file mode 100644 index 000000000..ddd917af9 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration.json @@ -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" + } + } +} diff --git a/tests/modules/programs/opencode/mcp-integration.nix b/tests/modules/programs/opencode/mcp-integration.nix new file mode 100644 index 000000000..f3d0f1808 --- /dev/null +++ b/tests/modules/programs/opencode/mcp-integration.nix @@ -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} + ''; +} diff --git a/tests/modules/programs/opkssh/default.nix b/tests/modules/programs/opkssh/default.nix new file mode 100644 index 000000000..9c6be4912 --- /dev/null +++ b/tests/modules/programs/opkssh/default.nix @@ -0,0 +1,3 @@ +{ + opkssh-basic-config = ./opkssh-basic-config.nix; +} diff --git a/tests/modules/programs/opkssh/opkssh-basic-config.nix b/tests/modules/programs/opkssh/opkssh-basic-config.nix new file mode 100644 index 000000000..a15263b47 --- /dev/null +++ b/tests/modules/programs/opkssh/opkssh-basic-config.nix @@ -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 + ''} + ''; +} diff --git a/tests/modules/programs/rio/default.nix b/tests/modules/programs/rio/default.nix index a6aba7fe1..461c51605 100644 --- a/tests/modules/programs/rio/default.nix +++ b/tests/modules/programs/rio/default.nix @@ -1,4 +1,5 @@ { rio-example-settings = ./example-settings.nix; rio-empty-settings = ./empty-settings.nix; + rio-themes = ./example-themes.nix; } diff --git a/tests/modules/programs/rio/empty-settings.nix b/tests/modules/programs/rio/empty-settings.nix index e89374d1f..07fc296bd 100644 --- a/tests/modules/programs/rio/empty-settings.nix +++ b/tests/modules/programs/rio/empty-settings.nix @@ -1,5 +1,7 @@ +{ config, ... }: { programs.rio.enable = true; + programs.rio.package = config.lib.test.mkStubPackage { }; nmt.script = '' assertPathNotExists home-files/.config/rio diff --git a/tests/modules/programs/rio/example-themes.nix b/tests/modules/programs/rio/example-themes.nix new file mode 100644 index 000000000..28a25d199 --- /dev/null +++ b/tests/modules/programs/rio/example-themes.nix @@ -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} + ''; +} diff --git a/tests/modules/programs/rio/foobar.toml b/tests/modules/programs/rio/foobar.toml new file mode 100644 index 000000000..ab93034f8 --- /dev/null +++ b/tests/modules/programs/rio/foobar.toml @@ -0,0 +1,4 @@ +[colors] +background = "#282a36" +cyan = "#8be9fd" +green = "#50fa7b" diff --git a/tests/modules/programs/superfile/example-pinned-folders.json b/tests/modules/programs/superfile/example-pinned-folders.json new file mode 100644 index 000000000..e7ec94643 --- /dev/null +++ b/tests/modules/programs/superfile/example-pinned-folders.json @@ -0,0 +1,6 @@ +[ + { + "location": "/nix/store", + "name": "Nix Store" + } +] diff --git a/tests/modules/programs/superfile/example-settings.nix b/tests/modules/programs/superfile/example-settings.nix index 8f682b4d2..5b2c96831 100644 --- a/tests/modules/programs/superfile/example-settings.nix +++ b/tests/modules/programs/superfile/example-settings.nix @@ -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} ''; } diff --git a/tests/modules/programs/vicinae/default.nix b/tests/modules/programs/vicinae/default.nix new file mode 100644 index 000000000..4a4736323 --- /dev/null +++ b/tests/modules/programs/vicinae/default.nix @@ -0,0 +1,4 @@ +{ lib, pkgs, ... }: +lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux) { + vicinae-example-settings = ./example-settings.nix; +} diff --git a/tests/modules/programs/vicinae/example-settings.nix b/tests/modules/programs/vicinae/example-settings.nix new file mode 100644 index 000000000..b9de5f24f --- /dev/null +++ b/tests/modules/programs/vicinae/example-settings.nix @@ -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" + ''; +} diff --git a/tests/modules/programs/vscode/default.nix b/tests/modules/programs/vscode/default.nix index 4db8b2c8a..a62521746 100644 --- a/tests/modules/programs/vscode/default.nix +++ b/tests/modules/programs/vscode/default.nix @@ -24,6 +24,8 @@ let keybindings = import ./keybindings.nix; tasks = import ./tasks.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; snippets = import ./snippets.nix; }; diff --git a/tests/modules/programs/vscode/mcp-integration-default.json b/tests/modules/programs/vscode/mcp-integration-default.json new file mode 100644 index 000000000..66e3dfd42 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-default.json @@ -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" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-test.json b/tests/modules/programs/vscode/mcp-integration-test.json new file mode 100644 index 000000000..4f35aeaf1 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-test.json @@ -0,0 +1,7 @@ +{ + "servers": { + "Github": { + "url": "https://api.githubcopilot.com/mcp/" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-with-override.json b/tests/modules/programs/vscode/mcp-integration-with-override.json new file mode 100644 index 000000000..10772d0bf --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-with-override.json @@ -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" + } + } +} diff --git a/tests/modules/programs/vscode/mcp-integration-with-override.nix b/tests/modules/programs/vscode/mcp-integration-with-override.nix new file mode 100644 index 000000000..a1d7371f0 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration-with-override.nix @@ -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} + ''; +} diff --git a/tests/modules/programs/vscode/mcp-integration.nix b/tests/modules/programs/vscode/mcp-integration.nix new file mode 100644 index 000000000..e40056747 --- /dev/null +++ b/tests/modules/programs/vscode/mcp-integration.nix @@ -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} + ''; +} diff --git a/tests/modules/programs/yazi/bash-integration-enabled.nix b/tests/modules/programs/yazi/bash-integration-enabled.nix index 1bebbccad..41f56d765 100644 --- a/tests/modules/programs/yazi/bash-integration-enabled.nix +++ b/tests/modules/programs/yazi/bash-integration-enabled.nix @@ -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; @@ -19,6 +7,12 @@ in }; 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"' ''; } diff --git a/tests/modules/programs/yazi/fish-integration-expected.fish b/tests/modules/programs/yazi/fish-integration-expected.fish index d8f11f284..d0019d999 100644 --- a/tests/modules/programs/yazi/fish-integration-expected.fish +++ b/tests/modules/programs/yazi/fish-integration-expected.fish @@ -1,7 +1,7 @@ function yy set -l tmp (mktemp -t "yazi-cwd.XXXXX") 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" end rm -f -- "$tmp" diff --git a/tests/modules/programs/yazi/zsh-integration-enabled.nix b/tests/modules/programs/yazi/zsh-integration-enabled.nix index ef8e0ccc8..6087810c5 100644 --- a/tests/modules/programs/yazi/zsh-integration-enabled.nix +++ b/tests/modules/programs/yazi/zsh-integration-enabled.nix @@ -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.yazi = { enable = true; - enableBashIntegration = true; + enableZshIntegration = true; }; 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"' ''; } diff --git a/tests/modules/programs/zed-editor/debug-empty.nix b/tests/modules/programs/zed-editor/debug-empty.nix new file mode 100644 index 000000000..eed8e7354 --- /dev/null +++ b/tests/modules/programs/zed-editor/debug-empty.nix @@ -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}" + ''; +} diff --git a/tests/modules/programs/zed-editor/debug-immutable.nix b/tests/modules/programs/zed-editor/debug-immutable.nix new file mode 100644 index 000000000..f433d9128 --- /dev/null +++ b/tests/modules/programs/zed-editor/debug-immutable.nix @@ -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}" + ''; +} diff --git a/tests/modules/programs/zed-editor/debug.nix b/tests/modules/programs/zed-editor/debug.nix new file mode 100644 index 000000000..b49fb85d3 --- /dev/null +++ b/tests/modules/programs/zed-editor/debug.nix @@ -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}" + ''; +} diff --git a/tests/modules/programs/zed-editor/default.nix b/tests/modules/programs/zed-editor/default.nix index 1029dba99..b7717320c 100644 --- a/tests/modules/programs/zed-editor/default.nix +++ b/tests/modules/programs/zed-editor/default.nix @@ -11,5 +11,8 @@ zed-tasks = ./tasks.nix; zed-tasks-immutable = ./tasks-immutable.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; } diff --git a/tests/modules/services/darkman/basic-configuration.nix b/tests/modules/services/darkman/basic-configuration.nix index dbc89ac94..00a0fad0a 100644 --- a/tests/modules/services/darkman/basic-configuration.nix +++ b/tests/modules/services/darkman/basic-configuration.nix @@ -17,7 +17,7 @@ ''; lightModeScripts.color-scheme-light = pkgs.writeScript "my-python-script" '' - #!${pkgs.python}/bin/python + #!${pkgs.python2}/bin/python print('Do something!') ''; diff --git a/tests/modules/services/local-ai/default.nix b/tests/modules/services/local-ai/default.nix new file mode 100644 index 000000000..bc67d7482 --- /dev/null +++ b/tests/modules/services/local-ai/default.nix @@ -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; +} diff --git a/tests/modules/services/local-ai/enabled-with-environment.nix b/tests/modules/services/local-ai/enabled-with-environment.nix new file mode 100644 index 000000000..ed744fd90 --- /dev/null +++ b/tests/modules/services/local-ai/enabled-with-environment.nix @@ -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\" }]" + ''; +} diff --git a/tests/modules/services/local-ai/enabled.nix b/tests/modules/services/local-ai/enabled.nix new file mode 100644 index 000000000..1d1100ac6 --- /dev/null +++ b/tests/modules/services/local-ai/enabled.nix @@ -0,0 +1,7 @@ +{ + services.local-ai.enable = true; + + nmt.script = '' + assertFileExists home-files/.config/systemd/user/local-ai.service + ''; +}