diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4cec4f3..97c8c147 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,10 +29,18 @@ jobs: fi - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main + uses: cachix/install-nix-action@v31 - name: Setup Nix cache - uses: DeterminateSystems/magic-nix-cache-action@main + uses: nix-community/cache-nix-action@v7 + with: + primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} + restore-prefixes-first-match: nix-${{ runner.os }}- + # don't keep caches from previous CI runs around. newest main should always be enough + purge: true + purge-prefixes: nix-${{ runner.os }}- + purge-created: 0 + purge-primary-key: never - name: Run checks run: nix flake check --log-format bar-with-logs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f3b67efa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## Unreleased + +### Breaking changes + +- `wrapPackage`: when passing explicit `args`, `"$@"` is no longer + appended automatically by the wrapper template. If you pass custom + `args` and want passthrough, include `"$@"` in your args list. + The default `args` (generated from `flags`) still includes `"$@"`. + +- `flagSeparator` default changed from `" "` to `null`. The old `" "` + default was misleading: it produced separate argv entries, not a + space-joined arg. `null` now means separate argv entries. If you + were explicitly passing `flagSeparator = " "` to get separate args, + remove it (or change to `null`). + +### Added + +- `lib/modules/command.nix`: base module with shared command spec + (args, env, hooks, exePath) used by both wrapper and systemd outputs. +- `lib/modules/flags.nix`: flags module with per-flag ordering via + `{ value, order }` submodules. Default order is 1000. Reading + `config.flags` returns clean values (order is transparent). +- `wrapper.nix` injects `"$@"` into args at order 1001, controllable + via the ordering system. +- `outputs.wrapper` as the canonical output path (config.wrapper is + a backward-compatible alias). diff --git a/README.md b/README.md index 3881ecef..bde5a531 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This library provides two main components: vo=gpu hwdec=auto ''; - "mpv.input".content = '' + "input.conf".content = '' WHEEL_UP seek 10 WHEEL_DOWN seek -10 ''; @@ -140,12 +140,14 @@ Arguments: - Example: `[ "--silent" "--connect-timeout" "30" ]` - If provided, overrides automatic generation from `flags` - `preHook`: Shell script executed before the command (default: `""`) +- `postHook`: Shell script executed after the command. This will leave a bash process running, use with caution (default: `""`) - `passthru`: Additional attributes for the derivation's passthru (default: `{}`) - `aliases`: List of additional symlink names for the executable (default: `[]`) - `filesToPatch`: List of file paths (glob patterns) relative to package root to patch for self-references (default: `["share/applications/*.desktop"]`) - Example: `["bin/*", "lib/*.sh"]` to replace original package paths with wrapped package paths - Desktop files are patched by default to update Exec= and Icon= paths - `filesToExclude`: List of file paths (glob patterns) to exclude from the wrapped package (default: `[]`) +- `patchHook`: Shell script that runs after patchPhase to modify the wrapper package files (default: `""`) - `wrapper`: Custom wrapper function (optional, overrides default exec wrapper) The function: @@ -170,12 +172,18 @@ Built-in options (always available): - `flagSeparator`: Separator between flag name and value (default: `" "`) - `args`: Command-line arguments list (auto-generated from `flags` if not provided) - `env`: Environment variables +- `preHook`: Shell script executed before the command (default: `""`) +- `postHook`: Shell script executed after the command. This will leave a bash process running, use with caution (default: `""`) - `passthru`: Additional passthru attributes - `filesToPatch`: List of file paths (glob patterns) to patch for self-references (default: `["share/applications/*.desktop"]`) - `filesToExclude`: List of file paths (glob patterns) to exclude from the wrapped package (default: `[]`) +- `patchHook`: Shell script that runs after patchPhase to modify the wrapper package files (default `""`) - `wrapper`: The resulting wrapped package (read-only, auto-generated from other options) - `apply`: Function to extend the configuration with additional modules (read-only) +Optional modules (import via `wlib.modules.`): +- `systemd`: Generates systemd service files (user and/or system), options are passed through from NixOS + Custom types: - `wlib.types.file`: File type with `content` and `path` options - `content`: File contents as string @@ -231,7 +239,7 @@ Wraps mpv with configuration file support and script management: vo=gpu profile=gpu-hq ''; - "mpv.input".content = '' + "input.conf".content = '' RIGHT seek 5 LEFT seek -5 ''; @@ -261,6 +269,105 @@ Wraps notmuch with INI-based configuration: }).wrapper ``` +### Generating systemd Services + +Import `wlib.modules.systemd` to generate systemd service files for your wrapper. +The options under `systemd` are the same as `systemd.services.` in NixOS, +passed through directly. + +`ExecStart` (including args), `Environment`, `PATH`, `preStart` and `postStop` +are picked up from the wrapper automatically, so you only need to set what's +specific to the service. + +The same config produces both a user and system service file, available at +`config.outputs.systemd-user` and `config.outputs.systemd-system`. Use +whichever fits your deployment. + +```nix +wlib.wrapModule ({ config, wlib, ... }: { + imports = [ wlib.modules.systemd ]; + + config = { + package = config.pkgs.hello; + flags."--greeting" = "world"; + env.HELLO_LANG = "en"; + systemd = { + description = "Hello service"; + serviceConfig.Type = "simple"; + serviceConfig.Restart = "on-failure"; + }; + }; +}) +``` + +Settings merge when using `apply`: + +```nix +extended = myWrapper.apply { + systemd.serviceConfig.Restart = "always"; + systemd.environment.EXTRA = "value"; +}; +``` + +#### Using in NixOS + +You need both `systemd.packages` for the unit file and the corresponding +`wantedBy` to actually activate it. NixOS does not read the `[Install]` section +from unit files, it creates the `.wants` symlinks from the module option instead. + +As a user service (for all users): + +```nix +# configuration.nix +{ pkgs, wrappers, ... }: +let + myHello = wrappers.wrapperModules.hello.apply { + inherit pkgs; + systemd.serviceConfig.Restart = "always"; + }; +in { + systemd.packages = [ myHello.outputs.systemd-user ]; + # NixOS needs this to create the .wants symlink, the [Install] + # section in the unit file alone is not enough + systemd.user.services.hello.wantedBy = [ "default.target" ]; +} +``` + +As a system service: + +```nix +# configuration.nix +{ pkgs, wrappers, ... }: +let + myHello = wrappers.wrapperModules.hello.apply { + inherit pkgs; + systemd.serviceConfig.Restart = "always"; + }; +in { + systemd.packages = [ myHello.outputs.systemd-system ]; + systemd.services.hello.wantedBy = [ "multi-user.target" ]; +} +``` + +#### Using in home-manager + +For per-user services, link via `xdg.dataFile`: + +```nix +# home.nix +{ pkgs, wrappers, ... }: +let + myHello = wrappers.wrapperModules.hello.apply { + inherit pkgs; + systemd.wantedBy = [ "default.target" ]; + systemd.serviceConfig.Restart = "always"; + }; +in { + xdg.dataFile."systemd/user/hello.service".source = + "${myHello.outputs.systemd-user}/systemd/user/hello.service"; +} +``` + ## alternatives - [wrapper-manager](https://github.com/viperML/wrapper-manager) by viperML. This project focuses more on a single module system, configuring wrappers and exporting them. This was an inspiration when building this library, but I wanted to have a more granular approach with a single module per package and a collection of community made modules. diff --git a/checks/exe-path-bin-name.nix b/checks/exe-path-bin-name.nix index e11a4253..b1299e3c 100644 --- a/checks/exe-path-bin-name.nix +++ b/checks/exe-path-bin-name.nix @@ -38,6 +38,13 @@ let }; }; + # Test 5: Custom binName updates meta.mainProgram + wrappedWithMeta = self.lib.wrapPackage { + inherit pkgs; + package = pkgs.hello; + binName = "hello-wrapped"; + }; + in pkgs.runCommand "exe-path-bin-name-test" { } '' set -e @@ -124,6 +131,15 @@ pkgs.runCommand "exe-path-bin-name-test" { } '' exit 1 fi + # Test 5: Custom binName updates meta.mainProgram + echo -e "\n=== Test 5: custom binName meta.mainProgram ===" + if [ "${wrappedWithMeta.meta.mainProgram}" = "hello-wrapped" ]; then + echo "PASS: meta.mainProgram follows custom binName" + else + echo "FAIL: meta.mainProgram should be 'hello-wrapped', got '${wrappedWithMeta.meta.mainProgram}'" + exit 1 + fi + echo -e "\n=== SUCCESS: All exePath and binName tests passed ===" touch $out '' diff --git a/checks/flags-empty-list.nix b/checks/flags-empty-list.nix index f0ab65fc..0a351b9d 100644 --- a/checks/flags-empty-list.nix +++ b/checks/flags-empty-list.nix @@ -13,7 +13,6 @@ let "--empty" = [ ]; "--output" = "file.txt"; }; - flagSeparator = " "; }; in diff --git a/checks/flags-false.nix b/checks/flags-false.nix index 0471147d..dcd0180e 100644 --- a/checks/flags-false.nix +++ b/checks/flags-false.nix @@ -15,7 +15,6 @@ let "--empty" = [ ]; "--output" = "file.txt"; }; - flagSeparator = " "; }; in diff --git a/checks/flags-list.nix b/checks/flags-list.nix index c70e6b86..6092ecf6 100644 --- a/checks/flags-list.nix +++ b/checks/flags-list.nix @@ -15,7 +15,6 @@ let ]; "--verbose" = true; }; - flagSeparator = " "; }; wrappedWithEqualsSep = self.lib.wrapPackage { diff --git a/checks/flags-order.nix b/checks/flags-order.nix new file mode 100644 index 00000000..64d79fe6 --- /dev/null +++ b/checks/flags-order.nix @@ -0,0 +1,70 @@ +{ + pkgs, + self, +}: + +let + helloModule = self.lib.wrapModule ( + { config, ... }: + { + config.package = config.pkgs.hello; + config.flags = { + # default order 1000: before "$@" (which is 1001) + "--greeting" = "hello"; + # explicit early order: should come first + "--early" = { + value = true; + order = 500; + }; + # explicit late order: should come after "$@" + "--late" = { + value = true; + order = 1500; + }; + }; + } + ); + + wrappedPackage = (helloModule.apply { inherit pkgs; }).wrapper; + +in +pkgs.runCommand "flags-order-test" { } '' + echo "Testing flag ordering with priorities..." + + wrapperScript="${wrappedPackage}/bin/hello" + if [ ! -f "$wrapperScript" ]; then + echo "FAIL: Wrapper script not found" + exit 1 + fi + + cat "$wrapperScript" + + # Flatten the script to a single line for position comparison + flat=$(cat "$wrapperScript" | tr -d '\n' | tr -s ' ') + + # --early (500) should come before --greeting (1000) + # --greeting (1000) should come before "$@" (1001) + # "$@" (1001) should come before --late (1500) + earlyPos=$(echo "$flat" | grep -bo -- '--early' | head -1 | cut -d: -f1) + greetingPos=$(echo "$flat" | grep -bo -- '--greeting' | head -1 | cut -d: -f1) + passthruPos=$(echo "$flat" | grep -bo '"\$@"' | head -1 | cut -d: -f1) + latePos=$(echo "$flat" | grep -bo -- '--late' | head -1 | cut -d: -f1) + + echo "Positions: early=$earlyPos greeting=$greetingPos passthru=$passthruPos late=$latePos" + + if [ "$earlyPos" -ge "$greetingPos" ]; then + echo "FAIL: --early should come before --greeting" + exit 1 + fi + if [ "$greetingPos" -ge "$passthruPos" ]; then + echo "FAIL: --greeting should come before \"\$@\"" + exit 1 + fi + if [ "$passthruPos" -ge "$latePos" ]; then + echo "FAIL: \"\$@\" should come before --late" + exit 1 + fi + + echo "SUCCESS: Flag ordering test passed" + touch $out +'' diff --git a/checks/flags-space-separator.nix b/checks/flags-space-separator.nix index 68e0d929..08c58fb4 100644 --- a/checks/flags-space-separator.nix +++ b/checks/flags-space-separator.nix @@ -11,7 +11,6 @@ let "--greeting" = "hi"; "--verbose" = true; }; - flagSeparator = " "; }; in diff --git a/checks/formatting.nix b/checks/formatting.nix index 7ddebfae..cdfb2bf2 100644 --- a/checks/formatting.nix +++ b/checks/formatting.nix @@ -4,6 +4,11 @@ }: pkgs.runCommand "formatting-check" { } '' - ${pkgs.lib.getExe self.formatter.${pkgs.system}} --no-cache --fail-on-change ${../.} + cp -r ${../.}/ src + # will be copied readonly from the /nix/store + # nixfmt sadly ignores --fail-on-change and still tries to write to the file + # ergo, we create our own writable copy + chmod -R +w src + ${pkgs.lib.getExe self.formatter.${pkgs.stdenv.hostPlatform.system}} --ci --tree-root ./src ./src touch $out '' diff --git a/checks/patchHook.nix b/checks/patchHook.nix new file mode 100644 index 00000000..26ef8915 --- /dev/null +++ b/checks/patchHook.nix @@ -0,0 +1,42 @@ +{ + pkgs, + self, +}: + +let + # Create a dummy package with a desktop file that references itself + dummyPackage = + (pkgs.runCommand "dummy-app" { } '' + # empty dir as a package + mkdir -p $out + '') + // { + meta.mainProgram = "dummy-app"; + }; + + # Wrap the package + wrappedPackage = self.lib.wrapPackage { + inherit pkgs; + package = dummyPackage; + patchHook = '' + touch $out/test + ''; + }; + +in +pkgs.runCommand "patchHook-test" + { + wrappedPath = "${wrappedPackage}"; + } + '' + echo "Testing patchHook functionality..." + echo "Wrapped package path: $wrappedPath" + + if [ ! -f "$wrappedPath/test" ]; then + echo "FAIL: file not created in patched package" + exit 1 + fi + + echo "SUCCESS: patchHook executed correctly" + touch $out + '' diff --git a/checks/systemd.nix b/checks/systemd.nix new file mode 100644 index 00000000..9fc04dac --- /dev/null +++ b/checks/systemd.nix @@ -0,0 +1,406 @@ +{ + pkgs, + self, +}: + +let + lib = pkgs.lib; + + # Test 1: Defaults from wrapper, both outputs from same config + withDefaults = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + flags."--greeting" = "world"; + env.HELLO_LANG = "en"; + systemd = { + description = "Hello service"; + serviceConfig.Type = "simple"; + wantedBy = [ "default.target" ]; + }; + }; + } + ); + + # Test 2: Override ExecStart + withOverride = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + env.FOO = "bar"; + systemd.serviceConfig = { + ExecStart = "/custom/bin/thing"; + Type = "oneshot"; + }; + }; + } + ); + + # Test 3: Service name from binName + customBinName = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + binName = "my-hello"; + systemd.serviceConfig.Type = "simple"; + }; + } + ); + + # Test 4: Deep merging via apply + baseModule = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + systemd = { + description = "Hello service"; + serviceConfig.Type = "simple"; + wantedBy = [ "default.target" ]; + }; + }; + } + ); + + extended = baseModule.apply { + systemd.serviceConfig.Restart = "always"; + systemd.environment.EXTRA = "value"; + }; + + # Test 5: Unit ordering + withDeps = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + systemd = { + description = "Hello with deps"; + after = [ "network.target" ]; + wants = [ "network.target" ]; + serviceConfig.Type = "simple"; + }; + }; + } + ); + + # Test 6: exePath, extraPackages, preHook, postHook + withHooks = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + extraPackages = [ pkgs.jq ]; + preHook = "echo pre"; + postHook = "echo post"; + systemd.serviceConfig.Type = "simple"; + }; + } + ); + + # Test 7: startAt generates a timer + withTimer = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + systemd = { + serviceConfig.Type = "oneshot"; + startAt = "hourly"; + }; + }; + } + ); + + # Test 8: Args with spaces are properly quoted for systemd + withSpacedArgs = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + flags."--greeting" = "hello world"; + systemd.serviceConfig.Type = "simple"; + }; + } + ); + + # Test 9: Args with quotes and backslashes + withSpecialArgs = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + flags."--greeting" = ''say "hi"''; + systemd.serviceConfig.Type = "simple"; + }; + } + ); + + # Test 10: Env vars with spaces + withSpecialEnv = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + env.MY_VAR = "hello world"; + env.SIMPLE = "plain"; + systemd.serviceConfig.Type = "simple"; + }; + } + ); + + # Test 11: Multiple extraPackages in PATH + withMultiPath = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + extraPackages = [ + pkgs.jq + pkgs.coreutils + ]; + systemd.serviceConfig.Type = "simple"; + }; + } + ); + + # Test 12: Minimal config (only required fields) + minimalConfig = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + }; + } + ); + + # Test 13: wrapper output still works when systemd module is imported + withWrapper = self.lib.wrapModule ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + config = { + pkgs = pkgs; + package = pkgs.hello; + flags."--greeting" = "world"; + systemd.serviceConfig.Type = "simple"; + }; + } + ); + + readUserService = drv: name: builtins.readFile "${drv}/lib/systemd/user/${name}.service"; + readSystemService = drv: name: builtins.readFile "${drv}/lib/systemd/system/${name}.service"; + readUserTimer = drv: name: builtins.readFile "${drv}/lib/systemd/user/${name}.timer"; + readSystemTimer = drv: name: builtins.readFile "${drv}/lib/systemd/system/${name}.timer"; +in +pkgs.runCommand "systemd-test" { } '' + echo "Testing systemd module..." + + # Test 1a: User service output + echo "Test 1a: User service defaults from wrapper" + user='${readUserService withDefaults.outputs.systemd-user "hello"}' + echo "$user" | grep -q 'Description=Hello service' || { echo "FAIL: missing description"; exit 1; } + echo "$user" | grep -q 'ExecStart=.*/bin/hello' || { echo "FAIL: ExecStart should default to exePath"; echo "$user"; exit 1; } + echo "$user" | grep -q '\-\-greeting' || { echo "FAIL: ExecStart should include args"; echo "$user"; exit 1; } + echo "$user" | grep -qF '"HELLO_LANG=en"' || { echo "FAIL: Environment should include env"; echo "$user"; exit 1; } + echo "$user" | grep -q 'WantedBy=default.target' || { echo "FAIL: missing WantedBy"; exit 1; } + echo "PASS: user service defaults" + + # Test 1b: System service output from same config + echo "Test 1b: System service output from same config" + system='${readSystemService withDefaults.outputs.systemd-system "hello"}' + echo "$system" | grep -q 'Description=Hello service' || { echo "FAIL: missing description"; exit 1; } + echo "$system" | grep -q 'ExecStart=.*/bin/hello' || { echo "FAIL: ExecStart should default to exePath"; echo "$system"; exit 1; } + echo "$system" | grep -qF '"HELLO_LANG=en"' || { echo "FAIL: Environment should include env"; echo "$system"; exit 1; } + echo "PASS: system service output from same config" + + # Test 2: Override ExecStart + echo "Test 2: Override ExecStart" + override='${readUserService withOverride.outputs.systemd-user "hello"}' + echo "$override" | grep -q 'ExecStart=/custom/bin/thing' || { echo "FAIL: ExecStart override not applied"; echo "$override"; exit 1; } + echo "$override" | grep -q 'Type=oneshot' || { echo "FAIL: Type override not applied"; exit 1; } + echo "PASS: override ExecStart" + + # Test 3: Service name from binName + echo "Test 3: Service name from binName" + test -f "${customBinName.outputs.systemd-user}/lib/systemd/user/my-hello.service" || { + echo "FAIL: user service file should be named my-hello.service" + ls -la "${customBinName.outputs.systemd-user}/lib/systemd/user/" + exit 1 + } + test -f "${customBinName.outputs.systemd-system}/lib/systemd/system/my-hello.service" || { + echo "FAIL: system service file should be named my-hello.service" + ls -la "${customBinName.outputs.systemd-system}/lib/systemd/system/" + exit 1 + } + echo "PASS: service name from binName" + + # Test 4: Deep merging via apply + echo "Test 4: Deep merging via apply" + extended='${readUserService extended.outputs.systemd-user "hello"}' + echo "$extended" | grep -q 'Description=Hello service' || { echo "FAIL: description lost after apply"; exit 1; } + echo "$extended" | grep -q 'Type=simple' || { echo "FAIL: Type lost after apply"; exit 1; } + echo "$extended" | grep -q 'Restart=always' || { echo "FAIL: Restart not merged"; exit 1; } + echo "$extended" | grep -qF '"EXTRA=value"' || { echo "FAIL: environment not merged"; exit 1; } + echo "$extended" | grep -q 'WantedBy=default.target' || { echo "FAIL: WantedBy lost after apply"; exit 1; } + echo "PASS: deep merging via apply" + + # Test 5: Unit ordering + echo "Test 5: Unit ordering" + withDeps='${readUserService withDeps.outputs.systemd-user "hello"}' + echo "$withDeps" | grep -q 'After=network.target' || { echo "FAIL: missing After"; exit 1; } + echo "$withDeps" | grep -q 'Wants=network.target' || { echo "FAIL: missing Wants"; exit 1; } + echo "PASS: unit ordering" + + # Test 6: exePath, extraPackages, preHook, postHook + echo "Test 6: exePath, extraPackages, preHook, postHook" + hooks='${readUserService withHooks.outputs.systemd-user "hello"}' + echo "$hooks" | grep -q 'ExecStart=${pkgs.hello}/bin/hello' || { echo "FAIL: ExecStart should use exePath"; echo "$hooks"; exit 1; } + echo "$hooks" | grep -q '${pkgs.jq}' || { echo "FAIL: extraPackages (jq) not in PATH"; echo "$hooks"; exit 1; } + echo "$hooks" | grep -q 'ExecStartPre=.*hello-pre-start' || { echo "FAIL: preHook not mapped to ExecStartPre"; echo "$hooks"; exit 1; } + echo "$hooks" | grep -q 'ExecStopPost=.*hello-post-stop' || { echo "FAIL: postHook not mapped to ExecStopPost"; echo "$hooks"; exit 1; } + echo "PASS: exePath, extraPackages, preHook, postHook" + + # Test 7: startAt generates a timer + echo "Test 7: startAt generates a timer" + timerSvc='${readUserService withTimer.outputs.systemd-user "hello"}' + echo "$timerSvc" | grep -q 'ExecStart=.*/bin/hello' || { echo "FAIL: service missing ExecStart"; echo "$timerSvc"; exit 1; } + timer='${readUserTimer withTimer.outputs.systemd-user "hello"}' + echo "$timer" | grep -q 'OnCalendar=hourly' || { echo "FAIL: timer missing OnCalendar"; echo "$timer"; exit 1; } + echo "$timer" | grep -q 'WantedBy=timers.target' || { echo "FAIL: timer missing WantedBy"; echo "$timer"; exit 1; } + systemTimer='${readSystemTimer withTimer.outputs.systemd-system "hello"}' + echo "$systemTimer" | grep -q 'OnCalendar=hourly' || { echo "FAIL: system timer missing OnCalendar"; echo "$systemTimer"; exit 1; } + echo "PASS: startAt generates a timer" + + # Test 8: Args with spaces are properly quoted + echo "Test 8: Args with spaces are quoted for systemd" + spaced='${readUserService withSpacedArgs.outputs.systemd-user "hello"}' + echo "$spaced" | grep -qF '"hello world"' || { echo "FAIL: spaced arg not quoted"; echo "$spaced"; exit 1; } + echo "PASS: args with spaces" + + # Test 9: Args with quotes and backslashes + echo "Test 9: Args with quotes and backslashes" + special='${readUserService withSpecialArgs.outputs.systemd-user "hello"}' + echo "$special" | grep -qF '"say \"hi\""' || { echo "FAIL: special chars not escaped"; echo "$special"; exit 1; } + echo "PASS: args with special chars" + + # Test 10: Env vars with spaces + echo "Test 10: Env vars with spaces" + specialEnv='${readUserService withSpecialEnv.outputs.systemd-user "hello"}' + echo "$specialEnv" | grep -qF 'MY_VAR=hello world' || { echo "FAIL: env with spaces"; echo "$specialEnv"; exit 1; } + echo "$specialEnv" | grep -qF 'SIMPLE=plain' || { echo "FAIL: simple env missing"; echo "$specialEnv"; exit 1; } + echo "PASS: env vars with spaces" + + # Test 11: Multiple extraPackages in PATH + echo "Test 11: Multiple extraPackages in PATH" + multiPath='${readUserService withMultiPath.outputs.systemd-user "hello"}' + echo "$multiPath" | grep -q '${pkgs.jq}' || { echo "FAIL: jq not in PATH"; echo "$multiPath"; exit 1; } + echo "$multiPath" | grep -q '${pkgs.coreutils}' || { echo "FAIL: coreutils not in PATH"; echo "$multiPath"; exit 1; } + echo "PASS: multiple extraPackages" + + # Test 12: Minimal config produces a valid unit + echo "Test 12: Minimal config" + minimal='${readUserService minimalConfig.outputs.systemd-user "hello"}' + echo "$minimal" | grep -q 'ExecStart=.*/bin/hello' || { echo "FAIL: minimal missing ExecStart"; echo "$minimal"; exit 1; } + echo "$minimal" | grep -q '\[Service\]' || { echo "FAIL: minimal missing [Service] section"; echo "$minimal"; exit 1; } + echo "PASS: minimal config" + + # Test 13: wrapper output still works with systemd module + echo "Test 13: wrapper still works with systemd module" + ${withWrapper.wrapper}/bin/hello | grep -q 'world' || { echo "FAIL: wrapper broken"; exit 1; } + echo "PASS: wrapper still works" + + echo "SUCCESS: All systemd tests passed" + touch $out +'' diff --git a/default.nix b/default.nix index f5d828b0..d2274b4c 100644 --- a/default.nix +++ b/default.nix @@ -1,8 +1,8 @@ { pkgs ? import { }, + lib ? pkgs.lib, }: let - lib = pkgs.lib; wlib = import ./lib { inherit lib; }; in { diff --git a/flake.lock b/flake.lock index 9fc6ab5d..d8b0cdc2 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764667669, - "narHash": "sha256-7WUCZfmqLAssbDqwg9cUDAXrSoXN79eEEq17qhTNM/Y=", + "lastModified": 1773734432, + "narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "418468ac9527e799809c900eda37cbff999199b6", + "rev": "cda48547b432e8d3b18b4180ba07473762ec8558", "type": "github" }, "original": { diff --git a/lib/default.nix b/lib/default.nix index 14d60b66..4eaf84b3 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -2,14 +2,18 @@ let /** flagToArgs { - flagSeparator: str, + flagSeparator: null | str, name: str, flag: bool | str | [ str | [ str ] ] - } -> [ str + } -> [ str ] + + flagSeparator = null -> ["--flag" "value"] (separate argv entries, default) + flagSeparator = "=" -> ["--flag=value"] (joined with separator) + flagSeparator = " " -> ["--flag value"] (joined with space, single arg) */ flagToArgs = { - flagSeparator ? " ", + flagSeparator ? null, name, flag, }: @@ -17,40 +21,40 @@ let [ ] else if flag == true then [ name ] - else if builtins.isString flag then - if flagSeparator == " " then + else if lib.isStringLike flag then + if flagSeparator == null then [ name - flag + (toString flag) ] else - [ "${name}${flagSeparator}${flag}" ] + [ "${name}${flagSeparator}${toString flag}" ] else if lib.isList flag then lib.concatMap ( v: - if builtins.isString v then - if flagSeparator == " " then + if lib.isStringLike v then + if flagSeparator == null then [ name - v + (toString v) ] else - [ "${name}${flagSeparator}${v}" ] + [ "${name}${flagSeparator}${toString v}" ] else if builtins.isList v then [ name ] ++ (map ( v_: - if builtins.isString v_ then - v_ + if lib.isStringLike v_ then + toString v_ else - throw "flag ${name} has unsupported list element type ${lib.typeOf v_}, expected str" + throw "flag ${name} has unsupported list element type ${lib.typeOf v_}, expected path or str" ) v) else - throw "flag ${name} has unsupported list element type ${lib.typeOf v}, expected str or list" + throw "flag ${name} has unsupported list element type ${lib.typeOf v}, expected path, str or list" ) flag else - throw "flag ${name} has unsupported type ${lib.typeOf flag}, expected bool, str, or list"; + throw "flag ${name} has unsupported type ${lib.typeOf flag}, expected bool, path, str, or list"; # Helper function to generate args list from flags attrset generateArgsFromFlags = @@ -249,7 +253,9 @@ let inherit modules class specialArgs; }; - modules = lib.genAttrs [ "package" "wrapper" "meta" ] (name: import ./modules/${name}.nix); + modules = lib.genAttrs [ "package" "flags" "command" "wrapper" "meta" "systemd" ] ( + name: import ./modules/${name}.nix + ); /** Create a wrapper configuration using the NixOS module system. @@ -377,15 +383,17 @@ let - `runtimeInputs`: List of packages to add to PATH (optional) - `env`: Attribute set of environment variables to export (optional) - `flags`: Attribute set of command-line flags to add (optional) - - `flagSeparator`: Separator between flag names and values when generating args from flags (optional, defaults to " ") + - `flagSeparator`: Separator between flag names and values when generating args from flags (optional, defaults to null for separate argv entries, use "=" for joined) - `args`: List of command-line arguments like argv in execve (optional, auto-generated from flags if not provided) - `preHook`: Shell script to run before executing the command (optional) + - `postHook`: Shell script to run after executing the command, removes the `exec` call. use with care (optional) - `passthru`: Attribute set to pass through to the wrapped derivation (optional) - `aliases`: List of additional names to symlink to the wrapped executable (optional) - `filesToPatch`: List of file paths (glob patterns) to patch for self-references (optional, defaults to ["share/applications/*.desktop"]) - `filesToExclude`: List of file paths (glob patterns) to exclude from the wrapped package (optional, defaults to []) + - `patchHook`: Shell script that runs after patchPhase to modify the wrapper package files (optional) - `wrapper`: Custom wrapper function (optional, defaults to exec'ing the original binary with args) - - Called with { env, flags, args, envString, flagsString, exePath, preHook } + - Called with { env, flags, args, envString, flagsString, exePath, preHook, postHook } # Example @@ -441,34 +449,37 @@ let runtimeInputs ? [ ], env ? { }, flags ? { }, - flagSeparator ? " ", - # " " for "--flag value" or "=" for "--flag=value" - args ? generateArgsFromFlags flags flagSeparator, + flagSeparator ? null, + # null for "--flag" "value" (separate args) or "=" for "--flag=value" + args ? generateArgsFromFlags flags flagSeparator ++ [ "$@" ], preHook ? "", + postHook ? "", passthru ? { }, aliases ? [ ], # List of file paths (glob patterns) relative to package root to patch for self-references (e.g., ["bin/*", "lib/*.sh"]) filesToPatch ? [ "share/applications/*.desktop" ], # List of file paths (glob patterns) to exclude from the wrapped package (e.g., ["bin/unwanted-*", "share/doc/*"]) filesToExclude ? [ ], + patchHook ? "", wrapper ? ( { exePath, flagsString, envString, preHook, + postHook, ... }: '' ${envString} ${preHook} - exec ${exePath}${flagsString} "$@" + ${lib.optionalString (postHook == "") "exec"} ${exePath}${flagsString} + ${postHook} '' ), }@funcArgs: let - # lndir was moved from xorg.lndir to lndir in https://github.com/NixOS/nixpkgs/pull/402102 - lndir = if pkgs ? xorg.lndir then pkgs.xorg.lndir else pkgs.lndir; + inherit (pkgs) lndir; # Generate environment variable exports envString = @@ -496,6 +507,7 @@ let flagsString exePath preHook + postHook ; }; @@ -512,14 +524,13 @@ let binName ? null, filesToPatch ? [ ], filesToExclude ? [ ], + patchHook ? "", ... }@args: pkgs.stdenv.mkDerivation ( { inherit name outputs; - nativeBuildInputs = lib.optionals (filesToPatch != [ ]) [ pkgs.replace ]; - buildCommand = '' # Symlink all paths to the main output mkdir -p $out @@ -559,7 +570,7 @@ let # Remove symlink and create a real file with patched content rm "$file" # Use replace-literal which works for both text and binary files - replace-literal "$oldPath" "$newPath" < "$target" > "$file" + substitute "$target" "$file" --replace-fail "$oldPath" "$newPath" # Preserve permissions chmod --reference="$target" "$file" fi @@ -567,6 +578,7 @@ let done '') filesToPatch} ''} + ${patchHook} # Create symlinks for aliases ${lib.optionalString (aliases != [ ] && binName != null) '' @@ -605,6 +617,7 @@ let "binName" "filesToPatch" "filesToExclude" + "patchHook" ]) ); @@ -639,6 +652,7 @@ let binName filesToPatch filesToExclude + patchHook ; passthru = (package.passthru or { }) @@ -649,6 +663,7 @@ let flags args preHook + postHook aliases ; override = @@ -660,8 +675,11 @@ let } ); }; - # Pass through original attributes - meta = package.meta or { }; + meta = + (package.meta or { }) + // lib.optionalAttrs (binName != null) { + mainProgram = binName; + }; } // lib.optionalAttrs (package ? version) { inherit (package) version; diff --git a/lib/modules/command.nix b/lib/modules/command.nix new file mode 100644 index 00000000..37c3db53 --- /dev/null +++ b/lib/modules/command.nix @@ -0,0 +1,74 @@ +{ + lib, + wlib, + config, + ... +}: +{ + _file = "lib/modules/command.nix"; + imports = [ + wlib.modules.package + wlib.modules.flags + ]; + options.args = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Command-line arguments to pass to the wrapper (like argv in execve). + This is a list of strings representing individual arguments. + If not specified, will be automatically generated from flags. + ''; + }; + options.extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = '' + Additional packages to add to the wrapper's runtime dependencies. + This is useful if the wrapped program needs additional libraries or tools to function correctly. + These packages will be added to the wrapper's runtime dependencies, ensuring they are available when the wrapped program is executed. + ''; + }; + options.env = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Environment variables to set in the wrapper. + ''; + }; + options.preHook = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Shell script to run before executing the command. + Multiple definitions are concatenated with newlines. + ''; + }; + options.postHook = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Shell script to run after executing the command. + Removes the `exec` call in the wrapper script which will leave a bash process + in the background, therefore use with care. + Multiple definitions are concatenated with newlines. + ''; + }; + options.exePath = lib.mkOption { + type = lib.types.path; + description = '' + Path to the executable within the package to be wrapped. + If not specified, the main executable of the package will be used. + ''; + default = lib.getExe config.package; + defaultText = "lib.getExe config.package"; + }; + options.binName = lib.mkOption { + type = lib.types.str; + description = '' + Name of the binary in the resulting wrapper package. + If not specified, the base name of exePath will be used. + ''; + default = builtins.baseNameOf config.exePath; + defaultText = "builtins.baseNameOf config.exePath"; + }; +} diff --git a/lib/modules/flags.nix b/lib/modules/flags.nix new file mode 100644 index 00000000..14422171 --- /dev/null +++ b/lib/modules/flags.nix @@ -0,0 +1,89 @@ +{ + lib, + wlib, + config, + options, + ... +}: +let + flagValueType = lib.types.oneOf [ + (lib.types.uniq lib.types.bool) + (lib.types.uniq lib.types.path) + (lib.types.uniq lib.types.str) + (lib.types.listOf ( + lib.types.oneOf [ + lib.types.path + lib.types.str + (lib.types.listOf ( + lib.types.oneOf [ + lib.types.path + lib.types.str + ] + )) + ] + )) + ]; + + flagSubmodule = lib.types.submodule { + options.value = lib.mkOption { + type = flagValueType; + description = "The flag value."; + }; + options.order = lib.mkOption { + type = lib.types.int; + default = 1000; + description = '' + Order priority for this flag in the generated args list. + Lower numbers come first. Default is 1000. + ''; + }; + }; +in +{ + _file = "lib/modules/flags.nix"; + + options.flags = lib.mkOption { + type = lib.types.lazyAttrsOf (lib.types.coercedTo flagValueType (v: { value = v; }) flagSubmodule); + default = { }; + apply = lib.mapAttrs (_: v: v.value); + description = '' + Flags to pass to the wrapper. + The key is the flag name, the value is the flag value. + If the value is true, the flag will be passed without a value. + If the value is false, the flag will not be passed. + If the value is a list, the flag will be passed multiple times with each value. + Can also be set to { value = ...; order = N; } to control ordering in args. + ''; + }; + + options._orderedFlags = lib.mkOption { + type = lib.types.lazyAttrsOf (lib.types.coercedTo flagValueType (v: { value = v; }) flagSubmodule); + internal = true; + default = { }; + }; + + options.flagSeparator = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Separator between flag names and values when generating args from flags. + null (default) for separate argv entries: "--flag" "value" + "=" for joined: "--flag=value" + ''; + }; + + config._orderedFlags = lib.mkAliasDefinitions options.flags; + + config.args = lib.mkMerge ( + lib.mapAttrsToList ( + name: flagDef: + lib.mkOrder flagDef.order ( + wlib.flagToArgs { + inherit name; + flag = flagDef.value; + flagSeparator = config.flagSeparator; + } + ) + ) config._orderedFlags + ); +} diff --git a/lib/modules/systemd.nix b/lib/modules/systemd.nix new file mode 100644 index 00000000..2bd4a630 --- /dev/null +++ b/lib/modules/systemd.nix @@ -0,0 +1,137 @@ +{ + config, + lib, + ... +}: +let + cfg = config.systemd; + pkgs = config.pkgs; + + serviceName = config.binName; + + # Import the systemd unit generation helpers directly from nixpkgs. + systemdLib = import (pkgs.path + "/nixos/lib/systemd-lib.nix") { + inherit lib pkgs; + config.systemd = { + globalEnvironment = { }; + enableStrictShellChecks = true; + package = pkgs.systemd; + }; + utils = { }; + }; + + unitOptions = import (pkgs.path + "/nixos/lib/systemd-unit-options.nix") { + inherit lib; + systemdUtils.lib = systemdLib; + }; + + # Evaluate a single service using the same submodule composition as + # NixOS (stage2ServiceOptions + unitConfig + stage2ServiceConfig). + svcEval = lib.evalModules { + modules = [ + unitOptions.stage2ServiceOptions + systemdLib.unitConfig + systemdLib.stage2ServiceConfig + { _module.args.name = serviceName; } + { config = cfg; } + ]; + }; + + svc = svcEval.config; + + hasTimer = cfg ? startAt && cfg.startAt != [ ] && cfg.startAt != ""; + + timerEval = lib.evalModules { + modules = [ + unitOptions.stage2TimerOptions + systemdLib.unitConfig + systemdLib.timerConfig + { _module.args.name = serviceName; } + { + config = { + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = cfg.startAt; + }; + } + ]; + }; + + timer = timerEval.config; + + mkOutput = + type: + let + unitDir = if type == "user" then "lib/systemd/user" else "lib/systemd/system"; + + serviceFile = pkgs.writeTextDir "${unitDir}/${serviceName}.service" (systemdLib.serviceToUnit svc) + .text; + + timerFile = pkgs.writeTextDir "${unitDir}/${serviceName}.timer" (systemdLib.timerToUnit timer).text; + in + if hasTimer then + pkgs.symlinkJoin { + name = "${serviceName}-${type}-units"; + paths = [ + serviceFile + timerFile + ]; + } + else + serviceFile; +in +{ + _file = "lib/modules/systemd.nix"; + + options.systemd = lib.mkOption { + type = lib.types.submodule { freeformType = with lib.types; attrsOf anything; }; + default = { }; + description = '' + Systemd service configuration. + Accepts the same options as systemd.services. in NixOS. + + ExecStart, Environment, PATH, preStart and postStop are set from the + wrapper by default. If startAt is set, a .timer unit is included in + the output. + ''; + }; + + config.systemd = { + enableDefaultPath = lib.mkDefault false; + serviceConfig.ExecStart = lib.mkDefault ( + let + # Systemd parses ExecStart using its own unquoting rules: bare + # words are split on whitespace, double-quoted strings preserve + # spaces. Backslash and double-quote inside a quoted word must + # be escaped with a backslash. + escapeForSystemd = + s: + let + escaped = lib.replaceStrings [ "\\" "\"" ] [ "\\\\" "\\\"" ] s; + in + if lib.hasInfix " " s || lib.hasInfix "\t" s || lib.hasInfix "\"" s then + "\"${escaped}\"" + else + escaped; + in + lib.concatStringsSep " " ([ config.exePath ] ++ map escapeForSystemd config.args) + ); + environment = lib.mkDefault config.env; + path = lib.mkDefault config.extraPackages; + preStart = lib.mkIf (config.preHook != "") (lib.mkDefault config.preHook); + postStop = lib.mkIf (config.postHook != "") (lib.mkDefault config.postHook); + }; + + options.outputs.systemd-user = lib.mkOption { + type = lib.types.package; + readOnly = true; + description = "The generated systemd user unit files."; + default = mkOutput "user"; + }; + + options.outputs.systemd-system = lib.mkOption { + type = lib.types.package; + readOnly = true; + description = "The generated systemd system unit files."; + default = mkOutput "system"; + }; +} diff --git a/lib/modules/wrapper.nix b/lib/modules/wrapper.nix index 9371c8ef..20aacd35 100644 --- a/lib/modules/wrapper.nix +++ b/lib/modules/wrapper.nix @@ -6,68 +6,7 @@ }: { _file = "lib/modules/wrapper.nix"; - imports = [ wlib.modules.package ]; - options.extraPackages = lib.mkOption { - type = lib.types.listOf lib.types.package; - default = [ ]; - description = '' - Additional packages to add to the wrapper's runtime dependencies. - This is useful if the wrapped program needs additional libraries or tools to function correctly. - These packages will be added to the wrapper's runtime dependencies, ensuring they are available when the wrapped program is executed. - ''; - }; - options.flags = lib.mkOption { - # we want to support: - # --flag = "somestring" ==> --flag "something" - # --flag = true ==> --flag - # --flag = false ==> no flag (used to remove flag via apply) - # --flag = [ "list" "of" "flags" ] ==> --flag list --flag of --flag flags - # --flag = [ [ "list" "of" "flags" ] "test" ]; ==> --flag list of flags --flag test - type = lib.types.lazyAttrsOf ( - lib.types.oneOf [ - (lib.types.uniq lib.types.str) - (lib.types.uniq lib.types.bool) - (lib.types.listOf ( - lib.types.oneOf [ - lib.types.str - (lib.types.listOf lib.types.str) - ] - )) - ] - ); - default = { }; - description = '' - Flags to pass to the wrapper. - The key is the flag name, the value is the flag value. - If the value is true, the flag will be passed without a value. - If the value is false, the flag will not be passed. - If the value is a list, the flag will be passed multiple times with each value. - ''; - }; - options.flagSeparator = lib.mkOption { - type = lib.types.str; - default = " "; - description = '' - Separator between flag names and values when generating args from flags. - " " for "--flag value" or "=" for "--flag=value" - ''; - }; - config.args = wlib.generateArgsFromFlags config.flags config.flagSeparator; - options.args = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = '' - Command-line arguments to pass to the wrapper (like argv in execve). - This is a list of strings representing individual arguments. - If not specified, will be automatically generated from flags. - ''; - }; - options.env = lib.mkOption { - type = lib.types.attrsOf lib.types.str; - default = { }; - description = '' - Environment variables to set in the wrapper. - ''; - }; + imports = [ wlib.modules.command ]; options.filesToPatch = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "share/applications/*.desktop" ]; @@ -85,27 +24,22 @@ Example: [ "bin/unwanted-tool" "share/applications/*.desktop" ] ''; }; - options.exePath = lib.mkOption { - type = lib.types.path; - description = '' - Path to the executable within the package to be wrapped. - If not specified, the main executable of the package will be used. - ''; - default = lib.getExe config.package; - defaultText = "lib.getExe config.package"; - }; - options.binName = lib.mkOption { + options.patchHook = lib.mkOption { type = lib.types.str; + default = ""; description = '' - Name of the binary in the resulting wrapper package. - If not specified, the base name of exePath will be used. + Shell script that runs after patchPhase to modify the wrapper package files. ''; - default = builtins.baseNameOf config.exePath; - defaultText = "builtins.baseNameOf config.exePath"; }; - options.wrapper = lib.mkOption { + + # Inject "$@" (passthrough of user arguments) into args at order 1001, + # so it comes just after the default flag order (1000). + # Use mkOrder on args to position it; other flags can use order > 1001 + # to appear after "$@" if needed. + config.args = lib.mkOrder 1001 [ "$@" ]; + + options.outputs.wrapper = lib.mkOption { type = lib.types.package; - readOnly = true; description = '' The wrapped package created by wrapPackage. This wraps the configured package with the specified flags, environment variables, runtime dependencies, and other @@ -123,10 +57,21 @@ env = config.env; filesToPatch = config.filesToPatch; filesToExclude = config.filesToExclude; + preHook = config.preHook; + postHook = config.postHook; + patchHook = config.patchHook; passthru = { configuration = config; } // config.passthru; }; }; + options.wrapper = lib.mkOption { + type = lib.types.package; + readOnly = true; + description = '' + Backward-compatible alias for outputs.wrapper. + ''; + default = config.outputs.wrapper; + }; } diff --git a/modules/alacritty/module.nix b/modules/alacritty/module.nix index cfac63a1..3ff76780 100644 --- a/modules/alacritty/module.nix +++ b/modules/alacritty/module.nix @@ -26,7 +26,7 @@ in }; }; config.flags = { - "--config-file" = toString config."alacritty.toml".path; + "--config-file" = config."alacritty.toml".path; }; config.package = config.pkgs.alacritty; config.meta.maintainers = [ lib.maintainers.zimward ]; diff --git a/modules/bat/check.nix b/modules/bat/check.nix new file mode 100644 index 00000000..93df2f97 --- /dev/null +++ b/modules/bat/check.nix @@ -0,0 +1,20 @@ +{ + pkgs, + self, +}: +let + batWrapped = + (self.wrapperModules.bat.apply { + inherit pkgs; + + "bat-config".content = '' + # Test config + --italic-text=never + --theme="Catppuccin Mocha" + ''; + }).wrapper; +in +pkgs.runCommand "bat-test" { } '' + ${batWrapped}/bin/bat -p + ''; + example = { + directory = "/music"; + library = "/music/library.db"; + plugins = [ + "chroma" + "fetchart" + ]; + }; + }; + + configFile = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = yamlFmt.generate "beets-config" config.settings; + description = '' + Configuration of beets + ''; + }; + + extraPlugins = lib.mkOption { + type = lib.types.attrsOf lib.types.package; + default = { }; + description = '' + Attrset mapping beets plugin names to the corresponding package. + + Plugins must be manually enabled in the configuration, + see + ''; + example = lib.literalExpression '' + { + alternatives = pkgs.python3Packages.beets-alternatives; + } + ''; + }; + }; + + config = { + package = config.basePackage.override ( + lib.optionalAttrs (config.extraPlugins != { }) { + pluginOverrides = lib.mapAttrs (_: pkg: { + enable = true; + propagatedBuildInputs = [ pkg ]; + }) config.extraPlugins; + } + ); + + flags."--config" = config.configFile.path; + + meta = { + maintainers = [ lib.maintainers.bandithedoge ]; + platforms = with lib.platforms; linux ++ darwin; + }; + }; +} diff --git a/modules/btop/module.nix b/modules/btop/module.nix index 98740df4..54a31f01 100644 --- a/modules/btop/module.nix +++ b/modules/btop/module.nix @@ -17,7 +17,7 @@ }; config.flags = { - "--config" = toString config."btop.conf".path; + "--config" = config."btop.conf".path; }; config.package = config.pkgs.btop; diff --git a/modules/fastfetch/module.nix b/modules/fastfetch/module.nix new file mode 100644 index 00000000..cd92fe88 --- /dev/null +++ b/modules/fastfetch/module.nix @@ -0,0 +1,38 @@ +{ + config, + lib, + wlib, + ... +}: +let + jsonFmt = config.pkgs.formats.json { }; +in +{ + _class = "wrapper"; + options = { + settings = lib.mkOption { + type = jsonFmt.type; + default = { }; + description = '' + fastfetch settings + see + ''; + }; + "config.jsonc" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = jsonFmt.generate "fastfetch-config" config.settings; + description = "fastfetch config file"; + }; + }; + config = { + package = config.pkgs.fastfetch; + flags."--config" = config."config.jsonc".path; + meta.maintainers = [ + { + name = "holly"; + github = "hollymlem"; + githubId = 35699052; + } + ]; + }; +} diff --git a/modules/foot/module.nix b/modules/foot/module.nix index 8e6e27dc..4e6d5a3c 100644 --- a/modules/foot/module.nix +++ b/modules/foot/module.nix @@ -5,7 +5,7 @@ ... }: let - iniFmt = config.pkgs.formats.ini { + iniFmt = config.pkgs.formats.iniWithGlobalSection { # from https://github.com/NixOS/nixpkgs/blob/89f10dc1a8b59ba63f150a08f8cf67b0f6a2583e/nixos/modules/programs/foot/default.nix#L11-L29 listsAsDuplicateKeys = true; mkKeyValue = @@ -30,7 +30,12 @@ in _class = "wrapper"; options = { settings = lib.mkOption { - inherit (iniFmt) type; + type = lib.types.attrsOf ( + lib.types.oneOf [ + iniFmt.lib.types.atom + (lib.types.attrsOf iniFmt.lib.types.atom) + ] + ); default = { }; description = '' Configuration of foot terminal. @@ -40,13 +45,21 @@ in "foot.ini" = lib.mkOption { type = wlib.types.file config.pkgs; description = "foot.init configuration file."; - default.path = iniFmt.generate "foot.ini" config.settings; + default.path = iniFmt.generate "foot.ini" { + globalSection = lib.filterAttrs (name: value: builtins.typeOf value != "set") config.settings; + sections = lib.filterAttrs (name: value: builtins.typeOf value == "set") config.settings; + }; }; }; - config.flags = { - "--config" = toString config."foot.ini".path; + config = { + filesToPatch = [ "share/systemd/user/foot-server.service" ]; + flags = { + "--config" = config."foot.ini".path; + }; + package = config.pkgs.foot; + meta = { + maintainers = [ lib.maintainers.randomdude ]; + platforms = lib.platforms.linux; + }; }; - config.package = config.pkgs.foot; - config.meta.maintainers = [ lib.maintainers.randomdude ]; - config.meta.platforms = lib.platforms.linux; } diff --git a/modules/fuzzel/module.nix b/modules/fuzzel/module.nix index 04eb76de..cba9d416 100644 --- a/modules/fuzzel/module.nix +++ b/modules/fuzzel/module.nix @@ -25,7 +25,7 @@ in }; config.flagSeparator = "="; config.flags = { - "--config" = toString config."fuzzel.ini".path; + "--config" = config."fuzzel.ini".path; }; config.package = config.pkgs.fuzzel; config.meta.maintainers = [ lib.maintainers.zimward ]; diff --git a/modules/ghostty/check.nix b/modules/ghostty/check.nix index da67b624..a4f6fb43 100644 --- a/modules/ghostty/check.nix +++ b/modules/ghostty/check.nix @@ -31,7 +31,7 @@ let in pkgs.runCommand "ghostty" { } '' "${ghosttyWrapped}/bin/ghostty" +validate-config - "${ghosttyWrapped}/bin/ghostty" +version | grep -q "${ghosttyWrapped.version}" + [[ "$(${ghosttyWrapped}/bin/ghostty +version)" == *"${ghosttyWrapped.version}"* ]] "${ghosttyFileWrapped}/bin/ghostty" +validate-config diff --git a/modules/ghostty/module.nix b/modules/ghostty/module.nix index e394a06a..c69104d8 100644 --- a/modules/ghostty/module.nix +++ b/modules/ghostty/module.nix @@ -54,17 +54,25 @@ in ''; }; }; - config.flagSeparator = "="; - config.flags = { - "--config-file" = toString config.configFile.path; + config = { + filesToPatch = [ + "share/dbus-1/services/com.mitchellh.ghostty.service" + "share/systemd/user/app-com.mitchellh.ghostty.service" + ]; + flagSeparator = "="; + flags = { + "--config-file" = config.configFile.path; + }; + package = config.pkgs.ghostty; + meta = { + platforms = lib.platforms.linux; + maintainers = [ + { + name = "turbio"; + github = "turbio"; + githubId = 1428207; + } + ]; + }; }; - config.package = config.pkgs.ghostty; - config.meta.platforms = lib.platforms.linux; - config.meta.maintainers = [ - { - name = "turbio"; - github = "turbio"; - githubId = 1428207; - } - ]; } diff --git a/modules/git-cliff/check.nix b/modules/git-cliff/check.nix new file mode 100644 index 00000000..8ba0d087 --- /dev/null +++ b/modules/git-cliff/check.nix @@ -0,0 +1,18 @@ +{ + pkgs, + self, +}: +let + gitCliffWrapped = + (self.wrapperModules.git-cliff.apply { + inherit pkgs; + settings = { + changelog.trim = true; + git.conventional_commits = true; + }; + }).wrapper; +in +pkgs.runCommand "git-cliff-test" { } '' + "${gitCliffWrapped}/bin/git-cliff" --version | grep -q "git-cliff" + touch $out +'' diff --git a/modules/git-cliff/module.nix b/modules/git-cliff/module.nix new file mode 100644 index 00000000..8783fafd --- /dev/null +++ b/modules/git-cliff/module.nix @@ -0,0 +1,46 @@ +{ + config, + lib, + wlib, + ... +}: +let + tomlFmt = config.pkgs.formats.toml { }; +in +{ + _class = "wrapper"; + + options = { + settings = lib.mkOption { + type = tomlFmt.type; + default = { }; + description = '' + Structured git-cliff configuration written to cliff.toml. + See + ''; + }; + + "cliff.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = tomlFmt.generate "cliff.toml" config.settings; + description = '' + The generated cliff.toml configuration file. + ''; + }; + }; + + config.package = config.pkgs.git-cliff; + + config.flags = { + "--config" = toString config."cliff.toml".path; + }; + + config.meta.maintainers = [ + { + name = "Alexander Kenji Berthold"; + github = "a-kenji"; + githubId = 65275785; + } + ]; + config.meta.platforms = lib.platforms.all; +} diff --git a/modules/git/check.nix b/modules/git/check.nix index 2e8501af..b9720680 100644 --- a/modules/git/check.nix +++ b/modules/git/check.nix @@ -17,6 +17,6 @@ let in pkgs.runCommand "git-test" { } '' - "${gitWrapped}/bin/git" --version | grep -q "git" + [[ "$(${gitWrapped}/bin/git --version)" == *git* ]] touch $out '' diff --git a/modules/halloy/module.nix b/modules/halloy/module.nix new file mode 100644 index 00000000..50e4db7b --- /dev/null +++ b/modules/halloy/module.nix @@ -0,0 +1,73 @@ +{ + config, + wlib, + lib, + ... +}: +let + tomlFmt = config.pkgs.formats.toml { }; +in +{ + _class = "wrapper"; + options = { + settings = lib.mkOption { + type = tomlFmt.type; + default = { }; + }; + "config.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = tomlFmt.generate "halloy-config" config.settings; + }; + extraFiles = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.nonEmptyStr; + description = "File name in the halloy config directory"; + }; + file = lib.mkOption { + type = wlib.types.file config.pkgs; + description = "File or path to add in the halloy config directory"; + }; + }; + } + ); + default = [ ]; + description = "Additional files to be placed in the config directory"; + }; + }; + config = { + env = { + XDG_CONFIG_HOME = toString ( + config.pkgs.linkFarm "halloy-merged-config" ( + map + (a: { + inherit (a) path; + name = "halloy/" + a.name; + }) + ( + let + entry = name: path: { inherit name path; }; + in + [ (entry "config.toml" config."config.toml".path) ] + ++ (map (f: { + inherit (f) name; + path = f.file.path; + }) config.extraFiles) + ) + ) + ); + }; + package = config.pkgs.halloy; + meta = { + maintainers = [ + { + name = "holly"; + github = "hollymlem"; + githubId = 35699052; + } + ]; + }; + }; +} diff --git a/modules/helix/module.nix b/modules/helix/module.nix index d9f62771..1446ff52 100644 --- a/modules/helix/module.nix +++ b/modules/helix/module.nix @@ -87,11 +87,29 @@ in ignoreFile = lib.mkOption { type = wlib.types.file config.pkgs; }; + extraFiles = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.nonEmptyStr; + description = "File name in the helix config directory"; + }; + file = lib.mkOption { + type = wlib.types.file config.pkgs; + description = "File or path to add in the helix config directory"; + }; + }; + } + ); + default = [ ]; + description = "Additional files to be placed in the config directory"; + }; }; config.ignoreFile.content = lib.strings.concatLines config.ignores; config.package = config.pkgs.helix; config.env = { - XDG_CONFIG_HOME = builtins.toString ( + XDG_CONFIG_HOME = toString ( config.pkgs.linkFarm "helix-merged-config" ( map (a: { @@ -108,6 +126,10 @@ in (entry "ignore" config.ignoreFile.path) ] ++ themes + ++ (map (f: { + inherit (f) name; + path = f.file.path; + }) config.extraFiles) ) ) ); diff --git a/modules/hypridle/check.nix b/modules/hypridle/check.nix new file mode 100644 index 00000000..69dc867a --- /dev/null +++ b/modules/hypridle/check.nix @@ -0,0 +1,62 @@ +{ + pkgs, + self, +}: + +let + hypridleWrapped = + (self.wrapperModules.hypridle.apply { + inherit pkgs; + + # example from https://wiki.hypr.land/Hypr-Ecosystem/hypridle/ + settings = { + general = { + lock_cmd = "pidof hyprlock || hyprlock"; + before_sleep_cmd = "loginctl lock-session"; + after_sleep_cmd = "hyprctl dispatch dpms on"; + }; + + listener = [ + # monitor backlight dimming (2.5min) + { + timeout = 150; + on-timeout = "brightnessctl -s set 10"; + on-resume = "brightnessctl -r"; + } + + # keyboard backlight off (2.5min) + { + timeout = 150; + on-timeout = "brightnessctl -sd rgb:kbd_backlight set 0"; + on-resume = "brightnessctl -rd rgb:kbd_backlight"; + } + + # lock screen (5min) + { + timeout = 300; + on-timeout = "loginctl lock-session"; + } + + # screen off (5.5min) + { + timeout = 330; + on-timeout = "hyprctl dispatch dpms off"; + on-resume = "hyprctl dispatch dpms on && brightnessctl -r"; + } + + # suspend (30min) + { + timeout = 1800; + on-timeout = "systemctl suspend"; + } + ]; + }; + }).wrapper; + +in +pkgs.runCommand "hypridle-test" { } '' + export XDG_RUNTIME_DIR=/run/user/$(id -u) + "${hypridleWrapped}/bin/hypridle" --version | grep -q "${hypridleWrapped.version}" + + touch $out +'' diff --git a/modules/hypridle/module.nix b/modules/hypridle/module.nix new file mode 100644 index 00000000..32d08470 --- /dev/null +++ b/modules/hypridle/module.nix @@ -0,0 +1,177 @@ +{ + config, + lib, + wlib, + ... +}: +let + # imported from home-manager + # https://github.com/nix-community/home-manager/blob/5a9efa93c586f79e80b0ad7d8036c450f53c3d1d/modules/lib/generators.nix#L4 + + toHyprconf = + { + attrs, + indentLevel ? 0, + importantPrefixes ? [ "$" ], + }: + let + inherit (lib) + all + concatMapStringsSep + concatStrings + concatStringsSep + filterAttrs + foldl + generators + hasPrefix + isAttrs + isList + mapAttrsToList + replicate + attrNames + ; + + initialIndent = concatStrings (replicate indentLevel " "); + + toHyprconf' = + indent: attrs: + let + isImportantField = + n: _: foldl (acc: prev: if hasPrefix prev n then true else acc) false importantPrefixes; + importantFields = filterAttrs isImportantField attrs; + withoutImportantFields = fields: removeAttrs fields (attrNames importantFields); + + allSections = filterAttrs (_n: v: isAttrs v || isList v) attrs; + sections = withoutImportantFields allSections; + + mkSection = + n: attrs: + if isList attrs then + let + separator = if all isAttrs attrs then "\n" else ""; + in + (concatMapStringsSep separator (a: mkSection n a) attrs) + else if isAttrs attrs then + '' + ${indent}${n} { + ${toHyprconf' " ${indent}" attrs}${indent}} + '' + else + toHyprconf' indent { ${n} = attrs; }; + + mkFields = generators.toKeyValue { + listsAsDuplicateKeys = true; + inherit indent; + }; + + allFields = filterAttrs (_n: v: !(isAttrs v || isList v)) attrs; + fields = withoutImportantFields allFields; + in + mkFields importantFields + + concatStringsSep "\n" (mapAttrsToList mkSection sections) + + mkFields fields; + in + toHyprconf' initialIndent attrs; + + hypridleConf = + (lib.optionalString (config.settings != { }) (toHyprconf { + attrs = config.settings; + importantPrefixes = config.importantPrefixes ++ lib.optional config.sourceFirst "source"; + })) + + lib.optionalString (config.extraConfig != "") config.extraConfig; + +in +{ + imports = [ wlib.modules.systemd ]; + + _class = "wrapper"; + + options = { + settings = lib.mkOption { + type = + with lib.types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "Hypridle configuration value"; + }; + in + valueType; + + default = { }; + + description = '' + Hypridle configuration written in Nix. Entries with the same key should + be written as lists. Variable names and colors should be quoted. See + for more examples. + ''; + }; + + importantPrefixes = lib.mkOption { + type = with lib.types; listOf str; + default = [ + "$" + ]; + + description = '' + List of important prefixes to source at the top of the config. + ''; + }; + + sourceFirst = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to put source entries at the top of the configuration."; + }; + + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Extra configuration lines to add to `hypridle.conf`. + ''; + }; + + systemdTarget = lib.mkOption { + type = lib.types.str; + default = "graphical-session.target"; + description = "Systemd target to bind to."; + }; + }; + + config.env = + let + # create directory structure that matches ~/.config/hypr/hypridle.conf + configDir = config.pkgs.runCommand "hypridle-config" { } '' + mkdir -p $out/hypr + cp ${config.pkgs.writeText "hypridle.conf" hypridleConf} $out/hypr/hypridle.conf + ''; + in + { + XDG_CONFIG_DIRS = "${configDir}"; + }; + + config.package = config.pkgs.hypridle; + config.filesToPatch = [ + "share/systemd/user/hypridle.service" + ]; + + config.meta.platforms = lib.platforms.linux; + + config.meta.maintainers = [ + { + name = "cooukiez"; + github = "cooukiez"; + githubId = 61082023; + } + ]; +} diff --git a/modules/hyprland/check.nix b/modules/hyprland/check.nix new file mode 100644 index 00000000..a78d345e --- /dev/null +++ b/modules/hyprland/check.nix @@ -0,0 +1,33 @@ +{ + pkgs, + self, +}: + +let + hyprlandWrapped = + (self.wrapperModules.hyprland.apply { + inherit pkgs; + + "hypr.conf".content = '' + + dwindle { + pseudotile = yes + preserve_split = yes + special_scale_factor = 0.95 + } + + general { + layout = dwindle + } + + ''; + }).wrapper; + +in +pkgs.runCommand "hypr-test" { } '' + + export XDG_RUNTIME_DIR=/run/user/$(id -u) + "${hyprlandWrapped}/bin/hyprland" --version | grep -q "${hyprlandWrapped.version}" + + touch $out +'' diff --git a/modules/hyprland/module.nix b/modules/hyprland/module.nix new file mode 100644 index 00000000..798ba8b0 --- /dev/null +++ b/modules/hyprland/module.nix @@ -0,0 +1,33 @@ +{ + config, + lib, + wlib, + ... +}: +{ + _class = "wrapper"; + options = { + "hypr.conf" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.content = ""; + description = '' + for basic setup of one hypr.conf file + ''; + }; + }; + + config.flags = { + "--config" = config."hypr.conf".path; + }; + + config.package = config.pkgs.hyprland; + + config.meta.maintainers = [ + { + name = "PeDro0210"; + github = "PeDro0210"; + githubId = 123851480; + } + ]; + config.meta.platforms = lib.platforms.linux; +} diff --git a/modules/hyprlock/check.nix b/modules/hyprlock/check.nix new file mode 100644 index 00000000..f025ac7c --- /dev/null +++ b/modules/hyprlock/check.nix @@ -0,0 +1,110 @@ +{ + pkgs, + self, +}: + +let + hyprlockWrapped = + (self.wrapperModules.hyprlock.apply { + inherit pkgs; + + # mostly https://github.com/hyprwm/hyprlock/blob/d332164dd97f4ae781f67d8aff8b1846ee46d671/assets/example.conf#L48 + settings = { + general = { + hide_cursor = false; + }; + + animations = { + enabled = true; + + fade_in = { + duration = 300; + bezier = "easeOutQuint"; + }; + + fade_out = { + duration = 300; + bezier = "easeOutQuint"; + }; + }; + + background = [ + { + monitor = ""; + path = "screenshot"; + blur_passes = 3; + } + ]; + + input-field = [ + { + monitor = ""; + size = "20%, 5%"; + outline_thickness = 3; + inner_color = "rgba(0, 0, 0, 0.0)"; + + outer_color = "rgba(33ccffee) rgba(00ff99ee) 45deg"; + check_color = "rgba(00ff99ee) rgba(ff6633ee) 120deg"; + fail_color = "rgba(ff6633ee) rgba(ff0066ee) 40deg"; + + font_color = "rgb(143, 143, 143)"; + fade_on_empty = false; + rounding = 15; + + font_family = "Monospace"; + placeholder_text = "Input password..."; + fail_text = "$PAMFAIL"; + + dots_spacing = 0.3; + + position = "0, -20"; + halign = "center"; + valign = "center"; + } + ]; + + label = [ + # TIME + { + monitor = ""; + text = "$TIME"; + font_size = 90; + font_family = "Monospace"; + + position = "-30, 0"; + halign = "right"; + valign = "top"; + } + # DATE + { + monitor = ""; + text = ''cmd[update:60000] date +"%A, %d %B %Y"''; + font_size = 25; + font_family = "Monospace"; + + position = "-30, -150"; + halign = "right"; + valign = "top"; + } + # LAYOUT + { + monitor = ""; + text = "$LAYOUT[en,ru]"; + font_size = 24; + onclick = "hyprctl switchxkblayout all next"; + + position = "250, -20"; + halign = "center"; + valign = "center"; + } + ]; + }; + }).wrapper; + +in +pkgs.runCommand "hyprlock-test" { } '' + export XDG_RUNTIME_DIR=/run/user/$(id -u) + "${hyprlockWrapped}/bin/hyprlock" --version | grep -q "${hyprlockWrapped.version}" + + touch $out +'' diff --git a/modules/hyprlock/module.nix b/modules/hyprlock/module.nix new file mode 100755 index 00000000..ed0807d5 --- /dev/null +++ b/modules/hyprlock/module.nix @@ -0,0 +1,167 @@ +{ + config, + lib, + wlib, + ... +}: +let + # imported from home-manager + # https://github.com/nix-community/home-manager/blob/5a9efa93c586f79e80b0ad7d8036c450f53c3d1d/modules/lib/generators.nix#L4 + + toHyprconf = + { + attrs, + indentLevel ? 0, + importantPrefixes ? [ "$" ], + }: + let + inherit (lib) + all + concatMapStringsSep + concatStrings + concatStringsSep + filterAttrs + foldl + generators + hasPrefix + isAttrs + isList + mapAttrsToList + replicate + attrNames + ; + + initialIndent = concatStrings (replicate indentLevel " "); + + toHyprconf' = + indent: attrs: + let + isImportantField = + n: _: foldl (acc: prev: if hasPrefix prev n then true else acc) false importantPrefixes; + importantFields = filterAttrs isImportantField attrs; + withoutImportantFields = fields: removeAttrs fields (attrNames importantFields); + + allSections = filterAttrs (_n: v: isAttrs v || isList v) attrs; + sections = withoutImportantFields allSections; + + mkSection = + n: attrs: + if isList attrs then + let + separator = if all isAttrs attrs then "\n" else ""; + in + (concatMapStringsSep separator (a: mkSection n a) attrs) + else if isAttrs attrs then + '' + ${indent}${n} { + ${toHyprconf' " ${indent}" attrs}${indent}} + '' + else + toHyprconf' indent { ${n} = attrs; }; + + mkFields = generators.toKeyValue { + listsAsDuplicateKeys = true; + inherit indent; + }; + + allFields = filterAttrs (_n: v: !(isAttrs v || isList v)) attrs; + fields = withoutImportantFields allFields; + in + mkFields importantFields + + concatStringsSep "\n" (mapAttrsToList mkSection sections) + + mkFields fields; + in + toHyprconf' initialIndent attrs; +in +{ + _class = "wrapper"; + + options = { + settings = lib.mkOption { + type = + with lib.types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "Hyprlock configuration value"; + }; + in + valueType; + + default = { }; + + description = '' + Hyprlock configuration written in Nix. Entries with the same key should + be written as lists. Variable names and colors should be quoted. See + for more examples. + ''; + }; + + importantPrefixes = lib.mkOption { + type = with lib.types; listOf str; + default = [ + "$" + "bezier" + "monitor" + "size" + ]; + + description = '' + List of important prefixes to source at the top of the config. + ''; + }; + + sourceFirst = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to put source entries at the top of the configuration."; + }; + + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Extra configuration lines to add to `hyprlock.conf`. + ''; + }; + }; + + config.env = + let + hyprlockConf = + (lib.optionalString (config.settings != { }) (toHyprconf { + attrs = config.settings; + importantPrefixes = config.importantPrefixes ++ lib.optional config.sourceFirst "source"; + })) + + lib.optionalString (config.extraConfig != "") config.extraConfig; + + # create directory structure that matches ~/.config/hypr/hyprlock.conf + configDir = config.pkgs.runCommand "hyprlock-config" { } '' + mkdir -p $out/hypr + cp ${config.pkgs.writeText "hyprlock.conf" hyprlockConf} $out/hypr/hyprlock.conf + ''; + in + { + XDG_CONFIG_DIRS = "${configDir}"; + }; + + config.package = config.pkgs.hyprlock; + config.meta.platforms = lib.platforms.linux; + + config.meta.maintainers = [ + { + name = "cooukiez"; + github = "cooukiez"; + githubId = 61082023; + } + ]; +} diff --git a/modules/i3/check.nix b/modules/i3/check.nix new file mode 100644 index 00000000..ba8b54c8 --- /dev/null +++ b/modules/i3/check.nix @@ -0,0 +1,32 @@ +{ + pkgs, + self, +}: + +let + i3Wrapped = + (self.wrapperModules.i3.apply { + inherit pkgs; + + configFile.content = '' + # Test config + set $mod Mod4 + bindsym $mod+Return exec alacritty + bindsym $mod+Shift+q kill + ''; + + }).wrapper; + +in +pkgs.runCommand "i3-test" { nativeBuildInputs = [ pkgs.dbus ]; } '' + + export DBUS_SESSION_BUS_ADDRESS="unix:path=$PWD/bus" + dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --nofork --nopidfile --print-address & + DBUS_PID=$! + + [[ "$(${i3Wrapped}/bin/i3 --version)" == *"${i3Wrapped.version}"* ]] + + kill $DBUS_PID 2>/dev/null || true + + touch $out +'' diff --git a/modules/i3/module.nix b/modules/i3/module.nix new file mode 100644 index 00000000..92ad05fc --- /dev/null +++ b/modules/i3/module.nix @@ -0,0 +1,28 @@ +{ + config, + lib, + wlib, + ... +}: +{ + _class = "wrapper"; + options = { + configFile = lib.mkOption { + type = wlib.types.file config.pkgs; + default.content = ""; + description = '' + I3 Window Manager configuration. + ''; + }; + }; + + config.flags = { + "--config" = config.configFile.path; + }; + + config.package = config.pkgs.i3; + config.meta.maintainers = [ + lib.maintainers.randomdude + ]; + config.meta.platforms = lib.platforms.linux; +} diff --git a/modules/inori/module.nix b/modules/inori/module.nix new file mode 100644 index 00000000..262e00d3 --- /dev/null +++ b/modules/inori/module.nix @@ -0,0 +1,46 @@ +{ + config, + lib, + wlib, + ... +}: +let + tomlFmt = config.pkgs.formats.toml { }; +in +{ + _class = "wrapper"; + options = { + settings = lib.mkOption { + type = tomlFmt.type; + default = { }; + description = '' + Configuration of inori. + See + ''; + }; + "config.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + # TODO add a pure toTOML function + default.path = tomlFmt.generate "config.toml" config.settings; + description = "inori configuration file."; + }; + }; + config = { + env.XDG_CONFIG_HOME = "${config.pkgs.linkFarm ([ + { + name = "inori/config.toml"; + inherit (config."config.toml") path; + } + ])}"; + package = config.pkgs.inori; + meta = { + maintainers = [ + { + name = "holly"; + github = "hollymlem"; + githubId = 35699052; + } + ]; + }; + }; +} diff --git a/modules/jujutsu/check.nix b/modules/jujutsu/check.nix index 5bced6d2..7f397702 100644 --- a/modules/jujutsu/check.nix +++ b/modules/jujutsu/check.nix @@ -16,7 +16,8 @@ let }).wrapper; in pkgs.runCommand "jujutsu-test" { } '' - "${jujutsuWrapped}/bin/jj" config list --user | grep -q 'user.name = "Test User"' - "${jujutsuWrapped}/bin/jj" config list --user | grep -q 'user.email = "test@example.com"' + config_list="$(${jujutsuWrapped}/bin/jj config list --user)" + [[ "$config_list" == *'user.name = "Test User"'* ]] + [[ "$config_list" == *'user.email = "test@example.com"'* ]] touch $out '' diff --git a/modules/kanshi/check.nix b/modules/kanshi/check.nix new file mode 100644 index 00000000..c412aed9 --- /dev/null +++ b/modules/kanshi/check.nix @@ -0,0 +1,32 @@ +{ + pkgs, + self, +}: + +let + kanshiConfig = pkgs.writeText "kanshi-test-config" '' + profile { + output eDP-1 enable scale 2 + } + ''; + + kanshiWrapped = + (self.wrapperModules.kanshi.apply { + inherit pkgs; + configFile.path = toString kanshiConfig; + }).wrapper; + +in +pkgs.runCommand "kanshi-test" { } '' + help_output="$(${kanshiWrapped}/bin/kanshi --help 2>&1)" + [[ "$help_output" == *config* ]] + + test -f "${kanshiConfig}" + grep -qF 'output eDP-1 enable scale 2' "${kanshiConfig}" + + wrapper_script=$(<"${kanshiWrapped}/bin/kanshi") + [[ "$wrapper_script" == *"--config"* ]] + [[ "$wrapper_script" == *"${kanshiConfig}"* ]] + + touch $out +'' diff --git a/modules/kanshi/module.nix b/modules/kanshi/module.nix new file mode 100644 index 00000000..24005b17 --- /dev/null +++ b/modules/kanshi/module.nix @@ -0,0 +1,35 @@ +{ + config, + lib, + wlib, + ... +}: +{ + _class = "wrapper"; + options = { + configFile = lib.mkOption { + type = wlib.types.file config.pkgs; + default.content = ""; + description = '' + Kanshi configuration file. + See {manpage}`kanshi(5)` for the configuration format. + ''; + example = '' + profile { + output LVDS-1 disable + output "Some Company ASDF 4242" mode 1600x900 position 0,0 + } + + profile nomad { + output LVDS-1 enable scale 2 + } + ''; + }; + }; + config.flags = { + "--config" = "${config.configFile.path}"; + }; + config.package = config.pkgs.kanshi; + config.meta.maintainers = [ lib.maintainers.adeci ]; + config.meta.platforms = lib.platforms.linux; +} diff --git a/modules/kitty/module.nix b/modules/kitty/module.nix index 19ee0a6b..843a36e4 100644 --- a/modules/kitty/module.nix +++ b/modules/kitty/module.nix @@ -53,7 +53,7 @@ in }; config.flags = { - "--config" = toString config."kitty.conf".path; + "--config" = config."kitty.conf".path; }; config.package = config.pkgs.kitty; diff --git a/modules/mako/module.nix b/modules/mako/module.nix index 8fb45bba..0ab92d57 100644 --- a/modules/mako/module.nix +++ b/modules/mako/module.nix @@ -5,7 +5,7 @@ ... }: let - iniFormat = config.pkgs.formats.iniWithGlobalSection { }; + iniFormat = config.pkgs.formats.iniWithGlobalSection { listsAsDuplicateKeys = true; }; iniAtomType = iniFormat.lib.types.atom; in { @@ -13,7 +13,10 @@ in options = { configFile = lib.mkOption { type = wlib.types.file config.pkgs; - default.path = iniFormat.generate "mako-settings" { globalSection = config.settings; }; + default.path = iniFormat.generate "mako-settings" { + globalSection = lib.filterAttrs (name: value: builtins.typeOf value != "set") config.settings; + sections = lib.filterAttrs (name: value: builtins.typeOf value == "set") config.settings; + }; }; settings = lib.mkOption { @@ -31,20 +34,25 @@ in ''; }; }; - - config.flagSeparator = "="; - config.flags = { - "--config" = toString config.configFile.path; + config = { + flagSeparator = "="; + flags = { + "--config" = config.configFile.path; + }; + package = config.pkgs.mako; + filesToPatch = [ + "share/dbus-1/services/fr.emersion.mako.service" + "share/systemd/user/mako.service" + ]; + meta = { + maintainers = [ + { + name = "altacountbabi"; + github = "altacountbabi"; + githubId = 82091823; + } + ]; + platforms = lib.platforms.linux; + }; }; - - config.package = config.pkgs.mako; - - config.meta.maintainers = [ - { - name = "altacountbabi"; - github = "altacountbabi"; - githubId = 82091823; - } - ]; - config.meta.platforms = lib.platforms.linux; } diff --git a/modules/mpv/check.nix b/modules/mpv/check.nix index c05b47fb..1348d4b7 100644 --- a/modules/mpv/check.nix +++ b/modules/mpv/check.nix @@ -15,6 +15,6 @@ let in pkgs.runCommand "mpv-test" { } '' - "${mpvWrapped}/bin/mpv" --version | grep -q "mpv" + [[ "$(${mpvWrapped}/bin/mpv --version)" == *mpv* ]] touch $out '' diff --git a/modules/mpv/module.nix b/modules/mpv/module.nix index 695f0868..17a3350c 100644 --- a/modules/mpv/module.nix +++ b/modules/mpv/module.nix @@ -12,7 +12,7 @@ default = [ ]; description = "Scripts to add to mpv via override."; }; - "mpv.input" = lib.mkOption { + "input.conf" = lib.mkOption { type = wlib.types.file config.pkgs; default.content = ""; }; @@ -23,8 +23,8 @@ }; config.flagSeparator = "="; config.flags = { - "--input-conf" = toString config."mpv.input".path; - "--include" = toString config."mpv.conf".path; + "--input-conf" = config."input.conf".path; + "--include" = config."mpv.conf".path; }; config.package = ( config.pkgs.mpv.override { diff --git a/modules/niri/check.nix b/modules/niri/check.nix index 6f88afba..afe46562 100644 --- a/modules/niri/check.nix +++ b/modules/niri/check.nix @@ -9,7 +9,12 @@ let inherit pkgs; settings = { binds = { - "Mod+T".spawn-sh = "alacritty"; + "Mod+T" = { + spawn-sh = "alacritty"; + _attrs = { + repeat = false; + }; + }; "Mod+J".focus-column-or-monitor-left = null; "Mod+N".spawn = [ "alacritty" @@ -70,6 +75,14 @@ let }; outputs = { + "DP-4" = { + position = { + _attrs = { + x = 1440; + y = 1080; + }; + }; + }; "DP-3" = { position = { x = 1440; @@ -98,7 +111,7 @@ let in pkgs.runCommand "niri-test" { } '' cat ${niriWrapped}/bin/niri - "${niriWrapped}/bin/niri" --version | grep -q "${niriWrapped.version}" + [[ "$(${niriWrapped}/bin/niri --version)" == *"${niriWrapped.version}"* ]] "${niriWrapped}/bin/niri" validate # since config is now checked at build time, testing a bad config is impossible touch $out diff --git a/modules/niri/module.nix b/modules/niri/module.nix index 8badca53..8508a44e 100644 --- a/modules/niri/module.nix +++ b/modules/niri/module.nix @@ -12,20 +12,26 @@ let leftpad = v: lib.strings.concatMapStrings (v: " ${v}\n") (lib.strings.splitString "\n" v); mkBlock = n: v: - if v != "" then - '' - ${n.name or n} ${ - # attrs must be qouted - let - attr = n._attrs or ""; - in - if attr != "" then ''"${attr}"'' else "" - } { + let + attrs = if builtins.isAttrs n then (n._attrs or "") else ""; + formattedAttrs = + if builtins.isAttrs attrs then + lib.concatMapAttrsStringSep " " (k: v: "${k}=${toVal v}") attrs + else if attrs != "" then + # string attrs must be quoted + ''"${attrs}"'' + else + ""; + body = lib.optionalString (v != "") '' + { ${leftpad v} - }'' - else - ""; - # surround strings with qoutes + } + ''; + in + '' + ${n.name or n} ${formattedAttrs} ${body} + ''; + # surround strings with quotes toVal = v: if lib.isString v then @@ -59,14 +65,12 @@ let else mkKeyVal n v ) a; - mkTagged = - t: k: v: - "${t} ${k}=${toVal v}"; + mkTagged = tag: attrs: "${tag} ${lib.concatMapAttrsStringSep " " (k: v: "${k}=${toVal v}") attrs}"; mkRule = block: r: let - matches = map (lib.concatMapAttrsStringSep "\n" (mkTagged "match")) r.matches or [ ]; - excludes = map (lib.concatMapAttrsStringSep "\n" (mkTagged "exclude")) r.excludes or [ ]; + matches = map (mkTagged "match") r.matches or [ ]; + excludes = map (mkTagged "exclude") r.excludes or [ ]; misc = attrsToKdl ( lib.attrsets.removeAttrs r [ "matches" @@ -128,7 +132,12 @@ in type = lib.types.attrs; description = "Bindings of niri"; example = { - "Mod+T".spawn-sh = "alacritty"; + "Mod+T" = { + spawn-sh = "alacritty"; + _attrs = { + repeat = false; + }; + }; "Mod+J".focus-column-or-monitor-left = null; "Mod+N".spawn = [ "alacritty" @@ -211,6 +220,12 @@ in example = { "DP-3" = { background-color = "#003300"; + position = { + _attrs = { + x = 0; + y = 0; + }; + }; hot-corners = { off = null; }; @@ -293,6 +308,17 @@ in "share/applications/*.desktop" "share/systemd/user/niri.service" ]; + config.patchHook = '' + chmod +w $out/share/systemd/user/niri.service + cat >> $out/share/systemd/user/niri.service< + ''; + }; + }; + + config = { + package = config.pkgs.tmux; + flags = { + "-f" = toString config."tmux.conf".path; + }; + + meta = { + maintainers = [ + { + name = "Skyler Oakeson"; + github = "skyler-oakeson"; + } + ]; + }; + }; +} diff --git a/modules/treefmt/check.nix b/modules/treefmt/check.nix new file mode 100644 index 00000000..9ca84d51 --- /dev/null +++ b/modules/treefmt/check.nix @@ -0,0 +1,21 @@ +{ + pkgs, + self, +}: +let + treefmtWrapped = + (self.wrapperModules.treefmt.apply { + inherit pkgs; + settings = { + formatter.nixfmt = { + command = "nixpkgs-fmt"; + includes = [ "*.nix" ]; + }; + }; + programs = [ pkgs.nixpkgs-fmt ]; + }).wrapper; +in +pkgs.runCommand "treefmt-test" { } '' + "${treefmtWrapped}/bin/treefmt" --version | grep -q "treefmt" + touch $out +'' diff --git a/modules/treefmt/module.nix b/modules/treefmt/module.nix new file mode 100644 index 00000000..178b99d7 --- /dev/null +++ b/modules/treefmt/module.nix @@ -0,0 +1,96 @@ +{ + config, + lib, + wlib, + ... +}: +let + tomlFmt = config.pkgs.formats.toml { }; +in +{ + _class = "wrapper"; + + options = { + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = tomlFmt.type; + options = { + excludes = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Global list of paths to exclude. Supports glob."; + }; + formatter = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + freeformType = tomlFmt.type; + options = { + command = lib.mkOption { + type = lib.types.str; + description = "Executable name or path obeying the treefmt formatter spec."; + }; + options = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Arguments to pass to the command."; + }; + includes = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "File patterns to include for formatting. Supports glob."; + }; + excludes = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "File patterns to exclude from formatting."; + }; + }; + } + ); + default = { }; + description = "Set of formatters to use."; + }; + }; + }; + default = { }; + description = '' + Structured treefmt configuration written to treefmt.toml. + See + ''; + }; + + programs = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = '' + Formatter packages to add to PATH. + ''; + }; + + "treefmt.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = tomlFmt.generate "treefmt.toml" ( + lib.filterAttrsRecursive (_: v: v != null) config.settings + ); + description = '' + The generated treefmt.toml configuration file. + ''; + }; + }; + + config.package = config.pkgs.treefmt; + + config.flags = { + "--config-file" = toString config."treefmt.toml".path; + }; + + config.extraPackages = config.programs; + + config.meta.maintainers = [ + { + name = "Alexander Kenji Berthold"; + github = "a-kenji"; + githubId = 65275785; + } + ]; + config.meta.platforms = lib.platforms.all; +} diff --git a/modules/udiskie/check.nix b/modules/udiskie/check.nix index 66d813fe..dbe39aeb 100644 --- a/modules/udiskie/check.nix +++ b/modules/udiskie/check.nix @@ -16,6 +16,6 @@ let }).wrapper; in pkgs.runCommand "udiskie-test" { } '' - "${udiskieWrapped}/bin/udiskie" --version | grep -q "udiskie" + [[ "$(${udiskieWrapped}/bin/udiskie --version)" == *udiskie* ]] touch $out '' diff --git a/modules/udiskie/module.nix b/modules/udiskie/module.nix index 0ec0f26e..717ac667 100644 --- a/modules/udiskie/module.nix +++ b/modules/udiskie/module.nix @@ -30,7 +30,7 @@ in config.flagSeparator = "="; config.flags = { - "--config" = toString config."config.yml".path; + "--config" = config."config.yml".path; }; config.exePath = "${config.package}/bin/udiskie"; diff --git a/modules/voxtype/check.nix b/modules/voxtype/check.nix new file mode 100644 index 00000000..8e5caee2 --- /dev/null +++ b/modules/voxtype/check.nix @@ -0,0 +1,16 @@ +{ + pkgs, + self, +}: + +let + voxtypeWrapped = + (self.wrapperModules.voxtype.apply { + inherit pkgs; + }).wrapper; +in +pkgs.runCommand "voxtype-test" { } '' + # this will fail, if the default config is off + "${voxtypeWrapped}/bin/voxtype" config + touch $out +'' diff --git a/modules/voxtype/module.nix b/modules/voxtype/module.nix new file mode 100644 index 00000000..b4c9aa56 --- /dev/null +++ b/modules/voxtype/module.nix @@ -0,0 +1,85 @@ +{ + config, + lib, + wlib, + ... +}: +let + tomlFmt = config.pkgs.formats.toml { }; + defaultSettings = { + hotkey = { }; + audio = { + device = "default"; + sample_rate = 16000; + max_duration_secs = 60; + }; + output = { + mode = "type"; + }; + }; +in +{ + _class = "wrapper"; + options = { + settings = lib.mkOption { + type = tomlFmt.type; + default = defaultSettings; + description = '' + Configuration of voxtype. + Default has all minimally required options set.` + ''; + example = + let + pkgs = config.pkgs; + in + { + engine = "parakeet"; + parakeet.model = + let + parakeetBaseUrl = "https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx/resolve/main"; + in + pkgs.linkFarm "parakeet-tdt-0.6b-v3" [ + { + name = "encoder-model.onnx"; + path = pkgs.fetchurl { + url = "${parakeetBaseUrl}/encoder-model.int8.onnx"; + hash = "sha256-YTnS+n4bCGCXsnfHFJcl7bq4nMfHrmSyPHQb5AVa/wk="; + }; + } + { + name = "decoder_joint-model.onnx"; + path = pkgs.fetchurl { + url = "${parakeetBaseUrl}/decoder_joint-model.int8.onnx"; + hash = "sha256-7qdIPuPRowN12u3I7YPjlgyRsJiBISeg2Z0ciXdmenA="; + }; + } + { + name = "vocab.txt"; + path = pkgs.fetchurl { + url = "${parakeetBaseUrl}/vocab.txt"; + hash = "sha256-1YVEZ56kvGrFY9H1Ret9R0vWz6Rn8KbiwdwcfTfjw10="; + }; + } + { + name = "config.json"; + path = pkgs.fetchurl { + url = "${parakeetBaseUrl}/config.json"; + hash = "sha256-ZmkDx2uXmMrywhCv1PbNYLCKjb+YAOyNejvA0hSKxGY="; + }; + } + ]; + hotkey.enabled = false; + }; + }; + "voxtype.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = tomlFmt.generate "voxtype.toml" (defaultSettings // config.settings); + }; + }; + config.flags = { + "--config" = config."voxtype.toml".path; + }; + config.package = config.pkgs.voxtype; + config.meta.maintainers = [ lib.maintainers.lenny ]; + config.meta.platforms = lib.platforms.linux; # voxtype not packaged on darwin atm +} diff --git a/modules/way-edges/module.nix b/modules/way-edges/module.nix new file mode 100644 index 00000000..f27236ac --- /dev/null +++ b/modules/way-edges/module.nix @@ -0,0 +1,64 @@ +{ + config, + lib, + wlib, + ... +}: +let + jsonFmt = config.pkgs.formats.json { }; +in +{ + _class = "wrapper"; + options = { + settings = lib.mkOption { + type = jsonFmt.type; + default = { }; + description = '' + way-edges settings + see + ''; + example = { + widgets = [ + { + namespace = "workspaces"; + monitor = "*"; + edge = "left"; + position = "top"; + layer = "top"; + active-increase = 0; + default-color = "#1e1e2e"; + focus-color = "#f5c2e7"; + hover-color = "#f5c2e7"; + length = 200; + preset = { + type = "niri"; + }; + thickness = 8; + type = "workspace"; + } + ]; + }; + }; + configFile = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = jsonFmt.generate "way-edges-config" config.settings; + description = '' + way-edges config file + see + ''; + }; + }; + config = { + package = config.pkgs.way-edges; + flags = { + "--config-path" = config.configFile.path; + }; + meta.maintainers = [ + { + name = "holly"; + github = "hollymlem"; + githubId = 35699052; + } + ]; + }; +} diff --git a/modules/waybar/module.nix b/modules/waybar/module.nix index 86c965f1..57cfce48 100644 --- a/modules/waybar/module.nix +++ b/modules/waybar/module.nix @@ -56,8 +56,8 @@ in config.package = lib.mkDefault config.pkgs.waybar; config.flags = { - "--config" = toString config.configFile.path; - "--style" = toString config."style.css".path; + "--config" = config.configFile.path; + "--style" = config."style.css".path; }; config.filesToPatch = [ "share/systemd/user/waybar.service" diff --git a/modules/yazi/check.nix b/modules/yazi/check.nix new file mode 100644 index 00000000..889cb61a --- /dev/null +++ b/modules/yazi/check.nix @@ -0,0 +1,14 @@ +{ + pkgs, + self, +}: +let + yaziWrapped = + (self.wrapperModules.yazi.apply { + inherit pkgs; + }).wrapper; +in +pkgs.runCommand "yazi-test" { } '' + "${yaziWrapped}/bin/yazi" --version | grep -q "${yaziWrapped.version}" + touch $out +'' diff --git a/modules/yazi/module.nix b/modules/yazi/module.nix new file mode 100644 index 00000000..adf61589 --- /dev/null +++ b/modules/yazi/module.nix @@ -0,0 +1,98 @@ +{ + wlib, + lib, + config, + ... +}: +let + tomlFmt = config.pkgs.formats.toml { }; +in +{ + options = { + settings = lib.mkOption { + type = tomlFmt.type; + default = { }; + description = '' + General settings. + See + ''; + }; + + keymap = lib.mkOption { + type = tomlFmt.type; + default = { }; + description = '' + Keymap settings. + See + ''; + }; + + theme = lib.mkOption { + type = tomlFmt.type; + default = { }; + description = '' + Theming. + See + ''; + }; + + "yazi.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = tomlFmt.generate "yazi.toml" config.settings; + }; + "keymap.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = tomlFmt.generate "keymap.toml" config.keymap; + }; + "theme.toml" = lib.mkOption { + type = wlib.types.file config.pkgs; + default.path = tomlFmt.generate "theme.toml" config.theme; + }; + + extraFiles = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.nonEmptyStr; + description = "File name in the config directory"; + }; + file = lib.mkOption { + type = wlib.types.file config.pkgs; + description = "File or path to add into the config directory"; + }; + }; + } + ); + default = [ ]; + description = "Additional files to be placed in the config directory"; + }; + }; + + config = { + package = lib.mkDefault config.pkgs.yazi; + env.YAZI_CONFIG_HOME = toString ( + config.pkgs.linkFarm "yazi-merged-config" ( + let + entry = name: path: { inherit name path; }; + in + [ + (entry "yazi.toml" config."yazi.toml".path) + (entry "keymap.toml" config."keymap.toml".path) + (entry "theme.toml" config."theme.toml".path) + ] + ++ (map (f: { + inherit (f) name; + path = f.file.path; + }) config.extraFiles) + ) + ); + meta.maintainers = [ + { + name = "holly"; + github = "hollymlem"; + githubId = 35699052; + } + ]; + }; +} diff --git a/modules/zathura/module.nix b/modules/zathura/module.nix new file mode 100644 index 00000000..d81c4677 --- /dev/null +++ b/modules/zathura/module.nix @@ -0,0 +1,123 @@ +{ + config, + lib, + wlib, + ... +}: +let + formatLine = + n: v: + let + formatValue = v: if lib.isBool v then (if v then "true" else "false") else toString v; + in + ''set ${n} "${formatValue v}"''; + + formatMapLine = n: v: "map ${n} ${toString v}"; +in +{ + _class = "wrapper"; + options = { + settings = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.str + lib.types.bool + lib.types.int + lib.types.float + ] + ); + default = { }; + description = '' + Add {option}`:set` command options to zathura and make + them permanent. See + {manpage}`zathurarc(5)` + for the full list of options. + ''; + example = { + default-bg = "#000000"; + default-fg = "#FFFFFF"; + }; + }; + mappings = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Add {option}`:map` mappings to zathura and make + them permanent. See + {manpage}`zathurarc(5)` + for the full list of possible mappings. + + You can create a mode-specific mapping by specifying the mode before the key: + `"[normal] " = "scroll left";` + ''; + example = { + D = "toggle_page_mode"; + "" = "navigate next"; + "[fullscreen] " = "zoom in"; + }; + }; + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Additional commands for zathura that will be added to the + {file}`zathurarc` file. + ''; + }; + "zathurarc" = lib.mkOption { + type = wlib.types.file config.pkgs; + description = "zathura config file"; + default.content = + lib.concatStringsSep "\n" ( + lib.optional (config.extraConfig != "") config.extraConfig + ++ lib.mapAttrsToList formatLine config.settings + ++ lib.mapAttrsToList formatMapLine config.mappings + ) + + "\n"; + }; + extraFiles = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.nonEmptyStr; + description = "File name in the config directory"; + }; + file = lib.mkOption { + type = wlib.types.file config.pkgs; + description = "File or path to add into the config directory"; + }; + }; + } + ); + default = [ ]; + description = "Additional files to be placed in the config directory"; + }; + }; + config = { + flags = { + "--config-dir" = toString ( + config.pkgs.linkFarm "zathura-merged-config" ( + let + entry = name: path: { inherit name path; }; + in + [ (entry "zathurarc" config."zathurarc".path) ] + ++ (map (f: { + inherit (f) name; + path = f.file.path; + }) config.extraFiles) + ) + ); + }; + package = config.pkgs.zathura; + meta = { + maintainers = [ + { + name = "holly"; + github = "hollymlem"; + githubId = 35699052; + } + ]; + }; + }; +} diff --git a/modules/zsh/check.nix b/modules/zsh/check.nix index ce1144d6..424efe55 100644 --- a/modules/zsh/check.nix +++ b/modules/zsh/check.nix @@ -13,6 +13,10 @@ let hdon = "hyprctl dispatch dpms on"; ls = "eza --icons"; }; + completion = { + enable = true; + caseInsensitive = true; + }; env = { NH_OS_FLAKE = "~/nixos-config"; }; diff --git a/modules/zsh/module.nix b/modules/zsh/module.nix index 9503f018..df9d0cd8 100644 --- a/modules/zsh/module.nix +++ b/modules/zsh/module.nix @@ -33,20 +33,128 @@ in description = "cd into a directory just by typing it in"; }; + integrations = { + fzf = { + enable = lib.mkEnableOption "fzf"; + package = lib.mkOption { + type = lib.types.package; + default = config.pkgs.fzf; + }; + }; + atuin = { + enable = lib.mkEnableOption "atuin"; + package = lib.mkOption { + type = lib.types.package; + default = config.pkgs.atuin; + }; + }; + oh-my-posh = { + enable = lib.mkEnableOption "oh-my-posh"; + package = lib.mkOption { + type = lib.types.package; + default = config.pkgs.oh-my-posh; + }; + }; + starship = { + enable = lib.mkEnableOption "starship"; + package = lib.mkOption { + type = lib.types.package; + default = config.pkgs.starship; # Or self'.packages.starship, assuming you use flake parts + }; + }; + pay-respects = { + enable = lib.mkEnableOption "pay-respects"; + package = lib.mkOption { + type = lib.types.package; + default = config.pkgs.pay-respects; + }; + flags = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + }; + }; + zoxide = { + enable = lib.mkEnableOption "zoxide"; + package = lib.mkOption { + type = lib.types.package; + default = config.pkgs.zoxide; + }; + flags = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + }; + }; + }; + + completion = { + enable = lib.mkEnableOption "completions"; + init = lib.mkOption { + default = "autoload -U compinit && compinit"; + description = "Initialization commands to run when completion is enabled."; + type = lib.types.lines; + }; + extraCompletions = lib.mkOption { + type = lib.types.bool; + default = false; + description = "enable to add zsh-completions package, it has extra completions for other tools"; + }; + colors = lib.mkOption { + type = lib.types.bool; + default = false; + description = "make the completions colorful (as if you were using ls --color)"; + }; + caseInsensitive = lib.mkEnableOption "completions"; + fuzzySearch = lib.mkEnableOption "fuzzy-completion"; + }; + + autoSuggestions = { + enable = lib.mkEnableOption "autoSuggestions"; + highlight = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "fg=#ff00ff,bg=cyan,bold,underline"; + description = "Custom styles for autosuggestion highlighting"; + }; + + strategy = lib.mkOption { + type = lib.types.listOf ( + lib.types.enum [ + "history" + "completion" + "match_prev_cmd" + ] + ); + default = [ + "history" + "completion" + ]; + }; + }; + history = { append = lib.mkOption { type = lib.types.bool; default = true; description = "append history for every new session instead of replacing it"; }; + file = lib.mkOption { + type = lib.types.str; + default = "$HOME/.zsh_history"; + description = "location to save the history"; + }; expanded = lib.mkOption { type = lib.types.bool; default = false; description = "save timestamps with history"; }; + share = lib.mkOption { + type = lib.types.bool; + default = false; + description = "share history between sessions"; + }; save = lib.mkOption { type = lib.types.int; - default = 10000; + default = cfg.history.size; description = "the number of history lines to save"; }; size = lib.mkOption { @@ -96,30 +204,86 @@ in description = "environment variables to put in .zshenv as an attribute set of strings just like environment.systemVariables"; }; }; + extraRC = lib.mkOption { type = lib.types.lines; default = ""; description = "extra stuff to put in .zshrc, gets appended *after* all of the options"; }; - ".zshrc" = lib.mkOption { - type = wlib.types.file config.pkgs; - default.content = builtins.concatStringsSep "\n" [ - ( - if cfg.keyMap == "viins" then - "bindkey -a" - else if cfg.keyMap == "vicmd" then - "bindkey -v" - else - "bindkey -e" - ) - (lib.concatMapAttrsStringSep "\n" (k: v: ''alias -- ${k}="${v}"'') cfg.shellAliases) - (if cfg.autocd then "setopt autocd" else "") - "HISTSIZE=${toString cfg.history.size}" - "HISTSAVE=${toString cfg.history.save}" - config.extraRC - ]; - }; + ".zshrc" = + let + zoxide-flags = lib.concatStringsSep " " cfg.integrations.zoxide.flags; + pay-respects-flags = lib.concatStringsSep " " cfg.integrations.pay-respects.flags; + ing = cfg.integrations; + in + lib.mkOption { + type = wlib.types.file config.pkgs; + default.content = builtins.concatStringsSep "\n" [ + "# KeyMap" + ( + if cfg.keyMap == "viins" then + "bindkey -a" + else if cfg.keyMap == "vicmd" then + "bindkey -v" + else + "bindkey -e" + ) + (lib.optionalString cfg.autocd "setopt autocd") + + "# Aliases" + + (lib.concatMapAttrsStringSep "\n" (k: v: ''alias -- ${k}="${v}"'') cfg.shellAliases) + + "# Completion" + (lib.optionalString cfg.completion.enable cfg.completion.init) + (lib.optionalString cfg.completion.caseInsensitive "zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' ") + (lib.optionalString cfg.completion.colors "zstyle ':completion:*' list-colors \"$\{(s.:.)LS_COLORS\}\" ") + (lib.optionalString cfg.completion.fuzzySearch '' + zstyle ':completion:*' menu no + source ${config.pkgs.zsh-fzf-tab}/share/fzf-tab/fzf-tab.plugin.zsh + '') + + "#Autosuggestions" + (lib.optionalString cfg.autoSuggestions.enable '' + source ${config.pkgs.zsh-autosuggestions}/share/zsh-autosuggestions/zsh-autosuggestions.zsh + ${lib.optionalString (cfg.autoSuggestions.strategy != [ ]) '' + ZSH_AUTOSUGGEST_STRATEGY=(${lib.concatStringsSep " " cfg.autoSuggestions.strategy}) + ''} + + ${lib.optionalString (cfg.autoSuggestions.highlight != null) '' + ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE=(${cfg.autoSuggestions.highlight}) + ''} + '') + + "# Integrations" + (lib.optionalString ing.fzf.enable "source <(fzf --zsh)") + (lib.optionalString ing.atuin.enable ''eval "$(atuin init zsh)"'') + (lib.optionalString ing.oh-my-posh.enable ''eval "$(oh-my-posh init zsh)"'') + (lib.optionalString ing.zoxide.enable ''eval "$(zoxide init zsh ${zoxide-flags})"'') + (lib.optionalString ing.pay-respects.enable ''eval "$(pay-respects zsh ${pay-respects-flags})"'') + (lib.optionalString ing.starship.enable ''eval "$(starship init zsh)"'') + + "# History" + + "HISTSIZE=${toString cfg.history.size}" + "SAVEHIST=${toString cfg.history.save}" + "HISTFILE=${toString cfg.history.file}" + + (lib.optionalString cfg.history.append "setopt appendhistory") + (lib.optionalString cfg.history.share "setopt sharehistory") + (lib.optionalString cfg.history.ignoreSpace "setopt hist_ignore_space") + (lib.optionalString cfg.history.ignoreAllDups "setopt hist_ignore_all_dups") + (lib.optionalString cfg.history.ignoreDups "setopt hist_ignore_dups") + (lib.optionalString cfg.history.saveNoDups "setopt hist_save_no_dups") + (lib.optionalString cfg.history.findNoDups "setopt histfindnodups") + (lib.optionalString cfg.history.expanded "setopt extendedhistory") + + "# Extra Content" + + config.extraRC + ]; + }; ".zshenv" = lib.mkOption { type = wlib.types.file config.pkgs; @@ -128,19 +292,28 @@ in ]; }; }; + config = { package = config.pkgs.zsh; + extraPackages = + let + ing = cfg.integrations; + in + lib.optional ing.fzf.enable ing.fzf.package + ++ lib.optional ing.atuin.enable ing.atuin.package + ++ lib.optional ing.zoxide.enable ing.zoxide.package + ++ lib.optional ing.oh-my-posh.enable ing.oh-my-posh.package + ++ lib.optional ing.starship.enable ing.starship.package + ++ lib.optional ing.pay-respects.enable ing.pay-respects.package + ++ lib.optional cfg.completion.enable config.pkgs.nix-zsh-completions + ++ lib.optional cfg.completion.extraCompletions config.pkgs.zsh-completions + ++ lib.optional cfg.completion.fuzzySearch config.pkgs.zsh-fzf-tab; + flags = { "--histfcntllock" = true; - "--histappend" = cfg.history.append; "--histexpiredupsfirst" = cfg.history.expireDupsFirst; - "--histfindnodups" = cfg.history.findNoDups; - "--histignorealldups" = cfg.history.ignoreAllDups; - "--histignoredups" = cfg.history.ignoreDups; - "--histignorespace" = cfg.history.ignoreSpace; - "--histsavenodups" = cfg.history.saveNoDups; - "--histexpand" = cfg.history.expanded; }; + env.ZDOTDIR = builtins.toString ( config.pkgs.linkFarm "zsh-merged-config" [ {