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/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/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..5c89da6e5 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 = { }; @@ -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/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/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; }