diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 1d703f27..1d7148ab 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -12,14 +12,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - name: Update Site run: | nix run --show-trace ./ci#docs - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: ./_site @@ -36,6 +36,6 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 with: token: ${{ secrets.PAT }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d79ebcc2..256cd120 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Adding Modules! +## Adding Modules! There are 2 kinds of modules in this repository. One kind which defines the `package` option, and one kind which does not. @@ -6,7 +6,7 @@ There are 2 kinds of modules in this repository. One kind which defines the `pac If you are making a wrapper module, i.e. one which **does** define the `config.package` option, and thus wraps a package: -You must define a `wrapperModules///wrapper.nix` file. +You must define a `wrapperModules///module.nix` file. The file must contain a single, unevaluated module. In other words, it must be importable without calling it like a function first. All wrapper modules must have a `config.meta.maintainers = [ ];` entry. @@ -45,31 +45,34 @@ You may optionally set the `meta.description` option to provide a short descript `pre` will be added after the title and before the content. `post` will be added after the content. +Do not name an option `options` or `config` if there is a chance the module system will try to use them as if they were top level module declarations. + ## Guidelines and Examples: When you provide an option to `enable` or `disable` something, you should call it `enable` regardless of its default value. This prevents people from needing to look it up to use it, and prevents contributors from having to think too hard about which to call it. -When you provide a `wlib.types.file` option, you should name it the actual filename, especially if there are multiple, but `configFile` is also OK, especially if it is unambiguous. +- Placeholders and `config.constructFiles.` -Keep in mind that even if you do not choose to use `wlib.types.file`, the user can usually still override the option that you set to provide the generated path if needed. +When you generate a file, it is generally better to do so as a string, and create it using the `constructFiles` option. -However, this makes the user of your module search for it, and in some situations, such as when your module is adding stuff to `list` or `DAL` type options, this can be slightly harder to override later. +This is because, this will make placeholders such as `${placeholder "out"}` work consistently across all your options, +allowing them to all point to the final wrapper derivation rather than several intermediate ones. -So making use of the `wlib.types.file` type or giving some other method of overriding the filepath when providing a file is generally recommended for this reason. +What this allows you to do, is manually build files via another option like `constructFiles`, and then refer to that created file within your settings! -- Placeholders +Making placeholders work in your module makes your modules generally more easily extensible, and is preferred when it is possible to generate a usable string. -When you generate a file, it is generally better to do so as a string, and create it using the `constructFiles` option. +It works by using `drv.passAsFile` and making a derivation attribute with the file contents, which is copied into place. -This is because, this will make placeholders such as `${placeholder "out"}` work consistently across all your options. +- `wlib.types.file` -What this allows you to do, is manually build files later using `buildCommand` option or a stdenv phase, and then refer to that created file within your settings! +When you provide a `wlib.types.file` option, you should name it the actual filename or something suggestive of it, especially if there are multiple, but `configFile` is also OK, especially if it is unambiguous. -Making placeholders work in your module makes your modules generally more easily extensible, and is preferred when it is possible to generate a usable string. +Keep in mind that even if you do not choose to use `wlib.types.file`, the user can usually still override the option that you set to provide the generated path if needed. -It works by using `drv.passAsFile` and making a derivation attribute with the file contents, which is copied into place. +So using something like `wlib.types.file` is only truly important when the file you are making an option for is passed to a list-style option, but may still be nice more generally. Example: @@ -93,11 +96,11 @@ Example: ''; }; configFile = lib.mkOption { - type = wlib.types.file pkgs; - default = { - path = config.constructFiles.gitconfig.path; # <- we can refer to the placeholder of our constructed file! - content = ""; + type = wlib.types.file { + # we can refer to the placeholder of our constructed file! + path = lib.mkOptionDefault config.constructFiles.gitconfig.path; }; + default = { }; description = "Generated git configuration file."; }; }; @@ -118,15 +121,15 @@ Example: } ``` -# Formatting +## Formatting `nix fmt` -# Tests +## Tests `nix flake check -Lv ./ci` -# Run Site Generator Locally +## Run Site Generator Locally `nix run ./ci` @@ -134,59 +137,130 @@ or `nix run ./ci#docs` -# Writing tests +To run the tests for an individual wrapper only, run + +`nix build ./ci#checks.{system}.wrapperModule-{name}` + +Example (neovim on `x86_64-linux`): + +`nix build ./ci#checks.x86_64-linux.wrapperModule-neovim` + +## Writing Tests You may also include a `check.nix` file in your module's directory. -It will be provided with the flake `self` value and `pkgs` +It will be called via `pkgs.callPackage`, provided with the flake `self` value, as well as a test-library `tlib` value. +(i.e. `pkgs.callPackage your_check.nix { inherit self tlib; }`) -It should build a derivation which tests the wrapper derivation as best you can. -If a command fails, it fails the test. If it builds the derivation successfully, it passes the test. +We provide a testing library `tlib` that provides an easy-to-use interface to write tests. -If the program gives options for running the program to check the generated configuration is correct, you should do that. +### Writing Tests for Wrappers -Sometimes it is not easily possible to run the program within a derivation, in those cases, searching the wrapper derivation and other generated files and their contents is also acceptable. +If you are writing tests for a wrapper module, it is important to pass the name +of the wrapper to the first argument of the `test` function like in the example +below (marked at `(*)`). By doing this, we can grab the specified `wrapper.meta.platforms` config +of the wrapper (if any) and ensure that the tests are only run on the required platforms. -Example: ```nix { pkgs, self, + tlib, + ... }: + let - gitWrapped = self.wrappers.git.wrap { - inherit pkgs; - settings = { - user = { - name = "Test User"; - email = "test@example.com"; + inherit (tlib) + fileContains + isDirectory + isFile + notIsFile + areEqual + test + ; +in +test { wrapper = "direnv"; } { # <-- Specify the name of the wrapper here (*) + + "direnv wrapper should be created" = + let + wrapper = self.wrappers.direnv.wrap { + inherit pkgs; + nix-direnv.enable = true; }; - }; + in + [ + "[[ -d ${wrapper} ]]" # <-- a simple condition to be asserted + { + cond = "[[ -d ${wrapper} ]]"; + msg = "No directory found for wrapper."; # <-- you can also specify a custom error message + } + (isDirectory wrapper) # <-- or use pre-defined helpers + ]; + + "wrapper should output correct version" = + let + wrapper = self.wrappers.direnv.wrap { + inherit pkgs; + }; + in + '' # <-- no need to provide a list if there is only one assertion + "${wrapper}/bin/direnv" --version | + grep -q "${wrapper.version}" + ''; + + "math-tests" = { # <-- tests can be arbitrarily grouped + addition = [ + (areEqual 2 (1 + 1)) + (areEqual 7 (5 + 2)) + ]; + multiplication = [ + (areEqual 1 (1 * 1)) + (areEqual 10 (5 * 2)) + ]; }; - -in -pkgs.runCommand "git-test" { } '' - "${gitWrapped}/bin/git" config user.name | grep -q "Test User" - "${gitWrapped}/bin/git" config user.email | grep -q "test@example.com" - touch $out -'' +} ``` -If your module declares a list of valid platforms via its `meta.platforms` option, you should disable your test on the relevant platforms like so: + +Pre-defined assertions like `isDirectory` or `areEqual` are already available in tlib. +Feel free to contribute more if you find new ones that other maintainers might benefit from. + +### Writing Tests for Helper Modules or Library Functions + +The syntax is identical to [the example above](#writing-tests-for-wrappers), +except you don't provide a wrapper but a name: ```nix -if builtins.elem pkgs.stdenv.hostPlatform.system self.wrappers.waybar.meta.platforms then - pkgs.runCommand "waybar-test" { } '' - "${waybarWrapped}/bin/waybar" --version | grep -q "${waybarWrapped.version}" - touch $out - '' -else - null +{ + pkgs, + self, + tlib, + ... +}: + +let + inherit (tlib) + fileContains + isDirectory + isFile + notIsFile + areEqual + test + ; +in +test "my-test" { # <-- Specify an arbitrary name for your test +# test { name = "my-test" } { # <-- This is equivalent + + "my first test" = [ ... ]; # <-- nothing new here + "my second test" = [ ... ]; +} ``` -# Commit Messages +If you are writing a helper module, or something very complex, you may wish to have multiple derivations. Simply return a set of them instead. + +## Commit Messages Changes to wrapper modules should be titled `(wrapperModules.): some description`. For new additions, the description should be `init`, with any further explanation on subsequent lines @@ -206,6 +280,6 @@ For everything else, do the best you can to follow conventional commit message s Why specify this? I was having trouble figuring out what to title my commits. So now I know. -# Questions? +## Questions? The [github discussions board](https://github.com/BirdeeHub/nix-wrapper-modules/discussions) is open and a great place to find help! diff --git a/README.md b/README.md index de668522..e759103c 100644 --- a/README.md +++ b/README.md @@ -108,14 +108,48 @@ For more information on how to do this, check out the [getting started](https:// It is the ideal of this project to become a hub for everyone to contribute, so that we can all enjoy our portable configurations with as little individual strife as possible. -In service of that ideal, the immediate goal would be to transfer this repo to nix-community the moment that becomes an option. +In service of that ideal, the plan is that ownership will be transferred to nix-community, +so that there is community ownership of where our contributions will be maintained. -Eventually I hope to have wrapper modules in nixpkgs, but again, nix-community would be the first step. +The road-map before beginning that process consists of at least most of the following items: + +- Better doc-generation options, less buggy and made more available to individual modules outside of the main repository. +- Services options for generating service files which can be installed by passing the package to the correct option. +- Non-intrusive `bubblewrap` helper module, for programs that are difficult to wrap. +- Better documentation in general. Things should already be covered in the docs, but not yet always in a way digestible for everyone. +- Maybe 1 or 2 other things. + +Once the dust has settled, the process will be started to move it to nix-community, +and we will start building a core team to maintain the repository long into the future! ## Short-term Goals Help us add more modules! Contributors are what makes projects like these which contain modules for so many programs amazing! +## Related Extension Projects: + +There may be projects that offer useful additions or pre-configurations of existing modules, +or new ways of using wrapper modules that do not yet fit in the main repository. + +For example, a collection of Neovim modules for various languages would be a good item for this list + +- [hm-wrapper-modules](https://github.com/sini/hm-wrapper-modules) + - This is a library designed to run home manager modules, figure out what paths it would add, + and use `bubblewrap` and a wrapper module to use the home manager module as a wrapper module. + - This repository may be useful to create wrapper modules for difficult to wrap programs until more direct ones can be written. + - It does not fit in the main repository, because the correct way to do it is to add a `bubblewrap` helper module to the main repository, + and figure out what files that program actually needs us to wrap via `bubblewrap` or not. + +Hopefully more will be listed here soon! + +Some examples why something may not fit in the main repository: + +- It doesn't fit in an existing category of offered things. + +- It requires more flake inputs beyond `nixpkgs` in order to import it from this repository and use it somewhere. + +- It is a never ending job of its own (i.e. making modules for new editor language integrations and plugins for an existing wrapper module like the Neovim one) + --- ### Why rewrite [lassulus/wrappers](https://github.com/Lassulus/wrappers)? diff --git a/ci/checks/all-modules-have-maintainers.nix b/ci/checks/all-modules-have-maintainers.nix index 8331fa51..cb952874 100644 --- a/ci/checks/all-modules-have-maintainers.nix +++ b/ci/checks/all-modules-have-maintainers.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/apply.nix b/ci/checks/apply.nix index 63320c2e..093b1d2a 100644 --- a/ci/checks/apply.nix +++ b/ci/checks/apply.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: # TODO: make sure theres no other stuff passing on accident in here let diff --git a/ci/checks/formatting.nix b/ci/checks/formatting.nix index 9b2c161b..3a7d0e50 100644 --- a/ci/checks/formatting.nix +++ b/ci/checks/formatting.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: pkgs.runCommand "formatting-check" { } '' diff --git a/ci/checks/makeCustomizable.nix b/ci/checks/makeCustomizable.nix index 02d61343..764b9098 100644 --- a/ci/checks/makeCustomizable.nix +++ b/ci/checks/makeCustomizable.nix @@ -1,75 +1,35 @@ { pkgs, self, + tlib, + ... }: let - luaEnv = self.lib.makeCustomizable "withPackages" { - mergeArgs = - og: new: lp: - og lp ++ new lp; - } pkgs.luajit.withPackages (lp: [ lp.inspect ]); - - # inspect + cjson - luaEnv2 = luaEnv.withPackages (lp: [ lp.cjson ]); - # inspect + cjson + luassert - luaEnv3 = luaEnv2.withPackages (lp: [ lp.luassert ]); - # inspect + cjson + luassert + luafilesystem - luaEnv4 = luaEnv3.withPackages (lp: [ lp.luafilesystem ]); - - getPkgs = v: pkgs.lib.escapeShellArg v.drvAttrs.pkgs; + inherit (tlib) + areEqual + test + ; + testfunctor = self.lib.makeCustomizable "test" { } (v: { value = v; }) { some = "args"; }; + + testfunctor2 = testfunctor.test (lp: { + more = lp.some; + }); + testfunctor3 = testfunctor.test (lp: { + more = "with overriding"; + }); + testfunctor4 = testfunctor3.test { again = "testing"; }; in -pkgs.runCommand "makeCustomizable-test" { } '' - - if ! echo ${getPkgs luaEnv} | grep -q "inspect"; then - echo "FAILURE: makeCustomizable test failed (inspect)" - exit 1 - fi - - if ! echo ${getPkgs luaEnv2} | grep -q "inspect"; then - echo "FAILURE: makeCustomizable test 2 failed (inspect)" - exit 1 - fi - - if ! echo ${getPkgs luaEnv2} | grep -q "cjson"; then - echo "FAILURE: makeCustomizable test 2 failed (cjson)" - exit 1 - fi - - if ! echo ${getPkgs luaEnv3} | grep -q "inspect"; then - echo "FAILURE: makeCustomizable test 3 failed (inspect)" - exit 1 - fi - - if ! echo ${getPkgs luaEnv3} | grep -q "cjson"; then - echo "FAILURE: makeCustomizable test 3 failed (cjson)" - exit 1 - fi - - if ! echo ${getPkgs luaEnv3} | grep -q "luassert"; then - echo "FAILURE: makeCustomizable test 3 failed (luassert)" - exit 1 - fi - - if ! echo ${getPkgs luaEnv4} | grep -q "inspect"; then - echo "FAILURE: makeCustomizable test 4 failed (inspect)" - exit 1 - fi - if ! echo ${getPkgs luaEnv4} | grep -q "cjson"; then - echo "FAILURE: makeCustomizable test 4 failed (cjson)" - exit 1 - fi +test "makeCustomizable-test" [ + (areEqual "args" testfunctor.value.some) - if ! echo ${getPkgs luaEnv4} | grep -q "luassert"; then - echo "FAILURE: makeCustomizable test 4 failed (luassert)" - exit 1 - fi + (areEqual "args" testfunctor2.value.some) + (areEqual "args" testfunctor2.value.more) - if ! echo ${getPkgs luaEnv4} | grep -q "luafilesystem"; then - echo "FAILURE: makeCustomizable test 4 failed (luafilesystem)" - exit 1 - fi + (areEqual "args" testfunctor3.value.some) + (areEqual "with overriding" testfunctor3.value.more) - echo "SUCCESS: makeCustomizable test passed (including multi-level chaining)" - touch $out -'' + (areEqual "args" testfunctor4.value.some) + (areEqual "with overriding" testfunctor4.value.more) + (areEqual "testing" testfunctor4.value.again) +] diff --git a/ci/checks/meta-maintainers.nix b/ci/checks/meta-maintainers.nix index f06d7017..3e5504bb 100644 --- a/ci/checks/meta-maintainers.nix +++ b/ci/checks/meta-maintainers.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/meta-platforms.nix b/ci/checks/meta-platforms.nix index acb18953..fdf9af38 100644 --- a/ci/checks/meta-platforms.nix +++ b/ci/checks/meta-platforms.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/no-module-prefix-in-checks.nix b/ci/checks/no-module-prefix-in-checks.nix deleted file mode 100644 index 3311e10d..00000000 --- a/ci/checks/no-module-prefix-in-checks.nix +++ /dev/null @@ -1,50 +0,0 @@ -{ - pkgs, - self, -}: - -let - # Get all check files in checks/ directory - checkFiles = builtins.readDir ./.; - - # Filter for .nix files that start with "module-" - checksWithPrefix = - prefix: - pkgs.lib.filter (name: pkgs.lib.hasPrefix prefix name && pkgs.lib.hasSuffix ".nix" name) ( - builtins.attrNames checkFiles - ); - - invalidChecksMod = checksWithPrefix "module-"; - invalidChecksWrap = checksWithPrefix "wrapperModule-"; - -in -pkgs.runCommand "no-module-prefix-in-checks-test" { } '' - echo "Checking that no checks in ci/checks/ directory start with 'module-' or 'wrapperModule-'..." - - ${ - if invalidChecksMod != [ ] then - '' - echo "FAIL: The following checks have invalid 'module-' prefix:" - ${pkgs.lib.concatMapStringsSep "\n" (name: ''echo " - ${name}"'') invalidChecksMod} - echo "" - echo "Checks starting with 'module-' are reserved for module-specific checks (modules/*/check.nix)." - echo "Please rename these checks to avoid namespace collision." - exit 1 - '' - else if invalidChecksWrap != [ ] then - '' - echo "FAIL: The following checks have invalid 'wrapperModule-' prefix:" - ${pkgs.lib.concatMapStringsSep "\n" (name: ''echo " - ${name}"'') invalidChecksWrap} - echo "" - echo "Checks starting with 'wrapperModule-' are reserved for wrapperModule-specific checks (wrapperModules/*/*/check.nix)." - echo "Please rename these checks to avoid namespace collision." - exit 1 - '' - else - '' - echo "SUCCESS: No checks start with 'module-' or 'wrapperModule-' prefixes" - '' - } - - touch $out -'' diff --git a/ci/checks/outputName-tests.nix b/ci/checks/outputName-tests.nix index 22915fb2..ba3feadc 100644 --- a/ci/checks/outputName-tests.nix +++ b/ci/checks/outputName-tests.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/specWith.nix b/ci/checks/specWith.nix new file mode 100644 index 00000000..ec270b93 --- /dev/null +++ b/ci/checks/specWith.nix @@ -0,0 +1,102 @@ +{ + pkgs, + self, + lib, + ... +}: + +let + module = + { + config, + pkgs, + lib, + wlib, + ... + }: + { + options.testSpecOption = lib.mkOption { + type = lib.types.attrsOf ( + wlib.types.specWith { + modules = [ + ( + { config, ... }: + { + options = { + theMainField = lib.mkOption { + type = lib.types.either (lib.types.functionTo lib.types.raw) lib.types.str; + }; + anotherField = lib.mkOption { + type = lib.types.str; + default = "a default value"; + }; + aDependentField = lib.mkOption { + type = lib.types.str; + default = "${config.theMainField} plus some extra"; + }; + }; + } + ) + ]; + } + ); + }; + config.testSpecOption.test1.theMainField = "test1 value"; + config.testSpecOption.test2 = "test2 value"; + config.testSpecOption.test3 = _: { + theMainField = "test3 value"; + }; + config.package = pkgs.hello; + }; + partial = (self.lib.evalModule module).config; + dontConvertFns = partial.apply ( + { lib, wlib, ... }: + { + options.testSpecOption = lib.mkOption { + type = lib.types.attrsOf ( + wlib.types.specWith { + dontConvertFunctions = true; + modules = [ ]; + } + ); + }; + } + ); +in +pkgs.runCommand "specWith-test" { } '' + echo "Testing spec type..." + + if [ ${lib.escapeShellArg partial.testSpecOption.test1.theMainField} != ${lib.escapeShellArg "test1 value"} ]; then + echo 'test failed, expected `test1 value`, but `theMainField` was: ${partial.testSpecOption.test1.theMainField}' + exit 1 + fi + if [ ${lib.escapeShellArg partial.testSpecOption.test1.aDependentField} != ${lib.escapeShellArg "test1 value plus some extra"} ]; then + echo 'test failed, expected `test1 value plus some extra`, but `aDependentField` was: ${partial.testSpecOption.test1.aDependentField}' + exit 1 + fi + if [ ${lib.escapeShellArg partial.testSpecOption.test1.anotherField} != ${lib.escapeShellArg "a default value"} ]; then + echo 'test failed, expected `a default value`, but `anotherField` was: ${partial.testSpecOption.test1.anotherField}' + exit 1 + fi + + if [ ${lib.escapeShellArg partial.testSpecOption.test2.theMainField} != ${lib.escapeShellArg "test2 value"} ]; then + echo 'test failed, expected `test2 value`, but `theMainField` was: ${partial.testSpecOption.test2.theMainField}' + exit 1 + fi + if [ ${lib.escapeShellArg partial.testSpecOption.test2.aDependentField} != ${lib.escapeShellArg "test2 value plus some extra"} ]; then + echo 'test failed, expected `test2 value plus some extra`, but `aDependentField` was: ${partial.testSpecOption.test2.aDependentField}' + exit 1 + fi + + if [ ${lib.escapeShellArg (partial.testSpecOption.test3.theMainField null).theMainField} != ${lib.escapeShellArg "test3 value"} ]; then + echo 'test failed, expected `test3 value`, but `(theMainField null).theMainField` was: ${lib.escapeShellArg (partial.testSpecOption.test3.theMainField null).theMainField}' + exit 1 + fi + if [ ${lib.escapeShellArg dontConvertFns.testSpecOption.test3.theMainField} != ${lib.escapeShellArg "test3 value"} ]; then + echo 'test failed, expected `test3 value`, but `theMainField` was: ${lib.escapeShellArg dontConvertFns.testSpecOption.test3.theMainField}' + exit 1 + fi + + echo "SUCCESS: spec type test passed" + touch $out +'' diff --git a/ci/checks/subwrappermodule.nix b/ci/checks/subwrappermodule.nix index 58aca0e8..613be394 100644 --- a/ci/checks/subwrappermodule.nix +++ b/ci/checks/subwrappermodule.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let evaled = self.lib.evalModule [ diff --git a/ci/checks/toKdl.nix b/ci/checks/toKdl.nix new file mode 100644 index 00000000..e0db6f01 --- /dev/null +++ b/ci/checks/toKdl.nix @@ -0,0 +1,152 @@ +{ + pkgs, + self, + ... +}: +let + lib = pkgs.lib; + toKdl = self.lib.toKdl; + + assertions = [ + { + description = "plain node"; + expected = ''"a" ''; + actual = toKdl { a = _: { }; }; + } + { + description = "primitive as argument"; + expected = ''"b" 1''; + actual = toKdl { b = 1; }; + } + { + description = "list of primitives as multiple args"; + expected = ''"c" "x" 2 #true #null''; + actual = toKdl { + c = [ + "x" + 2 + true + null + ]; + }; + } + { + description = "attrset as child block"; + expected = "\"d\" {\n \"x\" 1\n}"; + actual = toKdl { + d = { + x = 1; + }; + }; + } + { + description = "list of attrsets as repeated child nodes"; + expected = "\"e\" {\n \"x\" 1\n \"x\" 2\n}"; + actual = toKdl { + e = [ + { x = 1; } + { x = 2; } + ]; + }; + } + { + description = "function with props only"; + expected = ''"h" "k"="v"''; + actual = toKdl { + h = _: { + props = { + k = "v"; + }; + }; + }; + } + { + description = "function with content only"; + expected = "\"i\" {\n \"j\" 1\n}"; + actual = toKdl { + i = x: { + content = { + j = 1; + }; + }; + }; + } + { + description = "function with props and content"; + expected = "\"f\" \"arg1\" \"key\"=\"val\" {\n \"g\" \n}"; + actual = toKdl { + f = _: { + props = [ + "arg1" + { key = "val"; } + ]; + content = { + g = _: { }; + }; + }; + }; + } + { + description = "nested structure"; + expected = "\"k\" {\n \"l\" {\n \"m\" \"a\"\n \"m\" \"b\"\n }\n}"; + actual = toKdl { + k = { + l = [ + { m = "a"; } + { m = "b"; } + ]; + }; + }; + } + { + description = "top-level list of attrsets"; + expected = "\"a\" 1\n\"b\" 2"; + actual = toKdl [ + { a = 1; } + { b = 2; } + ]; + } + { + description = "null value"; + expected = ''"n" #null''; + actual = toKdl { n = null; }; + } + { + description = "mixed args and block"; + expected = "\"mixed\" \"arg1\" {\n \"child\" \"val\"\n}"; + actual = toKdl { + mixed = _: { + props = "arg1"; + content = { + child = "val"; + }; + }; + }; + } + ]; + + failedAssertions = builtins.filter (a: a.expected != a.actual) assertions; + numPassed = builtins.length (builtins.filter (a: a.expected == a.actual) assertions); + numFailed = builtins.length failedAssertions; + reportFailed = lib.concatMapStringsSep "\n" (a: '' + - ${a.description}: + Expected (${toString (builtins.stringLength a.expected)} chars): + ${lib.concatMapStringsSep "\n" (l: " ${l}") (lib.splitString "\n" a.expected)} + Actual (${toString (builtins.stringLength a.actual)} chars): + ${lib.concatMapStringsSep "\n" (l: " ${l}") (lib.splitString "\n" a.actual)} + '') failedAssertions; +in +pkgs.runCommand "toKdl-test" { } '' + echo "Testing toKdl function..." + echo "" + if [ ${toString numFailed} -gt 0 ]; then + echo "FAILED: ${toString numFailed} test(s) failed, ${toString numPassed} passed" + echo "" + echo "Failed tests:" + echo "${reportFailed}" + exit 1 + else + echo "PASSED: All ${toString numPassed} tests passed!" + touch $out + fi +'' diff --git a/ci/checks/types-file.nix b/ci/checks/types-file.nix index 1340b9ae..1e534f6c 100644 --- a/ci/checks/types-file.nix +++ b/ci/checks/types-file.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let lib = pkgs.lib; @@ -54,7 +55,7 @@ let evaledModuleWithOptionDefault = lib.evalModules { modules = [ module - { fileWithContent.content = lib.mkOptionDefault "test content5"; } + { fileWithContent.content = lib.mkDefault "test content5"; } # those two cause `content was accessed but no value defined` errors # { fileWithPathOverride.path = lib.mkOptionDefault "/etc/hosts5"; } # { diff --git a/ci/docs/default.nix b/ci/docs/default.nix index e39ecc21..544655bd 100644 --- a/ci/docs/default.nix +++ b/ci/docs/default.nix @@ -208,6 +208,23 @@ in src = "${placeholder "generated"}/wrapper_docs/${n}.md"; }) (builtins.attrNames config.drv.wrapper_docs); } + { + name = "Contributing"; + data = "numbered"; + path = "md/CONTRIBUTING.md"; + src = ../../CONTRIBUTING.md; + subchapters = [ + { + name = "tlib"; + data = "numbered"; + path = "tlib.md"; + src = "${placeholder "out"}/wrappers-lib/tlib.md"; + build = '' + ${pkgs.nixdoc}/bin/nixdoc --category "" --description 'Testing library `tlib` documentation' --file '${../test-lib.nix}' --prefix "tlib" >> $out/wrappers-lib/tlib.md + ''; + } + ]; + } ]; }; } diff --git a/ci/docs/md/getting-started.md b/ci/docs/md/getting-started.md index 8fa1a527..bbd47ec3 100644 --- a/ci/docs/md/getting-started.md +++ b/ci/docs/md/getting-started.md @@ -215,7 +215,7 @@ With a single, simple function, you can use any wrapper module directly as a mod # in a nixos module { ... }: { imports = [ - (inputs.wrappers.lib.mkInstallModule { name = "tmux"; value = inputs.wrappers.lib.wrapperModules.tmux; }) + (inputs.wrappers.lib.getInstallModule { name = "tmux"; value = inputs.wrappers.lib.wrapperModules.tmux; }) ]; wrappers.tmux = { enable = true; @@ -230,8 +230,7 @@ With a single, simple function, you can use any wrapper module directly as a mod # in a home-manager module { config, lib, ... }: { imports = [ - (inputs.wrappers.lib.mkInstallModule { - loc = [ "home" "packages" ]; + (inputs.wrappers.lib.getInstallModule { name = "neovim"; value = inputs.wrappers.lib.wrapperModules.neovim; }) @@ -261,7 +260,7 @@ With a single, simple function, you can use any wrapper module directly as a mod } ``` -See the [`wlib.mkInstallModule`](../lib/wlib.html#function-library-wlib.mkInstallModule) documentation for more info! +See the [`wlib.getInstallModule`](../lib/wlib.html#function-library-wlib.getInstallModule) documentation for more info! ### `flake-parts` @@ -294,32 +293,51 @@ It offers a template! `nix flake init -t github:BirdeeHub/nix-wrapper-modules#fl # Import the flake-parts module: imports = [ wrappers.flakeModules.wrappers ]; - perSystem = - { pkgs, ... }: - { - # wrappers.pkgs = pkgs; # choose a different `pkgs` - wrappers.control_type = "exclude"; # | "build" (default: "exclude") - wrappers.packages = { - alacritty = true; # <- set to true to exclude from being built into `packages.*.*` flake output - }; - }; + # provide wrapper modules to flake.wrappers flake.wrappers.alacritty = { pkgs, wlib, ... }: { imports = [ wlib.wrapperModules.alacritty ]; settings.terminal.shell.program = "${pkgs.zsh}/bin/zsh"; settings.terminal.shell.args = [ "-l" ]; }; + flake.wrappers.xplr = wrappers.lib.wrapperModules.xplr; flake.wrappers.tmux = { wlib, pkgs, ... }: { imports = [ wlib.wrapperModules.tmux ]; plugins = with pkgs.tmuxPlugins; [ onedark-theme ]; }; - flake.wrappers.xplr = wrappers.lib.wrapperModules.xplr; + + flake.wrappers.tmux-modified = { + # using flake.wrappers will also make importable forms + # available in config.flake.wrapperModules! + imports = [ self.wrapperModules.tmux ]; + # these will add to the above config which added the onedark-theme plugin + modeKeys = "vi"; + statusKeys = "vi"; + vimVisualKeys = true; + }; + + # no need for getInstallModule with flake-parts! + flake.nixosModules = builtins.mapAttrs (_: v: v.install) self.wrappers; + flake.homeModules = self.nixosModules; + # you don't have to export them from there specifically, + # this just shows that you can access `.install` directly when using the flake-parts module + + # (optionally) Control which packages get built! + perSystem = + { pkgs, ... }: + { + # wrappers.pkgs = pkgs; # (optionally) choose a different `pkgs` + wrappers.control_type = "exclude"; # | "build" (default: "exclude") + wrappers.packages = { + tmux-modified = true; # <- set to true to exclude from being built into `packages.*.*` flake output + }; + }; }; } ``` -The above flake will export the partially evaluated submodule from `outputs.wrappers` as it shows. +The above flake will export the partially evaluated submodules from `outputs.wrappers` as it shows. However, it also offers the values in importable form from `outputs.wrapperModules` for you! diff --git a/ci/docs/per-mod/rendermd.nix b/ci/docs/per-mod/rendermd.nix index 641a9d71..4d597a89 100644 --- a/ci/docs/per-mod/rendermd.nix +++ b/ci/docs/per-mod/rendermd.nix @@ -84,7 +84,13 @@ let ${mkOptField opt "description" ""}${mkOptField opt "relatedPackages" "Related packages:\n"}${ mkOptField opt "type" "Type:${lib.optionalString (opt.readOnly or false == true) " (read-only)"}" - }${mkOptField opt "default" "Default:"}${mkOptField opt "example" "Example:"}${ + }${ + let + # default can depend on another field without a default value + res = builtins.tryEval (mkOptField opt "default" "Default:"); + in + if res.success or false then res.value else "" + }${mkOptField opt "example" "Example:"}${ lib.optionalString (opt.declarations or [ ] != [ ]) '' Declared by: diff --git a/ci/flake.lock b/ci/flake.lock index 92389efe..ad59db80 100644 --- a/ci/flake.lock +++ b/ci/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1773840656, - "narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=", + "lastModified": 1775579569, + "narHash": "sha256-/m3yyS/EnXqoPGBJYVy4jTOsirdgsEZ3JdN2gGkBr14=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512", + "rev": "dfd9566f82a6e1d55c30f861879186440614696e", "type": "github" }, "original": { diff --git a/ci/flake.nix b/ci/flake.nix index b0e585cc..5d6e3965 100644 --- a/ci/flake.nix +++ b/ci/flake.nix @@ -20,39 +20,48 @@ inherit system; config.allowUnfree = true; }; + tlib = pkgs.callPackage ./test-lib.nix { inherit self; }; - # Load checks from checks/ directory - checkFiles = builtins.readDir ./checks; - importCheck = name: { - name = lib.removeSuffix ".nix" name; - value = import (./checks + "/${name}") { - inherit pkgs; - self = self; - }; - }; - checksFromDir = builtins.listToAttrs ( - map importCheck (builtins.filter (name: lib.hasSuffix ".nix" name) (builtins.attrNames checkFiles)) - ); + # Load checks from ci/checks/ directory + coreAndCiChecks = lib.pipe ./checks [ + builtins.readDir + builtins.attrNames + (builtins.filter (name: lib.hasSuffix ".nix" name)) + (map (n: { + name = lib.removeSuffix ".nix" n; + value = ./checks + "/${n}"; + })) + builtins.listToAttrs + ]; - importModuleCheck = prefix: name: value: { - name = "${prefix}-${name}"; - value = import value { - inherit pkgs; - self = self; - }; - }; - checksFromModules = builtins.listToAttrs ( - builtins.filter (v: v.value or null != null) ( - lib.mapAttrsToList (importModuleCheck "module") (wlib.checks.helper or { }) - ) - ); - checksFromWrapperModules = builtins.listToAttrs ( - builtins.filter (v: v.value or null != null) ( - lib.mapAttrsToList (importModuleCheck "wrapperModule") (wlib.checks.wrapper or { }) - ) - ); + checksFrom = + prefix: attrset: + let + importModuleCheck = + name: value: + let + helper = prefix: name: value: { + name = "${prefix}-${name}"; + inherit value; + }; + result = pkgs.callPackage value { inherit self tlib; }; + in + if result == null then + [ ] + else if result ? outPath then + [ (helper prefix name result) ] + else + lib.mapAttrsToList (helper "${prefix}-${name}") (lib.filterAttrs (_: v: v ? outPath) result); + in + lib.pipe attrset [ + (lib.mapAttrsToList importModuleCheck) + builtins.concatLists + builtins.listToAttrs + ]; in - checksFromDir // checksFromModules // checksFromWrapperModules + checksFrom "wlib" coreAndCiChecks + // checksFrom "module" (wlib.checks.helper or { }) + // checksFrom "wrapperModule" (wlib.checks.wrapper or { }) ); formatter = forAllSystems ( system: (wlib-flake (import nixpkgs { inherit system; })).formatter.${system} diff --git a/ci/test-lib.nix b/ci/test-lib.nix new file mode 100644 index 00000000..225c9ea1 --- /dev/null +++ b/ci/test-lib.nix @@ -0,0 +1,340 @@ +{ + self, + lib, + runCommand, + writeShellScript, + stdenv, + pkgs, + ... +}: +let + wlib = self.lib; + + errMsg = msg: "(echo ${renderMsg msg} >&2 && return 1)"; + + indentBlock = + str: num: + let + idnt = " "; + lines = lib.splitString "\n" str; + indentLine = line: (wlib.repeatStr idnt num) + line; + in + lib.concatMapStringsSep "\n" indentLine lines; + + # Use [ANSI-C Quoting](https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html#ANSI_002dC-Quoting-1) + # to properly display multi-line messages without being indented if occurring in deeply nested tests. + renderMsg = + str: + let + lines = lib.splitString "\n" str; + result = "$'${lib.concatStringsSep "\\n" lines}'"; + in + if (lib.length lines) == 1 then str else result; + + toSanitizedJSON = + value: + if builtins.isAttrs value then + builtins.toJSON ( + lib.mapAttrsRecursive ( + path: v: + if builtins.isFunction v then + let + res = builtins.unsafeGetAttrPos (lib.last path) ( + lib.getAttrFromPath (lib.sublist 0 (builtins.length path - 1) path) value + ); + in + "" + else + v + ) value + ) + else + builtins.toJSON value; + + render = + node: + let + renderAssertion = + let + _renderAssertion = + { cond, msg }: + '' + ( + ${cond} + ) || ${errMsg msg}''; + in + assertion: + if lib.isString assertion then + _renderAssertion { + cond = assertion; + msg = "Failed assertion: ${assertion}"; + } + else + _renderAssertion assertion; + + renderAssertions = + assertions: lib.concatMapStringsSep " &&\n" (a: "(${renderAssertion a})") assertions; + + isAssertion = + x: + lib.isString x + || ( + lib.isAttrs x + && + lib.attrNames x == [ + "cond" + "msg" + ] + ); + + renderNode = + node: + let + block = + if lib.isList node || isAssertion node then + renderAssertions (lib.toList node) + else + lib.concatMapAttrsStringSep " &&\n" (name: childNode: '' + (( + ${renderNode childNode} + ) || ${errMsg name})'') node; + in + indentBlock block 1; + in + renderNode node; + + _test = + { + name, + debug ? false, + }: + testSet: + let + testScript = '' + run () { + ${render testSet}; + } + run || (echo "Test '${name}' failed" >&2 && exit 1) + ''; + in + if debug then + writeShellScript name '' + ${testScript} + '' + else + runCommand name { passthru.test = testScript; } '' + ${testScript} + touch $out + ''; + +in +{ + /** + Takes tests provided as an attrs in `testSet`, renders it to a bash script, + and executes the rendered script using `pkgs.runCommand`. + + If the command runs successfully, the test set passes. Otherwise, a detailed + error message describing the failed assertion is shown in the logs. + + # Type + ``` + test :: (String | AttrSet) -> TestSet -> Derivation + ``` + + where: + + ``` + TestSet :: Test | attrsOf (TestSet | Test) + + Test :: Assertion | [ Assertion ] + + Assertion :: String | { cond :: String; msg :: String; } + ``` + + # Arguments + + settings + : Either a string or an attrs. + + If it is a string, it will be taken as the name of the derivation built + by `runCommand`. + + If it is a set, at least one of the keys `name` or `wrapper` need to be set + with a string value. + + - If you are defining tests for wrappers you should set `settings.wrapper` to the name + of the wrapper. This way, the `wrapper.meta.platforms` is considered during test execution, + and the test will only be run if the system it is running on is supported. + - If `settings.wrapper` is not set, the test will always be run. This makes sense if you + are testing core options or library functions. + - If only `settings.wrapper` is set, the name will be derived from this value by suffixing it with `-test`. + - If `settings.name` is set, it will be taken as the name of the derivation. + - If both are set, `settings.name` takes precedence. + + There is also an optional boolean option `settings.debug` (default = `false`). + If it is set to `true`, the generated bash script is built as an executable script that can be inspected + and run in `result` when running a specific test. + + The test can be disabled by setting `settings.enable = false`. + + testSet + : The set of tests to run. + */ + test = + settings: testSet: + let + name = + if lib.isString settings then + settings + else if settings ? name then + settings.name + else if settings ? wrapper then + "${settings.wrapper}-test" + else + throw '' + Invalid argument for `test`. + The first argument must be either a string (the test name) or an attrs + with at least one of the keys 'name' or 'wrapper', but got: + + ${lib.toSanitizedJSON settings} + ''; + wrapperName = settings.wrapper or null; + platforms = + if wrapperName == null then + null + else if (wrapperName != null) && (lib.hasAttr wrapperName self.wrappers) then + self.wrappers.${wrapperName}.meta.platforms + else + throw '' + Invalid argument for `test`. + The provided wrapper '${wrapperName}' was not found. + Available wrappers are: + + ${lib.toSanitizedJSON (lib.attrNames self.wrappers)} + ''; + enabled = + (settings.enable or true) + && (platforms == null || (platforms != null && builtins.elem stdenv.hostPlatform.system platforms)); + in + if enabled then + _test { + inherit name; + debug = settings.debug or false; + } testSet + else + null; + + /** + Returns an `Assertion` that checks whether `path` is an existing directory. + + # Type + ``` + isDirectory :: String -> Assertion + ``` + + # Arguments + path + : The filesystem path to check. + */ + isDirectory = path: { + cond = ''[[ -d "${path}" ]]''; + msg = "No such directory ${path}"; + }; + + /** + Returns an `Assertion` that checks whether `path` is an existing regular file. + + # Type + ``` + isFile :: String -> Assertion + ``` + + # Arguments + path + : The filesystem path to check. + */ + isFile = path: { + cond = ''[ -f "${path}" ]''; + msg = "No such file ${path}"; + }; + + /** + Returns an `Assertion` that checks whether `path` does **not** exist as a regular file. + + # Type + ``` + notIsFile :: String -> Assertion + ``` + + # Arguments + path + : The filesystem path that should be absent. + */ + notIsFile = path: { + cond = ''[ ! -f "${path}" ]''; + msg = "File ${path} should not exist"; + }; + + /** + Returns an `Assertion` that checks whether `file` contains a line matching `pattern`. + + The check is performed with `grep -q`, so `pattern` is treated as a basic regular expression. + + # Type + ``` + fileContains :: String -> String -> Assertion + ``` + + # Arguments + file + : Path to the file to search. + + pattern + : Basic regular expression to search for. + */ + fileContains = file: pattern: { + cond = ''grep -q '${pattern}' "${file}"''; + msg = "Pattern '${pattern}' not found in ${file}"; + }; + + /** + Returns an `Assertion` that checks whether `expected` and `actual` are equal. + + The comparison is performed in Nix at evaluation time. If the values differ, + the assertion message shows both values serialised as JSON. + + # Type + ``` + areEqual :: Any -> Any -> Assertion + ``` + + # Arguments + expected + : The expected value. + + actual + : The value to compare against `expected`. + */ + areEqual = + expected: actual: + let + equal = expected == actual; + in + { + cond = if equal then "true" else "false"; + msg = + if !equal then + '' + Expected: + ${toSanitizedJSON expected} + but got: + ${toSanitizedJSON actual}'' + else + "This should never happen."; + }; +} diff --git a/flake.lock b/flake.lock index 92389efe..ad59db80 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1773840656, - "narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=", + "lastModified": 1775579569, + "narHash": "sha256-/m3yyS/EnXqoPGBJYVy4jTOsirdgsEZ3JdN2gGkBr14=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512", + "rev": "dfd9566f82a6e1d55c30f861879186440614696e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 330dd4ca..80aafa8d 100644 --- a/flake.nix +++ b/flake.nix @@ -10,27 +10,28 @@ { templates = import ./templates; lib = import ./lib { inherit lib; }; + wrappers = lib.mapAttrs (_: v: (self.lib.evalModule v).config) self.lib.wrapperModules; + wrapperModules = self.lib.wrapperModules; flakeModules = { wrappers = ./parts.nix; default = self.flakeModules.wrappers; }; - nixosModules = builtins.mapAttrs (name: value: { - inherit name value; - _file = value; - key = value; - __functor = self.lib.mkInstallModule; - }) self.lib.wrapperModules; - homeModules = builtins.mapAttrs ( - _: v: - v - // { - loc = [ - "home" - "packages" - ]; - } - ) self.nixosModules; - wrappers = lib.mapAttrs (_: v: (self.lib.evalModule v).config) self.lib.wrapperModules; + nixosModules = builtins.mapAttrs ( + name: value: self.lib.getInstallModule { inherit name value; } + ) self.lib.wrapperModules; + homeModules = self.nixosModules; + formatter = forAllSystems ( + system: + ( + if inputs.pkgs.stdenv.hostPlatform.system or null == system then + inputs.pkgs + else + import (inputs.pkgs.path or inputs.nixpkgs or ) { + inherit system; + overlays = inputs.pkgs.overlays or [ ]; + } + ).nixfmt-tree + ); wrappedModules = lib.mapAttrs ( _: lib.warn '' @@ -45,29 +46,5 @@ This output will be removed on August 31, 2026 '' ) self.wrappers; - wrapperModules = lib.mapAttrs ( - _: - lib.warn '' - Attention: `inputs.nix-wrapper-modules.wrapperModules` is deprecated, use `inputs.nix-wrapper-modules.wrappers` instead - - Apologies for any inconvenience this has caused. But the title `wrapperModules` should be specific to ones you can import. - - In the future, rather than being removed, this will be replaced by the unevaluated wrapper modules from `wlib.wrapperModules` - - This output will be replaced with module paths on April 30, 2026 - '' - ) self.wrappers; - formatter = forAllSystems ( - system: - ( - if inputs.pkgs.stdenv.hostPlatform.system or null == system then - inputs.pkgs - else - import (inputs.pkgs.path or inputs.nixpkgs or ) { - inherit system; - overlays = inputs.pkgs.overlays or [ ]; - } - ).nixfmt-tree - ); }; } diff --git a/lib/core.nix b/lib/core.nix index cc107d2b..f4825cf4 100644 --- a/lib/core.nix +++ b/lib/core.nix @@ -4,6 +4,8 @@ lib, wlib, extendModules, + moduleType, + _prefix ? [ ], # NOTE: makes sure builderFunction gets name from _module.args name ? null, ... @@ -172,7 +174,192 @@ in ''; config.meta.maintainers = [ wlib.maintainers.birdee ]; config._module.args.pkgs = config.pkgs; + config.install.modules = { + homeManager = + { config, ... }: + { + config.home.packages = + let + cfg = args.config.install.getWrapperConfig config; + in + lib.mkIf cfg.enable [ cfg.wrapper ]; + }; + nixos = + { config, ... }: + { + config.environment.systemPackages = + let + cfg = args.config.install.getWrapperConfig config; + in + lib.mkIf cfg.enable [ cfg.wrapper ]; + }; + darwin = + { config, ... }: + { + config.environment.systemPackages = + let + cfg = args.config.install.getWrapperConfig config; + in + lib.mkIf cfg.enable [ cfg.wrapper ]; + }; + }; options = { + install = lib.mkOption { + description = '' + This submodule contains options which create a generic integration module. + + You can then import this submodule as a module in the module systems you specify in `config.install.modules` + + i.e. in nixos or home manager `{ imports = [ inputs.nix-wrapper-modules.wrappers..install ]; }` + + home manager, nixos, and nix darwin will by default also install the package. + + It will create the options at `config.install.optionLocation` + + This will default to `[ "wrappers" (lib.last _prefix) ]`, + so before importing it like this you will want to either set `config.install.optionLocation` yourself, + or use `wlib.getInstallModule` to set prefix before importing the module. + + In this way, your wrapper module can specify installation instructions for any module system evaluation which sets the `class` attribute + ''; + type = lib.types.submodule ( + { config, ... }: + { + _file = wlib.core; + options = { + optionLocation = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = lib.optionals (_prefix != [ ]) [ + "wrappers" + (lib.last _prefix) + ]; + description = '' + The location for the generated submodule option containing the wrapper module + + This will be set for you if you use the provided flake-parts module or `wlib.getInstallModule`, otherwise you must set it manually. + + This is to prevent the option location depending on the `pkgs` of the target evaluation, as it would if we used `config.binName`. + ''; + }; + mkWrapperExtension = lib.mkOption { + type = lib.types.functionTo lib.types.raw; + readOnly = true; + default = + key: module: + lib.setAttrByPath config.optionLocation { + imports = lib.toList module; + key = builtins.unsafeDiscardStringContext "${wlib.core}:${ + if lib.isStringLike module then "${module}." else "" + }install.mkWrapperExtension:${toString key}"; + }; + description = '' + Function that takes a wrapper module to place at the path indicated by `config.install.optionLocation`. + + It then returns the set to pass to config within an `install.modules` option + + To provide this alongside the config for the target module system, you can use `lib.mkMerge` + + ```nix + { ... }@top: { # <- in a wrapper module + config.install.modules.nixos = { config, pkgs, lib, ... }: { + config = lib.mkMerge [ + # pass another wrapper module + # but available for only this module system (nixos in this example) + (top.config.install.mkWrapperExtension "unique key name" ( + { wlib, pkgs, lib, config, ... }: { + options.enableService = lib.mkEnableOption "installation of service units"; + } + )) + # config for nixos module (in this example) + { + systemd.packages = let + # get the config from the wrapper modules option namespace + cfg = top.config.install.getWrapperConfig config; + in lib.mkIf cfg.enableService [ cfg.wrapper ]; + } + ]; + }; + } + ``` + + Tip: Instead of using `mkWrapperExtension`, you might wish to consider simply declaring the option in the wrapper module, + and just checking its value when relevant in your `install.modules` modules + + This function is generally most useful when the option declaration itself must know something about the target module system. + ''; + }; + getWrapperConfig = lib.mkOption { + type = lib.types.functionTo lib.types.raw; + default = + let + res = lib.attrByPath config.optionLocation null; + in + if res == null then + throw "could not find the wrapper config! `getWrapperConfig` works only in the modules exported by `.install` and the configurations which import it!" + else + res; + readOnly = true; + description = '' + This option indexes into config and returns the wrapper submodule at `config.install.optionLocation` within it + + ```nix + { ... }@top: { # <- in a wrapper module + config.install.modules.nixos = { config, lib, ... }: let + wrappercfg = top.config.install.getWrapperConfig config; + in { + config = lib.mkIf wrappercfg.enable { + # ... + }; + }; + } + ``` + ''; + }; + modules = lib.mkOption { + type = lib.types.lazyAttrsOf lib.types.deferredModule; + # don't set default, instead set via config, so that you can add some default behavior to some of them + description = '' + A set of modules for integrating the wrapper module with an external module system. Place a module at the attribute name corresponding with the class of the target module evaluation + ''; + }; + __functor = lib.mkOption { + type = lib.types.raw; + internal = true; + readOnly = true; + default = + _: + { + _class, + pkgs ? null, + ... + }: + { + _file = wlib.core; + imports = lib.toList (config.modules.${_class} or [ ]); + options = + assert + config.optionLocation != [ ] + || throw "to import `.install`, you must set `config.install.optionLocation` to a non-empty list of strings!"; + lib.setAttrByPath config.optionLocation ( + lib.mkOption { + type = moduleType; + default = { }; + } + ); + config = config.mkWrapperExtension "wrappers-install-add-enable-option" { + _file = wlib.core; + config._module.args.name = lib.mkOverride (lib.modules.defaultOverridePriority - 1) ( + lib.last config.optionLocation + ); + options.enable = lib.mkEnableOption "the wrapper module."; + config.pkgs = lib.mkIf (pkgs != null) pkgs; + }; + }; + }; + }; + } + ); + }; meta = { maintainers = lib.mkOption { description = "Maintainers of this module."; @@ -298,7 +485,6 @@ in }; }; } - config.overmods ]; } ); @@ -419,7 +605,7 @@ in }; binName = lib.mkOption { type = lib.types.str; - default = + default = builtins.unsafeDiscardStringContext ( if config.package.meta.mainProgram or null != null then baseNameOf ( builtins.addErrorContext '' @@ -430,8 +616,8 @@ in else if builtins.isString config.package || builtins.isPath config.package then baseNameOf (toString config.package) else - config.package.pname or config.package.name - or (throw "config.binName was not able to be detected!"); + config.package.pname or config.package.name or (throw "config.binName was not able to be detected!") + ); description = '' The name of the binary to be output to `config.wrapperPaths.placeholder` @@ -440,7 +626,7 @@ in }; exePath = lib.mkOption { type = lib.types.nullOr wlib.types.nonEmptyLine; - default = + default = builtins.unsafeDiscardStringContext ( if config.package.meta.mainProgram or null != null then lib.removePrefix "/" ( lib.removePrefix "${config.package}" ( @@ -457,7 +643,8 @@ in lib.optionalString (config.binDir != null) "${config.binDir}/" + "${config.package.pname or config.package.name or (throw "config.binName was not able to be detected! Please specify it manually!") - }"; + }" + ); description = '' The relative path to the executable to wrap. i.e. `bin/exename` @@ -916,63 +1103,5 @@ in This may prove useful when dealing with subWrapperModules or packages, which otherwise would not have access to some of them. ''; }; - symlinkScript = lib.mkOption { - type = lib.types.nullOr ( - lib.types.functionTo ( - lib.types.either lib.types.str (lib.types.functionTo (lib.types.attrsOf lib.types.raw)) - ) - ); - internal = true; - default = null; - description = "DEPRECATED"; - }; - overmods = lib.mkOption { - type = lib.types.nullOr lib.types.deferredModule; - default = null; - internal = true; - description = "DEPRECATED"; - apply = - x: - let - msg = '' - Attention: config.overmods is deprecated! - - Why? You could already do it like this! - - ```nix - { config, lib, wlib, pkgs, ... }:{ - options.overrides = lib.mkOption { - type = wlib.types.seriesOf (wlib.types.spec ({ config, ... }: { - options = {}; # spec types support type merging! - config = {}; - })); - }; - } - ``` - - Before the addition of `wlib.types.seriesOf`, - trying that with `listOf` would mess up the ordering. - - You could reimplement this option yourself like the following example. - Don't forget to declare the option you want to use for it! - - ```nix - { config, lib, wlib, ... }:{ - options.overrides = lib.mkOption { - type = wlib.types.seriesOf (wlib.types.spec (config.)); - }; - } - ``` - ''; - in - if x != null then lib.warn msg x else { }; - }; }; - config.builderFunction = lib.mkIf (config.symlinkScript != null) ( - lib.warn '' - Renamed option in wrapper module for ${config.binName}! - `config.symlinkScript` -> `config.builderFunction` - Please update all usages of the option to the new name. - '' config.symlinkScript - ); } diff --git a/lib/dag.nix b/lib/dag.nix index 0b653688..a96e5bd5 100644 --- a/lib/dag.nix +++ b/lib/dag.nix @@ -33,7 +33,6 @@ let isFunction types ; - inherit (lib.attrsets) filterAttrs; inherit (lib.lists) toposort; inherit (wlib.dag) isEntry @@ -53,18 +52,6 @@ let mkDagEntryModule = settings: elemType: let - isStrict = - if isBool (settings.strict or null) then - lib.warn "dagWith `strict` setting deprecated, set freeformType from within a module passed to the modules argument instead" settings.strict - else - true; - extraOptions = - if settings ? extraOptions then - lib.warn - "Deprecated dagWith/dalWith setting: `extraOptions` set. Use `modules` list instead to provide extra options" - (if isAttrs (settings.extraOptions or null) then settings.extraOptions else { }) - else - null; dataOptFn = if isFunction (settings.dataTypeFn or null) then args: { type = settings.dataTypeFn elemType args; } @@ -77,19 +64,16 @@ let description = settings.description or null; mainField = settings.mainField or null; dontConvertFunctions = settings.dontConvertFunctions or null; - modules = - optionals (!isStrict) [ { freeformType = wlib.types.attrsRecursive; } ] - ++ [ - (mkDagEntry { - dataOptFn = if isFunction (settings.dataOptFn or null) then settings.dataOptFn else dataOptFn; - defaultNameFn = - if isFunction (settings.defaultNameFn or null) then settings.defaultNameFn else null; - isDal = if isBool (settings.isDal or null) then settings.isDal else false; - }) - ] - ++ optionals (elemType.name == "submodule" || elemType.name == "spec") [ dagNameModule ] - ++ optionals (extraOptions != null) [ { options = filterAttrs (_: v: !isBool v) extraOptions; } ] - ++ optionals (isList (settings.modules or null)) settings.modules; + modules = [ + (mkDagEntry { + dataOptFn = if isFunction (settings.dataOptFn or null) then settings.dataOptFn else dataOptFn; + defaultNameFn = + if isFunction (settings.defaultNameFn or null) then settings.defaultNameFn else null; + isDal = if isBool (settings.isDal or null) then settings.isDal else false; + }) + ] + ++ optionals (elemType.name == "submodule") [ dagNameModule ] + ++ optionals (isList (settings.modules or null)) settings.modules; }; in { @@ -200,7 +184,7 @@ in You would do this because the name argument of that submodule will receive the field it was in, not the one from the parent `attrsOf` type. - If you use `dagWith` or `dalWith`, this is done for you for `submodule` and `spec`. + If you use `dagWith` or `dalWith`, this is done for you for `submodule` */ dagNameModule = { config, name, ... }: @@ -665,7 +649,4 @@ in Payload values that will be converted into DAG entries. */ entriesBefore = tag: before: entriesBetween tag before [ ]; - - dagOf = lib.warn "wlib.dag.dagOf deprecated. Use wlib.types.dagOf instead." wlib.types.dagOf; - dalOf = lib.warn "wlib.dag.dalOf deprecated. Use wlib.types.dalOf instead." wlib.types.dalOf; } diff --git a/lib/lib.nix b/lib/lib.nix index b314c6da..39c38df4 100644 --- a/lib/lib.nix +++ b/lib/lib.nix @@ -110,35 +110,38 @@ in ```nix { name, # string - value, # module or list of modules - optloc ? [ "wrappers" ], - loc ? [ - "environment" - "systemPackages" - ], - as_list ? true, - # Also accepts any valid top-level module attribute - # other than `config` or `options` - ... + value, # wrapper module or list of wrapper modules + ... # other evalModules arguments if necessary }: ``` - Creates a `wlib.types.subWrapperModule` option with an extra `enable` option at - the path indicated by `optloc ++ [ name ]`, with the default `optloc` being `[ "wrappers" ]` + It will return the value of `config.install`, which may be imported as a module in other module systems. + + If you did not set `config.install.optionLocation`, it will default to making the option at `config.wrappers.${name}` + + The exported module may contain specific integrations for the module classes + indicated by the provided attributes in `config.install.modules` for that wrapper module. + + If used in a module class not specified in that set, it will only create the submodule option containing the wrapper module. + + Likewise, if used in another wrapper module, that is all it will do as well. - Defines a list value at the path indicated by `loc` containing the `.wrapper` value of the submodule, - with the default `loc` being `[ "environment" "systemPackages" ]` + If you have your wrapper module imported via an `attrsOf subWrapperModule` option in an evaluation external to the target module system, + as is the case when using the `flake-parts` module, you DO NOT NEED THIS FUNCTION. - If `as_list` is false, it will set the value at the path indicated by `loc` as it is, - without putting it into a list. + This is because `config.install.optionLocation` will default to `[ "wrappers" (lib.last _prefix) ]` if present. - This means it will create a module that can be used like so: + In that case, you should do something like `flake.nixosModules. = { imports = [ config.flake.wrappers..install ]; };` directly. + + Again, if you are using the `flake-parts` module, or importing it in a similar manner, YOU DO NOT NEED THIS FUNCTION. + + *Examples:* ```nix # in a nixos module { ... }: { imports = [ - (mkInstallModule { name = "?"; value = someWrapperModule; }) + (installModule { name = "?"; value = someWrapperModule; }) ]; config.wrappers."?" = { enable = true; @@ -151,7 +154,7 @@ in # in a home-manager module { ... }: { imports = [ - (mkInstallModule { name = "?"; loc = [ "home" "packages" ]; value = someWrapperModule; }) + (installModule { name = "?"; value = someWrapperModule; }) ]; config.wrappers."?" = { enable = true; @@ -162,81 +165,35 @@ in If needed, you can also grab the package directly with `config.wrappers."?".wrapper` - Note: This function will try to provide a `pkgs` to the `subWrapperModule` automatically. + It will try to provide a `pkgs` to the `subWrapperModule` automatically. If the target module evaluation does not provide a `pkgs` via its module arguments to use, you will need to supply it to the submodule yourself later. - */ - mkInstallModule = - { - optloc ? [ "wrappers" ], - loc ? [ - "environment" - "systemPackages" - ], - as_list ? true, - name, - value, - ... - }@args: - { - pkgs ? null, - lib, - config, - ... - }: - # https://github.com/NixOS/nixpkgs/blob/c171bfa97744c696818ca23d1d0fc186689e45c7/lib/modules.nix#L615C1-L623C25 - builtins.intersectAttrs { - _class = null; - _file = null; - key = null; - disabledModules = null; - imports = null; - meta = null; - freeformType = null; - } args - // { - options = lib.setAttrByPath (optloc ++ [ name ]) ( - lib.mkOption { - default = { }; - description = '' - wrapper module for `${name}` as a submodule option - ''; - type = wlib.types.subWrapperModule ( - (lib.toList value) - ++ [ - { - _file = ./lib.nix; - config.pkgs = lib.mkIf (pkgs != null) pkgs; - options.enable = lib.mkEnableOption name; - } - ] - ); - } - ); - config = lib.setAttrByPath loc ( - lib.mkIf - (lib.getAttrFromPath ( - optloc - ++ [ - name - "enable" - ] - ) config) - ( - let - res = lib.getAttrFromPath ( - optloc - ++ [ - name - "wrapper" - ] - ) config; - in - if as_list then [ res ] else res - ) - ); + + Again, if you are using the `nix-wrapper-modules` `flake-parts` module, you should do something like this instead: + + ```nix + { config, ... }: { + flake.wrappers.someWrapperModule = a_wrapper_module; + flake.modules.nixos.someNixosModule = config.flake.wrappers.someWrapperModule.install; + flake.modules.homeManager.someHomeModule = config.flake.wrappers.someWrapperModule.install; + # the above requires these flake-parts modules be imported somewhere + imports = [ inputs.flake-parts.flakeModules.modules inputs.nix-wrapper-modules.flakeModules.wrappers ]; }; + ``` + */ + getInstallModule = + { name, value, ... }@evalModulesArgs: + (wlib.evalModules ( + removeAttrs evalModulesArgs [ + "name" + "value" + ] + // { + modules = toList value; + prefix = [ name ]; + } + )).config.install; /** Imports `wlib.modules.default` then evaluates the module. It then returns `.config` so that `.wrap` is easily accessible! @@ -544,6 +501,263 @@ in mapAttrsToList0 = f: v: lib.imap0 (i: v: f i v.name v.value) (lib.mapAttrsToList lib.nameValuePair v); + /** + Partition an attribute set into two attribute sets based on a predicate. + + The predicate is applied to each attribute as `(name: value: ...)`. Attributes + for which the predicate returns `true` are placed in `right`, and all others + are placed in `wrong`. + + This is like `lib.lists.partition`, but for attribute sets. + + Type: + ``` + (string -> any -> bool) -> attrs -> { + right :: attrs; + wrong :: attrs; + } + ``` + + Example: + ```nix + partitionAttrs (name: value: value > 10) { + a = 5; + b = 20; + c = 15; + } + => { + right = { b = 20; c = 15; }; + wrong = { a = 5; }; + } + ``` + + Notes: + - Iteration order follows `builtins.attrNames`, which is lexicographically sorted. + */ + partitionAttrs = + pred: attrs: + builtins.foldl' + ( + acc: name: + let + value = attrs.${name}; + in + if pred name value then + acc + // { + right = acc.right // { + ${name} = value; + }; + } + else + acc + // { + wrong = acc.wrong // { + ${name} = value; + }; + } + ) + { + right = { }; + wrong = { }; + } + (builtins.attrNames attrs); + + /** + `genStr` :: `string` -> `int` -> `string` + + or + + `genStr` :: (`int`: `string`) -> `int` -> `string` + + Generates a string by repeating the input string the specified number of times, + or by calling the provided function with the index the specified number of times. + */ + genStr = str: num: builtins.concatStringsSep "" (builtins.genList (lib.toFunction str) num); + + /** + `repeatStr` :: `string` -> `int` -> `string` + + Generates a string by repeating the input string the specified number of times + */ + repeatStr = str: num: builtins.concatStringsSep "" (builtins.genList (_: str) num); + + /** + Converts a Nix value to a KDL document string. + + The top-level argument, and individual nodes can be either an attrset or a list of attrsets: + - Attrset: each pair becomes a node (in a child block if not the top level) + - List of attrsets: each attrset of nodes is processed, and then they are concatenated in sequence. + This is useful for when there are repeated node names + + Inside nodes, attrsets and lists of attrsets create child blocks. + + For any individual node, instead of providing the content as an attrset or an attrset of lists, + you may instead provide a function. + + Functions produce nodes with: + - `props`: (optional) node arguments. May be an attrset, or a list containing mixed values and attrsets. + Plain values are provided as arguments. Attrset values are mapped to properties, i.e. `nodename "key"="value" {}`. + These values may not be sensibly nested further. + - `content`: (optional) child block content (attrs or list of attrs, like top level) + - `type`: (optional) a string to be placed in a type annotation on the node name. (If you provide a function returning a set with this field to props, it will add it to the value instead) + - `custom`: (optional) a function of type `{ indent, lvl, name } -> string` which is to replace the node (including its name) with a custom string. + + This means you can make a node with only a name like `toKdl { mynode = _: { }; }`, which will produce a string containing just `mynode` + + If you provide a list which contains more than just attrsets as a node's value, it will be assumed to be arguments/properties instead. + + If you provide a primitive value, it will likewise be considered to be an argument. + + Otherwise, it will be assumed to be a block, and to pass arguments, you should use the function form. + + The argument to the function is provided by calling the function with `lib.fix`. + + The top level argument to `wlib.toKdl` may also be a function, but it is slightly different + than the function form you can provide to a normal node. + + As a top-level argument, rather than passing the content directly as the argument, + you may provide a function like: + + ```nix + _: { + lvl = 0; + indent = " "; + version = 2; # or 1 (default 2) + content = set_or_list_of_sets; # required + } + # This allows you to set the indentation level of the generated nodes, and indentation width/character. + ``` + + Example: + + ```nix + { + # plain node (no args, no block) + a = _: { }; + # primitive → argument + b = 1; + # list of primitives → multiple args + c = [ "x" 2 true null ]; + # attrset → child block + d = { + x = 1; + }; + # list of attrsets → repeated child nodes + e = [ + { x = 1; } + { x = 2; } + ]; + # function form: props (args + properties) + content (block) + f = _: { + props = [ + "arg1" + { key = "val"; } + ]; + content = { + g = _: { }; + }; + }; + # function with only props (no block) + h = _: { + props = { k = "v"; }; + }; + # function with only content (block, no props) + i = _: { + content = { + j = 1; + }; + }; + # nested combination + k = { + l = [ + { m = "a"; } + { m = "b"; } + ]; + }; + # typed argument in props (list form) + n = [ (_: { type = "string"; content = "o"; }) ]; + # typed argument and typed property and block content + p = _: { + props = [ (_: { type = "string"; content = "q"; }) { r = (_: { type = "string"; content = "s"; }); } ]; + type = "string"; + content = { + t = "u"; + }; + }; + } + ``` + + ```kdl + "a" + "b" 1 + "c" "x" 2 true #null + "d" { + "x" 1 + } + "e" { + "x" 1 + "x" 2 + } + "f" "arg1" "key"="val" { + "g" + } + "h" "k"="v" + "i" { + "j" 1 + } + "k" { + "l" { + "m" "a" + "m" "b" + } + } + "n" (string)"o" + (string)"p" (string)"q" "r"=(string)"s" { + "t" "u" + } + ``` + */ + toKdl = import ./toKdl.nix { inherit lib wlib; }; + + /** + Sanitize a string into a valid environment variable name. + + This function sanitizes all characters that are not allowed in typical + POSIX environment variable names (`[A-Za-z0-9_]`), and ensures the + resulting string starts with a valid leading character (`[A-Za-z_]`). + + Behavior: + - All invalid characters are replaced with underscore characters (`_`) + + Examples: + ``` + sanitizeEnvVarName "FOO-BAR" => "FOO_BAR" + sanitizeEnvVarName "123.abc" => "_23_abc" + sanitizeEnvVarName "!@#" => "___" + sanitizeEnvVarName "hello, world!" => "hello__world_" + ``` + + Notes: + - Only ASCII characters are considered; all other characters are removed + - This does not guarantee uniqueness across multiple inputs + */ + sanitizeEnvVarName = + s: + let + isUpper = c: c >= "A" && c <= "Z"; + isLower = c: c >= "a" && c <= "z"; + isDigit = c: c >= "0" && c <= "9"; + + valid = + i: c: + if i == 0 then + isUpper c || isLower c || c == "_" + else + isUpper c || isLower c || isDigit c || c == "_"; + in + lib.concatStrings (lib.imap0 (i: c: if valid i c then c else "_") (lib.stringToCharacters s)); + /** Placeholder value used when overriding a non-main field of a spec type. @@ -587,4 +801,82 @@ in */ ignoreSpecField = lib.mkIf false null; + mkInstallModule = + lib.warn + '' + mkInstallModule deprecated: use `installModule`, or grab `flake.wrappers..install` (or any other method of setting `config.install.optionLocation` and retrieving that value) + + This function will be removed on August 31, 2026 + '' + ( + { + optloc ? [ "wrappers" ], + loc ? [ + "environment" + "systemPackages" + ], + as_list ? true, + name, + value, + ... + }@args: + { + pkgs ? null, + lib, + config, + ... + }: + # https://github.com/NixOS/nixpkgs/blob/c171bfa97744c696818ca23d1d0fc186689e45c7/lib/modules.nix#L615C1-L623C25 + builtins.intersectAttrs { + _class = null; + _file = null; + key = null; + disabledModules = null; + imports = null; + meta = null; + freeformType = null; + } args + // { + options = lib.setAttrByPath (optloc ++ [ name ]) ( + lib.mkOption { + default = { }; + description = '' + wrapper module for `${name}` as a submodule option + ''; + type = wlib.types.subWrapperModule ( + (lib.toList value) + ++ [ + { + _file = ./lib.nix; + config.pkgs = lib.mkIf (pkgs != null) pkgs; + options.enable = lib.mkEnableOption name; + } + ] + ); + } + ); + config = lib.setAttrByPath loc ( + lib.mkIf + (lib.getAttrFromPath ( + optloc + ++ [ + name + "enable" + ] + ) config) + ( + let + res = lib.getAttrFromPath ( + optloc + ++ [ + name + "wrapper" + ] + ) config; + in + if as_list then [ res ] else res + ) + ); + } + ); } diff --git a/lib/makeWrapper/default.nix b/lib/makeWrapper/default.nix index 08121870..5f5a52d2 100644 --- a/lib/makeWrapper/default.nix +++ b/lib/makeWrapper/default.nix @@ -16,7 +16,7 @@ let ( if (wrapperImplementation != null) then wrapperImplementation - else if config.wrapperImplementation or "nix" == "nix" then + else if v.config.wrapperImplementation or config.wrapperImplementation or "nix" == "nix" then ./makeWrapperNix.nix else ./makeWrapper.nix diff --git a/lib/specWith.nix b/lib/specWith.nix index 0370b1f3..669b2016 100644 --- a/lib/specWith.nix +++ b/lib/specWith.nix @@ -69,7 +69,7 @@ let baseNoCheck = base.extendModules { modules = [ noCheckForDocsModule ]; }; withoutDefaults = attrNames ( - filterAttrs (n: v: isOption v && !(v.isDefined or true)) baseNoCheck.options + filterAttrs (n: v: isOption v && v.highestPrio == 9999) baseNoCheck.options ); main_field = if isString mainField then diff --git a/lib/toKdl.nix b/lib/toKdl.nix new file mode 100644 index 00000000..d1a15878 --- /dev/null +++ b/lib/toKdl.nix @@ -0,0 +1,87 @@ +{ lib, wlib }: +let + inherit (builtins) isList isAttrs toJSON; + listOfNodes = l: isList l && builtins.all isAttrs l; + toKdlNode = + version: indent_str: i: n: val: + let + mkArgs = + args: + let + toVal = + v: + if version == 2 && v == null then + "#null" + else if version == 2 && builtins.isBool v then + if v then "#true" else "#false" + else if lib.isFunction v then + let + res = lib.fix v; + in + lib.optionalString (res ? type) "(${toString res.type})" + + lib.optionalString (res ? content) "${toJSON res.content}" + else if isAttrs v || isList v then + toJSON (toJSON v) + else + toJSON v; + mkAttrsOrVal = + attrs: + if isAttrs attrs then + lib.concatMapAttrsStringSep " " (n: v: "${toJSON n}=${toVal v}") attrs + else + toVal attrs; + in + if isList args then + let + partitioned = lib.partition isAttrs args; + args' = if version == 2 then partitioned.wrong ++ partitioned.right else args; + in + lib.concatMapStringsSep " " mkAttrsOrVal args' + else + mkAttrsOrVal args; + indent = wlib.repeatStr indent_str; + special = lib.isFunction val; + res = if special then lib.fix val else val; + v = if special then res.content or null else res; + nodetype = if res ? type then "(${toString res.type})" else ""; + attrs = if special && res ? props then mkArgs res.props else ""; + in + if special && res ? custom then + res.custom { + indent = indent_str; + lvl = i; + name = n; + } + else if isAttrs v then + '' + ${indent i}${nodetype}${toJSON n} ${attrs} { + ${lib.concatMapAttrsStringSep "\n" (toKdlNode version indent_str (i + 1)) v} + ${indent i}}'' + else if listOfNodes v then + '' + ${indent i}${nodetype}${toJSON n} ${attrs} { + ${lib.concatMapStringsSep "\n" (lib.concatMapAttrsStringSep "\n" ( + toKdlNode version indent_str (i + 1) + )) v} + ${indent i}}'' + else if special then + "${indent i}${nodetype}${toJSON n} ${attrs}" + else + "${indent i}${nodetype}${toJSON n} ${mkArgs v}"; + toKdl = + version: indent: i: value: + if isAttrs value then + lib.concatMapAttrsStringSep "\n" (toKdlNode version indent i) value + else if listOfNodes value then + lib.concatMapStringsSep "\n" (lib.concatMapAttrsStringSep "\n" (toKdlNode version indent i)) value + else + throw "ERROR wlib.toKdl: argument to wlib.toKdl is expected to be an attrset or a list of attrsets which represent the top level nodes of a kdl file!"; +in +value: +if lib.isFunction value then + let + res = lib.fix value; + in + toKdl (res.version or 2) (res.indent or " ") (res.lvl or 0) res.content +else + toKdl 2 " " 0 value diff --git a/lib/types.nix b/lib/types.nix index b84ea6eb..376d19a8 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -422,41 +422,90 @@ in ); /** - File type with content and path options + A simple template option for when you wish to offer options concerning generating a text file + + ```nix + options.configFile = lib.mkOption { + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.gitconfig.path; + }; + default = { }; + description = "Generated git configuration file."; + }; + config.constructFiles.gitconfig = { + relPath = "${config.binName}config"; + content = lib.generators.toGitINI config.settings + "\n" + config.configFile.content; + }; + ``` + + If instead of a module, you pass it nixpkgs, it will set defaults for `configFile.path` to `pkgs.writeText name content` Arguments: - - `pkgs`: nixpkgs instance + - `extra`: extra module(s) for the submodule option. Can instead be `pkgs` for a shorthanding effect. Fields: - - `content`: File contents as string - - `path`: Derived path using `pkgs.writeText` + - `content`: File contents as string, defaults to `""` + - `path`: Normally no default, you should provide one. But if the type is provided `pkgs` instead of modules, it contains a derived path using `pkgs.writeText` */ file = - # we need to pass pkgs here, because writeText is in pkgs - pkgs: - lib.types.submodule ( - { name, config, ... }: - { - options = { - content = lib.mkOption { - type = lib.types.lines; - description = '' - Content of the file. This can be a multi-line string that will be - written to the Nix store and made available via the path option. - ''; - }; - path = lib.mkOption { - type = wlib.types.stringable; - description = '' - The path to the file. By default, this is automatically - generated using pkgs.writeText with the attribute name and content. - ''; - default = pkgs.writeText name config.content; - defaultText = lib.literalExpression "pkgs.writeText name "; - }; - }; - } - ); + arg: + let + withDefaultPath = + pkgs: + lib.types.submodule ( + { + name, + config, + _prefix, + ... + }: + { + options = { + content = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Content of the file specified by `${lib.options.showOption _prefix}`. + This is a multi-line string that will be written to the Nix store. + ''; + }; + path = lib.mkOption { + type = wlib.types.stringable; + description = '' + The path to the file specified by `${lib.options.showOption _prefix}` + ''; + default = pkgs.writeText name config.content; + defaultText = lib.literalMD "```nix\npkgs.writeText ${name} \n```"; + }; + }; + } + ); + textFile = + extra: + lib.types.submodule ( + { _prefix, ... }: + { + imports = lib.toList extra; + options = { + content = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Content of the file specified by `${lib.options.showOption _prefix}`. + This is a multi-line string that will be written to the Nix store. + ''; + }; + path = lib.mkOption { + type = wlib.types.stringable; + description = '' + The path to the file specified by `${lib.options.showOption _prefix}` + ''; + }; + }; + } + ); + in + if lib.types.pkgs.check arg then withDefaultPath arg else textFile arg; /** Like `lib.types.anything`, but allows contained lists to also be merged diff --git a/maintainers/default.nix b/maintainers/default.nix index ec2a9382..9d3b2773 100644 --- a/maintainers/default.nix +++ b/maintainers/default.nix @@ -11,6 +11,11 @@ github = "rencire"; githubId = 546296; }; + ricardomaps = { + name = "Ricardo Mapurunga Junior"; + github = "ricardomaps"; + githubId = 49507078; + }; vinnymeller = { name = "Vinny Meller"; github = "vinnymeller"; @@ -45,4 +50,49 @@ github = "alexlov"; githubId = 100994; }; + boundless-recursion = { + name = "boundless-recursion"; + github = "boundless-recursion"; + githubId = 59110523; + }; + nikitawootten = { + email = "me@nikita.computer"; + github = "nikitawootten"; + githubId = 8916363; + name = "Nikita Wootten"; + }; + zenoli = { + name = "Zenoli"; + github = "zenoli"; + githubId = 8073528; + }; + pengolord = { + name = "pengo"; + email = "pbalternates@gmail.com"; + github = "Pengolord"; + githubId = 152470365; + }; + clay53 = { + name = "Clayton Hickey"; + email = "clayton@claytonhickey.me"; + github = "clay53"; + githubId = 16981283; + }; + nouritsu = { + email = "ab@nouritsu.com"; + github = "nouritsu"; + githubId = 113834791; + name = "Aneesh B"; + }; + jtrrll = { + name = "Jackson Terrill"; + email = "jacksonterrill3@gmail.com"; + github = "jtrrll"; + githubId = 77407057; + }; + ormoyo = { + name = "Ormoyo"; + github = "ormoyo"; + githubId = 58147142; + }; } diff --git a/modules/constructFiles/module.nix b/modules/constructFiles/module.nix index eb96ee17..4f598a2b 100644 --- a/modules/constructFiles/module.nix +++ b/modules/constructFiles/module.nix @@ -31,7 +31,12 @@ default = name; description = '' The attribute to add the file contents to on the final derivation + + If you get an error like "config.jsonPath: invalid variable name", + then that means you should set this value + to something which is a valid shell variable name. ''; + apply = wlib.sanitizeEnvVarName; }; content = lib.mkOption { type = lib.types.lines; @@ -81,16 +86,36 @@ config.drv = lib.mkIf (config.constructFiles != { }) ( let files = builtins.attrValues config.constructFiles; + mkUnique = + attrs: base: + let + try = + i: + let + candidate = if i == null then base else "${base}_${toString i}"; + in + if attrs ? ${candidate} then try (if i == null then 0 else i + 1) else candidate; + in + try null; result = builtins.foldl' - (acc: v: { - attrs = acc.attrs // { - ${v.key} = v.content; - }; - passAsFile = acc.passAsFile ++ [ v.key ]; - }) + ( + acc: v: + let + key = mkUnique acc.attrs v.key; + in + { + attrs = acc.attrs // { + ${key} = v.content; + }; + passAsFile = acc.passAsFile ++ [ key ]; + } + ) { - attrs = { }; + attrs = { + # prevents something from being named this + passAsFile = [ ]; + }; passAsFile = [ ]; } files; diff --git a/modules/makeWrapper/check.nix b/modules/makeWrapper/check.nix new file mode 100644 index 00000000..25e5e614 --- /dev/null +++ b/modules/makeWrapper/check.nix @@ -0,0 +1,12 @@ +{ + callPackage, + lib, + ... +}@args: +lib.pipe ./checks [ + builtins.readDir + (lib.filterAttrs (name: type: type == "regular" && lib.hasSuffix ".nix" name)) + (lib.mapAttrs' ( + name: _: lib.nameValuePair (lib.removeSuffix ".nix" name) (callPackage (./checks + "/${name}") args) + )) +] diff --git a/ci/checks/args-direct.nix b/modules/makeWrapper/checks/args-direct.nix similarity index 99% rename from ci/checks/args-direct.nix rename to modules/makeWrapper/checks/args-direct.nix index 554ef1ee..b17bb174 100644 --- a/ci/checks/args-direct.nix +++ b/modules/makeWrapper/checks/args-direct.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/env-null.nix b/modules/makeWrapper/checks/env-null.nix similarity index 99% rename from ci/checks/env-null.nix rename to modules/makeWrapper/checks/env-null.nix index 1055d1e2..7c647238 100644 --- a/ci/checks/env-null.nix +++ b/modules/makeWrapper/checks/env-null.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/flags-equals-separator.nix b/modules/makeWrapper/checks/flags-equals-separator.nix similarity index 99% rename from ci/checks/flags-equals-separator.nix rename to modules/makeWrapper/checks/flags-equals-separator.nix index 2f1ba67f..30cc24a6 100644 --- a/ci/checks/flags-equals-separator.nix +++ b/modules/makeWrapper/checks/flags-equals-separator.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/flags-list.nix b/modules/makeWrapper/checks/flags-list.nix similarity index 99% rename from ci/checks/flags-list.nix rename to modules/makeWrapper/checks/flags-list.nix index c70e6b86..bc57773e 100644 --- a/ci/checks/flags-list.nix +++ b/modules/makeWrapper/checks/flags-list.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/flags-null-false.nix b/modules/makeWrapper/checks/flags-null-false.nix similarity index 99% rename from ci/checks/flags-null-false.nix rename to modules/makeWrapper/checks/flags-null-false.nix index 72cce64d..4f4c591d 100644 --- a/ci/checks/flags-null-false.nix +++ b/modules/makeWrapper/checks/flags-null-false.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/flags-space-separator.nix b/modules/makeWrapper/checks/flags-space-separator.nix similarity index 99% rename from ci/checks/flags-space-separator.nix rename to modules/makeWrapper/checks/flags-space-separator.nix index 68e0d929..fe396a63 100644 --- a/ci/checks/flags-space-separator.nix +++ b/modules/makeWrapper/checks/flags-space-separator.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/flags-module.nix b/modules/makeWrapper/checks/flags.nix similarity index 99% rename from ci/checks/flags-module.nix rename to modules/makeWrapper/checks/flags.nix index 2ebf1213..9e1080e8 100644 --- a/ci/checks/flags-module.nix +++ b/modules/makeWrapper/checks/flags.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/modules/symlinkScript/check.nix b/modules/symlinkScript/check.nix new file mode 100644 index 00000000..25e5e614 --- /dev/null +++ b/modules/symlinkScript/check.nix @@ -0,0 +1,12 @@ +{ + callPackage, + lib, + ... +}@args: +lib.pipe ./checks [ + builtins.readDir + (lib.filterAttrs (name: type: type == "regular" && lib.hasSuffix ".nix" name)) + (lib.mapAttrs' ( + name: _: lib.nameValuePair (lib.removeSuffix ".nix" name) (callPackage (./checks + "/${name}") args) + )) +] diff --git a/ci/checks/filesToExclude-glob.nix b/modules/symlinkScript/checks/filesToExclude-glob.nix similarity index 99% rename from ci/checks/filesToExclude-glob.nix rename to modules/symlinkScript/checks/filesToExclude-glob.nix index c630230a..b2606f0c 100644 --- a/ci/checks/filesToExclude-glob.nix +++ b/modules/symlinkScript/checks/filesToExclude-glob.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/filesToExclude-module.nix b/modules/symlinkScript/checks/filesToExclude-more.nix similarity index 99% rename from ci/checks/filesToExclude-module.nix rename to modules/symlinkScript/checks/filesToExclude-more.nix index e0309596..3aa00cc7 100644 --- a/ci/checks/filesToExclude-module.nix +++ b/modules/symlinkScript/checks/filesToExclude-more.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/filesToExclude.nix b/modules/symlinkScript/checks/filesToExclude.nix similarity index 99% rename from ci/checks/filesToExclude.nix rename to modules/symlinkScript/checks/filesToExclude.nix index ef6782fd..53e2b073 100644 --- a/ci/checks/filesToExclude.nix +++ b/modules/symlinkScript/checks/filesToExclude.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/ci/checks/filesToPatch.nix b/modules/symlinkScript/checks/filesToPatch.nix similarity index 99% rename from ci/checks/filesToPatch.nix rename to modules/symlinkScript/checks/filesToPatch.nix index 6010a2ff..af1fa392 100644 --- a/ci/checks/filesToPatch.nix +++ b/modules/symlinkScript/checks/filesToPatch.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/templates/neovim/flake.nix b/templates/neovim/flake.nix index 0c0e9c57..62fb2d0d 100644 --- a/templates/neovim/flake.nix +++ b/templates/neovim/flake.nix @@ -25,11 +25,8 @@ module = nixpkgs.lib.modules.importApply ./module.nix inputs; wrapper = wrappers.lib.evalModule module; in + # for demonstration purposes, we will set up all the outputs. { - overlays = { - neovim = final: prev: { neovim = wrapper.config.wrap { pkgs = final; }; }; - default = self.overlays.neovim; - }; wrapperModules = { neovim = module; default = self.wrapperModules.neovim; @@ -38,37 +35,35 @@ neovim = wrapper.config; default = self.wrappers.neovim; }; + overlays = { + neovim = final: prev: { neovim = self.wrappers.neovim.wrap { pkgs = final; }; }; + default = self.overlays.neovim; + }; packages = forAllSystems ( system: let pkgs = import nixpkgs { inherit system; }; in { - neovim = wrapper.config.wrap { inherit pkgs; }; + neovim = self.wrappers.neovim.wrap { inherit pkgs; }; default = self.packages.${system}.neovim; } ); + # home manager and nixos modules # `wrappers.neovim.enable = true` + # You can set any of the options. + # But that is how you enable it. nixosModules = { default = self.nixosModules.neovim; - neovim = wrappers.lib.mkInstallModule { + neovim = wrappers.lib.getInstallModule { name = "neovim"; value = module; }; }; - # `wrappers.neovim.enable = true` - # You can set any of the options. - # But that is how you enable it. homeModules = { default = self.homeModules.neovim; - neovim = wrappers.lib.mkInstallModule { - name = "neovim"; - value = module; - loc = [ - "home" - "packages" - ]; - }; + # they produce generically importable modules + neovim = self.nixosModules.neovim; }; }; } diff --git a/wrapperModules/a/aria2/check.nix b/wrapperModules/a/aria2/check.nix index 03462222..657c3b09 100644 --- a/wrapperModules/a/aria2/check.nix +++ b/wrapperModules/a/aria2/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let aria2Wrapper = self.wrappers.aria2.wrap { diff --git a/wrapperModules/a/atool/check.nix b/wrapperModules/a/atool/check.nix index 8ddefd45..88255abb 100644 --- a/wrapperModules/a/atool/check.nix +++ b/wrapperModules/a/atool/check.nix @@ -1,4 +1,4 @@ -{ pkgs, self }: +{ pkgs, self, ... }: let atoolWrapped = self.wrappers.atool.wrap { inherit pkgs; diff --git a/wrapperModules/b/btop/module.nix b/wrapperModules/b/btop/module.nix index ebc55e52..cb08bb27 100644 --- a/wrapperModules/b/btop/module.nix +++ b/wrapperModules/b/btop/module.nix @@ -133,20 +133,14 @@ in output = config.configDrvOutput; }; } - // lib.pipe config.themes [ - (wlib.mapAttrsToList0 ( - i: n: v: - lib.nameValuePair n { - key = "theme_${toString i}"; - relPath = lib.mkOverride 0 "${config.binName}-themes/${n}.theme"; - output = lib.mkOverride 0 config.configDrvOutput; - ${if builtins.isPath v || lib.isStorePath v then null else "content"} = v; - ${if builtins.isPath v || lib.isStorePath v then "builder" else null} = - ''mkdir -p "$(dirname "$2")" && cp ${v} "$2"''; - } - )) - builtins.listToAttrs - ]; + // builtins.mapAttrs (n: v: { + key = "theme_${n}"; + relPath = lib.mkOverride 0 "${config.binName}-themes/${n}.theme"; + output = lib.mkOverride 0 config.configDrvOutput; + ${if builtins.isPath v || lib.isStorePath v then null else "content"} = v; + ${if builtins.isPath v || lib.isStorePath v then "builder" else null} = + ''mkdir -p "$(dirname "$2")" && cp ${v} "$2"''; + }) config.themes; meta.maintainers = [ wlib.maintainers.ameer ]; } diff --git a/wrapperModules/c/cava/check.nix b/wrapperModules/c/cava/check.nix index 92d36f78..66d4f57b 100644 --- a/wrapperModules/c/cava/check.nix +++ b/wrapperModules/c/cava/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let cavaWrapper = self.wrappers.cava.wrap { inherit pkgs; }; diff --git a/wrapperModules/c/claude-code/check.nix b/wrapperModules/c/claude-code/check.nix index fdf993de..7a27347b 100644 --- a/wrapperModules/c/claude-code/check.nix +++ b/wrapperModules/c/claude-code/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let claudeCodeWrapped = self.wrappers.claude-code.wrap { diff --git a/wrapperModules/e/emacs/check.nix b/wrapperModules/e/emacs/check.nix new file mode 100644 index 00000000..d6e9d9b9 --- /dev/null +++ b/wrapperModules/e/emacs/check.nix @@ -0,0 +1,29 @@ +{ + pkgs, + self, + ... +}: +let + emacsWrapped = + (self.wrappers.emacs.apply { + inherit pkgs; + emacsPackages = + epkgs: + let + m = epkgs.melpaPackages; + in + [ + m.evil + m.ivy + ]; + configFile = '' + (setq inhibit-startup-message t) + (set-fringe-mode 10) + ''; + }).wrapper; +in +pkgs.runCommand "emacs-test" { } '' + "${emacsWrapped}/bin/emacs" --help | grep -q "Usage" + grep -q --no-ignore-case -- "--init-directory" "${emacsWrapped}/bin/emacs" + touch $out +'' diff --git a/wrapperModules/e/emacs/module.nix b/wrapperModules/e/emacs/module.nix new file mode 100644 index 00000000..c6a5bf5d --- /dev/null +++ b/wrapperModules/e/emacs/module.nix @@ -0,0 +1,93 @@ +{ + config, + lib, + wlib, + pkgs, + ... +}: +{ + imports = [ wlib.modules.default ]; + options = { + emacsPackages = lib.mkOption { + type = wlib.types.withPackagesType; + default = ps: [ ]; + example = lib.literalExpression "epkgs: with epkgs.melpaPackages; [ evil ivy ]"; + description = "Packages for emacs. This value is provided to pkgs.emacs.pkgs.withPackages, so it should +either be a list of emacs packages, or a function that takes a single input and returns a list of packages. +That input provides `.melpaPackages` which contains all packages from Melpa."; + }; + configFile = lib.mkOption { + type = lib.types.lines; + default = ""; + example = '' + (require 'use-package) + + (setq inhibit-startup-message t) + (set-fringe-mode 10) + ''; + description = '' + emacs config file. + + Because of emacs quirks, if `~/.emacs` exists, then it will be used first. + If you need to work around this, add `[ [ "-l" config.constructFiles.init.path ] "-q" ]` + to `config.addFlag`, or set those arguments via `config.flags`. + ''; + }; + earlyConfigFile = lib.mkOption { + type = lib.types.lines; + default = ""; + example = '' + (setq extra-files-path $${./path/to/extra/files}) + ''; + description = "The contents of `early-init.el`."; + }; + userDirectory = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = "~/.emacs.d"; + description = "After loading our config file, `user-emacs-directory` will be set to the value of this +option. If the option is null, `user-emacs-directory` will point to a read-only location in the nix store +(not recommended, since some emacs packages depend on being able to write to the user config directory). + +This is done at the start of `early-init.el`."; + }; + generatedConfigOutput = lib.mkOption { + type = lib.types.str; + default = config.outputName; + description = '' + The derivation output for the config generated by this wrapper module + ''; + }; + }; + config.wrapperImplementation = "binary"; + config.passthru.generatedConfig = "${config.wrapper.${config.generatedConfigOutput}}/emacs.d"; + config.constructFiles.early-init = { + key = "earlyInit"; + output = lib.mkOverride 0 config.generatedConfigOutput; + relPath = lib.mkOverride 0 "emacs.d/early-init.el"; + content = + lib.optionalString (config.userDirectory != null) '' + (setq user-emacs-directory "${config.userDirectory}") + '' + + config.earlyConfigFile; + }; + config.constructFiles.init = { + relPath = lib.mkOverride 0 "emacs.d/init.el"; + output = lib.mkOverride 0 config.generatedConfigOutput; + content = config.configFile; + }; + config.wrapperVariants = { + "emacs-${config.package.emacs.version}" = { }; + }; + config.flags."--init-directory" = lib.mkIf ( + config.configFile != "" || config.earlyConfigFile != "" + ) "${dirOf config.constructFiles.init.path}"; + config.package = lib.mkDefault pkgs.emacs; + config.overrides = [ + { + name = "emacsPackages"; + data = v: v.pkgs.withPackages config.emacsPackages; + } + ]; + config.meta.description = "Wrapper for emacs"; + config.meta.maintainers = [ wlib.maintainers.boundless-recursion ]; +} diff --git a/wrapperModules/e/eww/module.nix b/wrapperModules/e/eww/module.nix new file mode 100644 index 00000000..8a799a68 --- /dev/null +++ b/wrapperModules/e/eww/module.nix @@ -0,0 +1,78 @@ +{ + config, + lib, + wlib, + pkgs, + ... +}: +{ + imports = [ wlib.modules.default ]; + options = { + yuck = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Configure windows, widgets, and variables for eww. + ''; + }; + style = lib.mkOption { + description = '' + The CSS or SCSS style file to use for eww. + ''; + default = { }; + type = lib.types.submodule { + options = { + path = lib.mkOption { + type = lib.types.nullOr wlib.types.stringable; + default = null; + description = '' + Path to an existing file. + Takes precedence over `content` if both are set. + ''; + }; + content = lib.mkOption { + type = lib.types.nullOr lib.types.lines; + default = null; + description = '' + Inline css/scss file content. + Used when `path` is null. + ''; + }; + format = lib.mkOption { + type = lib.types.enum [ + "css" + "scss" + ]; + default = "scss"; + description = '' + File type extension of the style file. + ''; + }; + }; + }; + }; + }; + + config = { + package = lib.mkDefault pkgs.eww; + + constructFiles.yuck = { + content = config.yuck; + relPath = "${config.binName}-config/eww.yuck"; + }; + + constructFiles.style = { + content = if builtins.isString (config.style.content or null) then config.style.content else ""; + output = lib.mkOverride 0 config.constructFiles.yuck.output; + relPath = lib.mkOverride 0 "${dirOf config.constructFiles.yuck.relPath}/eww.${config.style.format}"; + ${if config.style.path or null != null then "builder" else null} = + ''mkdir -p "$(dirname "$2")" && ln -s "${config.style.path}" "$2"''; + }; + + flags."--config" = dirOf config.constructFiles.yuck.path; + + passthru.generatedConfig = dirOf config.constructFiles.yuck.outPath; + + meta.maintainers = [ wlib.maintainers.clay53 ]; + }; +} diff --git a/wrapperModules/f/fastfetch/module.nix b/wrapperModules/f/fastfetch/module.nix index f1298e85..7627c83f 100644 --- a/wrapperModules/f/fastfetch/module.nix +++ b/wrapperModules/f/fastfetch/module.nix @@ -9,7 +9,7 @@ imports = [ wlib.modules.default ]; options = { settings = lib.mkOption { - type = lib.types.json; + type = lib.types.json or (pkgs.formats.json { }).type; default = { }; description = '' Configuration passed to fastfetch using `--config` flag diff --git a/wrapperModules/f/fish/check.nix b/wrapperModules/f/fish/check.nix new file mode 100644 index 00000000..90d77ae9 --- /dev/null +++ b/wrapperModules/f/fish/check.nix @@ -0,0 +1,18 @@ +{ + pkgs, + self, + ... +}: +let + fishWrapped = self.wrappers.fish.wrap { + inherit pkgs; + configFile.content = "echo \"hello world\""; + }; +in +if builtins.elem pkgs.stdenv.hostPlatform.system self.wrappers.fish.meta.platforms then + pkgs.runCommand "fish-test" { } '' + "${fishWrapped}/bin/fish" --version | grep -q "${fishWrapped.version}" + touch $out + '' +else + null diff --git a/wrapperModules/f/fish/module.nix b/wrapperModules/f/fish/module.nix new file mode 100644 index 00000000..e906e66b --- /dev/null +++ b/wrapperModules/f/fish/module.nix @@ -0,0 +1,536 @@ +{ + wlib, + lib, + pkgs, + config, + ... +}: +let + inherit (lib) + attrValues + concatMapStringsSep + concatStringsSep + escapeShellArg + foldl' + mapAttrsToList + literalExpression + mkDefault + mkOption + mkOptionDefault + optionalString + partition + pipe + splitString + types + ; + + cfg = config; + split = wlib.makeWrapper.splitDal (wlib.makeWrapper.aggregateSingleOptionSet { inherit config; }); + + abbreviationModule = + { name, ... }: + { + options = { + word = mkOption { + type = types.str; + default = name; + description = "The word to be replaced"; + }; + expansion = mkOption { + type = types.str; + description = "The expansion to replace the word with"; + }; + position = mkOption { + type = types.enum [ + "anywhere" + "command" + ]; + default = "anywhere"; + description = '' + The scope of the abbreviation. + + "anywhere": The abbreviation may expand anywhere in the command line + + "command": The abbreviation would only expand if it is positioned as a command + ''; + }; + regex = mkOption { + type = types.nullOr types.str; + default = null; + description = "Special regex to expand instead of a word"; + }; + command = mkOption { + type = types.nullOr types.str; + default = null; + description = "The abbreviation will only expand if it is used as an argument to this command"; + }; + function = mkOption { + type = types.nullOr types.str; + default = null; + description = "When the abbreviation matches, this function will be called with the matching token as an argument"; + }; + cursor = mkOption { + type = types.either types.bool types.str; + default = false; + description = "The cursor is moved to the first occurrence of this in the expansion, or to \"%\" if set to true"; + }; + }; + }; + completionModule = + { name, config, ... }: + { + config.path = mkOptionDefault (pkgs.writeText name config.content); + options.command = mkOption { + type = types.str; + default = name; + description = "The command to apply the completion for"; + }; + }; + pluginModule = { + options = { + src = mkOption { + type = types.package; + description = "The package which contains the plugin"; + }; + configDirs = mkOption { + type = types.listOf types.str; + default = cfg.pluginConfigDirs; + description = "The directories which will be checked for config files"; + }; + completionDirs = mkOption { + type = types.listOf types.str; + default = cfg.pluginCompletionDirs; + description = "The directories which will be checked for config files"; + }; + }; + }; +in +{ + imports = [ + wlib.modules.symlinkScript + wlib.modules.constructFiles + ./variants.nix + ( + (import wlib.modules.makeWrapper) + // { + excluded_options.wrapperFunction = true; + excluded_options.wrapperImplementation = true; + } + ) + ]; + options = { + configFile = mkOption { + type = wlib.types.file { + path = mkOptionDefault config.constructFiles.generatedConfig.path; + }; + default = { + content = ""; + path = config.constructFiles.generatedConfig.path; + }; + description = '' + The main fish configuration file. + + Provide either `.content` to inline shell configuration or `.path` to reference an external file. + It is sourced by fish using `--init-command`. + ''; + }; + + abbreviations = mkOption { + type = types.attrsOf (wlib.types.spec abbreviationModule); + default = { }; + description = "Abbreviations to be included in the shell"; + example = literalExpression '' + { + lshome = "ls ~/"; + find-extension = { + word = "ext"; + expansion = "~/ -name \"*.%\""; + command = "find"; + cursor = true; + }; + please = { + expansion = "sudo"; + position = "command"; + }; + } + ''; + }; + shellAliases = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Aliases to be included in the shell"; + example = { + ls = "ls -a"; + ll = "ls -l"; + }; + }; + completionFiles = mkOption { + type = types.attrsOf (wlib.types.file completionModule); + default = { }; + description = "Completions to be included in the shell"; + }; + plugins = mkOption { + type = types.listOf (wlib.types.spec pluginModule); + default = [ ]; + description = "List of fish plugins to install"; + example = literalExpression '' + [ + pkgs.fishPlugins.hydro + { + src = pkgs.fishPlugins.fzf-fish; + configDirs = [ "share/fish/vendor_conf.d" ]; + completionDirs = [ "completions" ]; + } + ] + ''; + }; + pluginConfigDirs = mkOption { + type = types.listOf types.str; + default = [ + "share/fish/vendor_functions.d" + "etc/fish/functions" + "share/fish/vendor_conf.d" + "etc/fish/conf.d" + ]; + description = "The default directories to check for configs in plugins"; + }; + pluginCompletionDirs = mkOption { + type = types.listOf types.str; + default = [ + "share/fish/vendor_completions.d" + "share/fish/completions" + ]; + description = "The default directories to check for completion files in plugins"; + }; + }; + + config.package = mkDefault pkgs.fish; + config.passthru.shellPath = config.wrapperPaths.relPath; + + config.buildCommand.completionFiles.data = optionalString (cfg.completionFiles != [ ]) '' + mkdir -p ${placeholder config.outputName}/completions + ${concatStringsSep "\n" ( + mapAttrsToList ( + _: c: "cp ${c.path} ${placeholder config.outputName}/completions/${c.command}" + ) cfg.completionFiles + )} + ''; + + config.flags = { + "--no-config" = mkDefault true; + "--init-command" = { + sep = "="; + data = [ + "source ${config.constructFiles.generatedConfig.path}" + ]; + }; + }; + + config.constructFiles.generatedConfig = { + relPath = "${config.binName}-config.fish"; + builder = + let + startSection = '' + if set -q __wrapped_fish_sourced + return + end + set -gx __wrapped_fish_sourced 1 + fish_add_path --path ${dirOf config.wrapperPaths.placeholder} + ''; + + wrapcmd = partial: "echo ${escapeShellArg partial} >> \"$2\""; + wrapperBuild = pipe split.other [ + (wlib.dag.unwrapSort "makeWrapper") + (builtins.concatMap ( + v: + let + esc-fn = if v.esc-fn or null != null then v.esc-fn else config.escapingFunction; + in + if v.type or null == "unsetVar" then + [ (wrapcmd "set -e ${esc-fn v.data}") ] + else if v.type or null == "env" then + [ (wrapcmd "wrapperSetEnv ${esc-fn v.attr-name} ${esc-fn v.data}") ] + else if v.type or null == "envDefault" then + [ (wrapcmd "wrapperSetEnvDefault ${esc-fn v.attr-name} ${esc-fn v.data}") ] + else if v.type or null == "prefixVar" then + let + env = builtins.elemAt v.data 0; + sep = builtins.elemAt v.data 1; + val = builtins.elemAt v.data 2; + vals = splitString sep val; + in + [ + (wrapcmd "wrapperPrefixEnv ${ + concatMapStringsSep " " esc-fn ( + [ + env + ] + ++ vals + ) + }") + ] + else if v.type or null == "suffixVar" then + let + env = builtins.elemAt v.data 0; + sep = builtins.elemAt v.data 1; + val = builtins.elemAt v.data 2; + vals = splitString sep val; + in + [ + (wrapcmd "wrapperSuffixEnv ${ + concatMapStringsSep " " esc-fn ( + [ + env + ] + ++ vals + ) + }") + ] + else if v.type or null == "prefixContent" then + let + env = builtins.elemAt v.data 0; + val = builtins.elemAt v.data 2; + cmd = "wrapperPrefixEnv ${esc-fn env} "; + in + [ ''echo ${escapeShellArg cmd}"$(cat ${esc-fn val})" >> "$2"'' ] + else if v.type or null == "suffixContent" then + let + env = builtins.elemAt v.data 0; + val = builtins.elemAt v.data 2; + cmd = "wrapperSuffixEnv ${esc-fn env} "; + in + [ ''echo ${escapeShellArg cmd}"$(cat ${esc-fn val})" >> "$2"'' ] + else if v.type or null == "chdir" then + [ (wrapcmd "cd ${esc-fn v.data}") ] + else if v.type or null == "runShell" then + [ (wrapcmd v.data) ] + else + [ ] + )) + (builtins.concatStringsSep "\n") + ]; + + wrapperInit = + let + setvarfunc = /* fish */ '' + function wrapperSetEnv -a env val + set -gx $env $val + end + ''; + setvardefaultfunc = /* fish */ '' + function wrapperSetEnvDefault -a env val + if not set -q $env + set -gx $env $val + end + end + ''; + prefixvarfunc = /* fish */ '' + function wrapperPrefixEnv -a env + for val in $argv[2..-1] + set -pgx $env $val + end + end + ''; + suffixvarfunc = /* fish */ '' + function wrapperSuffixEnv -a env + for val in $argv[2..-1] + set -agx $env $val + end + end + ''; + in + builtins.concatStringsSep "\n" ( + lib.optional (config.env or { } != { }) setvarfunc + ++ lib.optional (config.envDefault or { } != { }) setvardefaultfunc + ++ lib.optional (config.prefixVar or [ ] != [ ] || config.prefixContent or [ ] != [ ]) prefixvarfunc + ++ lib.optional (config.suffixVar or [ ] != [ ] || config.suffixContent or [ ] != [ ]) suffixvarfunc + ); + + # make the main bin/fish wrapper binary with the arg wrapper items + wrapperTeardown = + let + args = + lib.optional (config.env or { } != { }) "wrapperSetEnv" + ++ lib.optional (config.envDefault or { } != { }) "wrapperSetEnvDefault" + ++ lib.optional ( + config.prefixVar or [ ] != [ ] || config.prefixContent or [ ] != [ ] + ) "wrapperPrefixEnv" + ++ lib.optional ( + config.suffixVar or [ ] != [ ] || config.suffixContent or [ ] != [ ] + ) "wrapperSuffixEnv"; + in + optionalString (args != [ ]) "functions -e ${builtins.concatStringsSep " " args}"; + in + builtins.concatStringsSep "\n" [ + (wrapcmd startSection) + (wrapcmd wrapperInit) + wrapperBuild + (wrapcmd wrapperTeardown) + ''cat "$1" >> "$2"'' + ]; + content = + let + # The plugins with the default config and completion directories will be sourced in a shell loop + # and the others will be sourced individually + configurationPlugins = partition (p: p.configDirs == cfg.pluginConfigDirs) cfg.plugins; + completionPlugins = partition (p: p.completionDirs == cfg.pluginCompletionDirs) cfg.plugins; + + mapPluginsToString = + { + plugins, + dirList, + functor, + multiple ? true, + }: + let + pluginLines = + if (builtins.isFunction dirList) then + map (plugin: map functor (dirList plugin)) plugins + else + map functor dirList; + pluginsToString = plugins: toString (map (p: p.src) plugins); + in + optionalString (plugins != [ ]) '' + set plugin${optionalString multiple "_list"} ${pluginsToString plugins} + ${optionalString multiple "for plugin_dir in $plugin_list"} + ${concatStringsSep "\n " pluginLines} + ${optionalString multiple "end"} + set -e plugin${optionalString multiple "_list"} + ''; + + pluginSources = mapPluginsToString { + plugins = configurationPlugins.right; + dirList = cfg.pluginConfigDirs; + functor = dir: '' + for plugin in $plugin_dir/${dir}/*.fish + source $plugin + end + ''; + }; + pluginCompletions = mapPluginsToString { + plugins = completionPlugins.right; + dirList = cfg.pluginCompletionDirs; + functor = dir: '' + if test -d $plugin_dir/${dir} + set -a fish_complete_path $plugin_dir/${dir} + end + ''; + }; + + customPluginSources = mapPluginsToString { + plugins = configurationPlugins.wrong; + dirList = plugin: plugin.configDirs; + multiple = false; + functor = dir: '' + for plugin in $plugin/${dir}/*.fish + source $plugin + end + ''; + }; + customPluginCompletions = mapPluginsToString { + plugins = completionPlugins.wrong; + dirList = plugin: plugin.configDirs; + multiple = false; + functor = dir: '' + if test -d $plugin/${dir} + set -a fish_complete_path $plugin/${dir} + end + ''; + }; + + mkAbbrArg = attr: abbr: optionalString (abbr.${attr} != null) "--${attr} ${abbr.${attr}}"; + abbrArgs = [ + "position" + "regex" + "command" + "function" + ]; + + mkCursorArg = + abbr: + optionalString ( + abbr.cursor != false + ) "--set-cursor${optionalString (builtins.isString abbr.cursor) "=${abbr.cursor}"}"; + + mkAbbrStr = + abbr: + (foldl' ( + acc: elem: acc + " " + (mkAbbrArg elem abbr) + ) "abbr --add ${abbr.word} ${mkCursorArg abbr}" abbrArgs) + + " " + + "\"${abbr.expansion}\""; + + abbrs = concatStringsSep "\n" (map mkAbbrStr (attrValues cfg.abbreviations)); + aliases = concatStringsSep "\n" ( + mapAttrsToList (name: value: "alias ${name}=\"${value}\"") cfg.shellAliases + ); + + completions = "set -a fish_complete_path ${placeholder config.outputName}/completions"; + in + (concatStringsSep "\n" [ + pluginSources + pluginCompletions + customPluginSources + customPluginCompletions + aliases + abbrs + completions + cfg.configFile.content + ]); + }; + + config.buildCommand.makeWrapper = + let + wrapperEntry = + let + baseArgs = map escapeShellArg [ + config.wrapperPaths.input + config.wrapperPaths.placeholder + ]; + cliArgs = pipe split.args [ + (wlib.makeWrapper.fixArgs { sep = config.flagSeparator or null; }) + ( + { addFlag, appendFlag }: + let + mapArgs = + name: + lib.flip pipe [ + (map ( + v: + let + esc-fn = if v.esc-fn or null != null then v.esc-fn else config.escapingFunction; + in + if builtins.isList (v.data or null) then + map esc-fn v.data + else if v ? data && v.data or null != null then + esc-fn v.data + else + [ ] + )) + lib.flatten + (builtins.concatMap (v: [ + "--${name}" + v + ])) + ]; + in + mapArgs "add-flag" addFlag ++ mapArgs "append-flag" appendFlag + ) + ]; + srcsetup = p: "source ${escapeShellArg "${p}/nix-support/setup-hook"}"; + in + '' + ( + OLD_OPTS="$(set +o)" + ${srcsetup pkgs.dieHook} + ${srcsetup pkgs.makeBinaryWrapper} + eval "$OLD_OPTS" + makeWrapper ${builtins.concatStringsSep " " (baseArgs ++ cliArgs)} + ) + ''; + in + wrapperEntry + "\n" + wlib.makeWrapper.wrapVariants { inherit config pkgs; }; + + config.meta.maintainers = [ wlib.maintainers.ormoyo ]; + config.meta.platforms = lib.platforms.linux; +} diff --git a/wrapperModules/f/fish/variants.nix b/wrapperModules/f/fish/variants.nix new file mode 100644 index 00000000..b227bc6a --- /dev/null +++ b/wrapperModules/f/fish/variants.nix @@ -0,0 +1,59 @@ +{ + lib, + wlib, + pkgs, + ... +}: +{ + options.wrapperVariants = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submoduleWith { + modules = [ + ( + { name, ... }: + { + _file = wlib.modules.makeWrapper; + config.mirror = lib.mkOverride 1400 false; + config.package = lib.mkOverride 1400 (pkgs.${name} or pkgs.hello); + # add back this option we removed from the top level + options.wrapperImplementation = lib.mkOption { + type = lib.types.enum [ + "nix" + "shell" + "binary" + ]; + default = "nix"; + description = '' + the `nix` implementation is the default + + It makes the `escapingFunction` most relevant. + + This is because the `shell` and `binary` implementations + use `pkgs.makeWrapper` or `pkgs.makeBinaryWrapper`, + and arguments to these functions are passed at BUILD time. + + So, generally, when not using the nix implementation, + you should always prefer to have `escapingFunction` + set to `lib.escapeShellArg`. + + However, if you ARE using the `nix` implementation, + using `wlib.escapeShellArgWithEnv` will allow you + to use `$` expansions, which will expand at runtime. + + `binary` implementation is useful for programs + which are likely to be used in "shebangs", + as macos will not allow scripts to be used for these. + + However, it is more limited. It does not have access to + `runShell`, `prefixContent`, and `suffixContent` options. + + Chosing `binary` will thus cause values in those options to be ignored. + ''; + }; + } + ) + ]; + } + ); + }; +} diff --git a/wrapperModules/g/git/check.nix b/wrapperModules/g/git/check.nix index be284e0d..06966848 100644 --- a/wrapperModules/g/git/check.nix +++ b/wrapperModules/g/git/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/g/git/module.nix b/wrapperModules/g/git/module.nix index d660a809..2686adf7 100644 --- a/wrapperModules/g/git/module.nix +++ b/wrapperModules/g/git/module.nix @@ -17,9 +17,10 @@ ''; }; configFile = lib.mkOption { - type = wlib.types.file pkgs; - default.path = config.constructFiles.gitconfig.path; - default.content = ""; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.gitconfig.path; + }; + default = { }; description = "Generated git configuration file."; }; }; diff --git a/wrapperModules/g/glance/check.nix b/wrapperModules/g/glance/check.nix new file mode 100644 index 00000000..b875a267 --- /dev/null +++ b/wrapperModules/g/glance/check.nix @@ -0,0 +1,43 @@ +{ + pkgs, + self, + ... +}: +let + glanceWrapped = self.wrappers.glance.wrap { + inherit pkgs; + settings = { + server.port = 5678; + pages = [ + { + name = "Home"; + columns = [ + { + size = "full"; + widgets = [ + { type = "calendar"; } + { + type = "weather"; + location = "London, United Kingdom"; + } + ]; + } + ]; + } + ]; + }; + }; +in +pkgs.runCommand "glance-test" { nativeBuildInputs = [ pkgs.yq-go ]; } '' + "${glanceWrapped}/bin/glance" config:validate + + config=$("${glanceWrapped}/bin/glance" config:print) + test "$(echo "$config" | yq '.server.port')" = "5678" + test "$(echo "$config" | yq '.pages[0].name')" = "Home" + test "$(echo "$config" | yq '.pages[0].columns[0].size')" = "full" + test "$(echo "$config" | yq '.pages[0].columns[0].widgets[0].type')" = "calendar" + test "$(echo "$config" | yq '.pages[0].columns[0].widgets[1].type')" = "weather" + test "$(echo "$config" | yq '.pages[0].columns[0].widgets[1].location')" = "London, United Kingdom" + + touch $out +'' diff --git a/wrapperModules/g/glance/module.nix b/wrapperModules/g/glance/module.nix new file mode 100644 index 00000000..cab1eb13 --- /dev/null +++ b/wrapperModules/g/glance/module.nix @@ -0,0 +1,31 @@ +{ + config, + lib, + pkgs, + wlib, + ... +}: +{ + imports = [ wlib.modules.default ]; + options = { + settings = lib.mkOption { + inherit (pkgs.formats.yaml { }) type; + default = { }; + description = '' + Configuration for glance. + See + for available options. + ''; + }; + }; + config = { + constructFiles.generatedConfig = { + content = lib.generators.toYAML { } config.settings; + relPath = "${config.binName}.yaml"; + }; + flags."--config" = config.constructFiles.generatedConfig.path; + package = lib.mkDefault pkgs.glance; + + meta.maintainers = [ wlib.maintainers.jtrrll ]; + }; +} diff --git a/wrapperModules/h/halloy/module.nix b/wrapperModules/h/halloy/module.nix new file mode 100644 index 00000000..7cbc14e6 --- /dev/null +++ b/wrapperModules/h/halloy/module.nix @@ -0,0 +1,127 @@ +{ + lib, + wlib, + pkgs, + config, + ... +}: +let + tomlFmt = pkgs.formats.toml { }; +in +{ + imports = [ wlib.modules.default ]; + options = { + settings = lib.mkOption { + inherit (tomlFmt) type; + default = { }; + description = '' + Configuration settings for halloy. All available options can be + found here: . Note that + halloy requires at least one `server` to be configured, see example. + ''; + example = { + "buffer.channel.topic".enabled = true; + "servers.liberachat" = { + nickname = "halloy-user"; + server = "irc.libera.chat"; + channels = [ "#halloy" ]; + }; + }; + }; + themes = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.oneOf [ + tomlFmt.type + lib.types.lines + lib.types.path + ] + ); + default = { }; + description = '' + Themes to be used with halloy. Each theme gets written to a themes directory accessible to halloy. + See for more information. + ''; + example = lib.literalExpression '' + { + my-theme = { + general = { + background = ""; + border = ""; + horizontal_rule = ""; + unread_indicator = ""; + }; + text = { + primary = ""; + secondary = ""; + tertiary = ""; + success = ""; + error = ""; + }; + }; + } + ''; + }; + generatedConfig.output = lib.mkOption { + type = lib.types.str; + default = config.outputName; + description = '' + The derivation output for the config generated by this wrapper module + ''; + }; + generatedConfig.placeholder = lib.mkOption { + type = lib.types.str; + default = "${placeholder config.generatedConfig.output}/${config.binName}-config"; + readOnly = true; + description = '' + The placeholder accessible in the wrapper derivation build script for the config generated by this wrapper module + ''; + }; + }; + + config = { + package = lib.mkDefault pkgs.halloy; + env.XDG_CONFIG_HOME = lib.mkIf (config.settings != { }) config.generatedConfig.placeholder; + passthru = lib.optionalAttrs (config.settings != { }) { + generatedConfig = "${ + config.wrapper.${config.generatedConfig.output} + }/${config.binName}-config/halloy"; + }; + buildCommand.makeThemesAnyway = '' + mkdir -p "${config.generatedConfig.placeholder}/halloy/themes" + ''; + constructFiles = { + config = lib.mkIf (config.settings != { }) { + content = builtins.toJSON config.settings; + relPath = lib.mkOverride 0 "${config.binName}-config/halloy/config.toml"; + output = lib.mkOverride 0 config.generatedConfig.output; + builder = ''mkdir -p "$(dirname "$2")" && ${pkgs.remarshal}/bin/json2toml "$1" "$2"''; + }; + } + // lib.pipe config.themes [ + (lib.filterAttrs (_: theme: theme != { } && theme != "")) + (builtins.mapAttrs ( + name: theme: { + relPath = lib.mkOverride 0 "${config.binName}-config/halloy/themes/${name}.toml"; + output = lib.mkOverride 0 config.generatedConfig.output; + content = + if builtins.isString theme then + theme + else if !(builtins.isPath theme || lib.isStorePath theme) then + builtins.toJSON theme + else + ""; + builder = + if builtins.isString theme then + ''mkdir -p "$(dirname "$2")" && cp "$1" "$2"'' + else if builtins.isPath theme || lib.isStorePath theme then + ''mkdir -p "$(dirname "$2")" && cp ${theme} "$2"'' + else + ''mkdir -p "$(dirname "$2")" && ${pkgs.remarshal}/bin/json2toml "$1" "$2"''; + } + )) + ]; + meta.maintainers = [ wlib.maintainers.ricardomaps ]; + + }; + +} diff --git a/wrapperModules/h/helix/check.nix b/wrapperModules/h/helix/check.nix index cadb67f1..6794e29c 100644 --- a/wrapperModules/h/helix/check.nix +++ b/wrapperModules/h/helix/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/h/helix/module.nix b/wrapperModules/h/helix/module.nix index d5297f2b..44e04312 100644 --- a/wrapperModules/h/helix/module.nix +++ b/wrapperModules/h/helix/module.nix @@ -111,10 +111,9 @@ in } // lib.pipe config.themes [ (lib.filterAttrs (_: theme: theme != { })) - (wlib.mapAttrsToList0 ( - i: name: theme: - lib.nameValuePair name { - key = "theme_${toString i}"; + (builtins.mapAttrs ( + name: theme: { + key = "theme_${name}"; relPath = lib.mkOverride 0 "${config.binName}-config/helix/themes/${name}.toml"; output = lib.mkOverride 0 config.generatedConfig.output; content = if builtins.isString theme then theme else builtins.toJSON theme; @@ -122,7 +121,6 @@ in ''mkdir -p "$(dirname "$2")" && ${pkgs.remarshal}/bin/json2toml "$1" "$2"''; } )) - builtins.listToAttrs ]; config.meta.maintainers = [ wlib.maintainers.birdee ]; } diff --git a/wrapperModules/h/himalaya/module.nix b/wrapperModules/h/himalaya/module.nix new file mode 100644 index 00000000..6d6106a4 --- /dev/null +++ b/wrapperModules/h/himalaya/module.nix @@ -0,0 +1,39 @@ +{ + wlib, + pkgs, + config, + lib, + ... +}: +let + tomlFmt = pkgs.formats.toml { }; +in +{ + imports = [ wlib.modules.default ]; + + options = { + settings = lib.mkOption { + type = tomlFmt.type; + default = { }; + description = '' + Configuration for himalaya mail client CLI + ''; + }; + }; + config = { + package = lib.mkDefault pkgs.himalaya; + constructFiles = { + generatedConfig = { + relPath = "${config.binName}-config.toml"; + content = builtins.toJSON config.settings; + builder = ''mkdir -p "$(dirname "$2")" && ${pkgs.remarshal}/bin/json2toml "$1" "$2"''; + }; + }; + + flags = { + "--config" = lib.mkIf (config.settings != { }) config.constructFiles.generatedConfig.path; + }; + + meta.maintainers = [ wlib.maintainers.rachitvrma ]; + }; +} diff --git a/wrapperModules/h/hyfetch/module.nix b/wrapperModules/h/hyfetch/module.nix new file mode 100644 index 00000000..c72fa21d --- /dev/null +++ b/wrapperModules/h/hyfetch/module.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + wlib, + pkgs, + ... +}: +let + jsonFormat = pkgs.formats.json { }; +in +{ + imports = [ wlib.modules.default ]; + options = { + settings = lib.mkOption { + type = jsonFormat.type; + default = { }; + description = "JSON config for HyFetch"; + example = lib.literalExpression '' + { + preset = "rainbow"; + mode = "rgb"; + color_align = { + mode = "horizontal"; + }; + } + ''; + }; + configFile = lib.mkOption { + type = wlib.types.stringable; + default = config.constructFiles.generatedConfig.path; + description = '' + The path to the config file. Can be anywhere. + + By default points to `config.constructFiles.generatedConfig.path`, which contains the generated result of `config.settings` + ''; + }; + }; + + config = { + package = lib.mkDefault pkgs.hyfetch; + constructFiles.generatedConfig = { + relPath = "${config.binName}-settings.json"; + content = builtins.toJSON config.settings; + }; + flags."--config-file" = config.configFile; + meta.maintainers = [ wlib.maintainers.ricardomaps ]; + }; +} diff --git a/wrapperModules/h/hyprlock/module.nix b/wrapperModules/h/hyprlock/module.nix new file mode 100644 index 00000000..bad9fb63 --- /dev/null +++ b/wrapperModules/h/hyprlock/module.nix @@ -0,0 +1,199 @@ +{ + config, + wlib, + lib, + pkgs, + ... +}: +let + /* + from: + https://github.com/nix-community/home-manager/blob/8a423e444b17dde406097328604a64fc7429e34e/modules/lib/generators.nix + */ + 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 +{ + imports = [ wlib.modules.default ]; + + options = { + settings = lib.mkOption { + /* + from: + https://github.com/nix-community/home-manager/blob/8a423e444b17dde406097328604a64fc7429e34e/modules/programs/hyprlock.nix + */ + type = + with lib.types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "Hyprlock configuration value"; + }; + in + valueType; + default = { }; + example = lib.literalExpression '' + { + general = { + grace = 5; + hide_cursor = true; + ignore_empty_input = true; + }; + + background = [ + { + path = "screenshot"; + blur_passes = 3; + blur_size = 8; + } + ]; + + input-field = [ + { + size = "200, 50"; + position = "0, -80"; + monitor = ""; + dots_center = true; + fade_on_empty = false; + } + ]; + } + ''; + description = '' + Configuration for Hyprlock. + See + ''; + }; + + "hyprlock.conf" = lib.mkOption { + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedConfig.path; + content = ( + lib.optionalString (config.settings != { }) (toHyprconf { + inherit (config) importantPrefixes; + attrs = config.settings; + }) + + lib.optionalString (config.extraConfig != "") config.extraConfig + ); + }; + default = { }; + description = '' + Hyprlock configuration file. + ''; + }; + + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + example = '' + source = /path/to/extra.conf + ''; + description = '' + Extra configuration lines appended to the end of + the Hyprlock configuration file. + ''; + }; + + importantPrefixes = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "$" + "bezier" + "monitor" + "size" + ]; + example = [ + "$" + "bezier" + ]; + description = '' + List of prefix strings whose matching configuration entries + are placed at the top of the generated configuration file. + ''; + }; + }; + + config.package = lib.mkDefault pkgs.hyprlock; + config.flags."--config" = config."hyprlock.conf".path; + + config.constructFiles.generatedConfig = { + content = config."hyprlock.conf".content; + relPath = "${config.binName}.conf"; + }; + + config.meta = { + maintainers = [ wlib.maintainers.nouritsu ]; + platforms = lib.platforms.linux; + }; +} diff --git a/wrapperModules/j/jujutsu/check.nix b/wrapperModules/j/jujutsu/check.nix index 25393569..abdcb388 100644 --- a/wrapperModules/j/jujutsu/check.nix +++ b/wrapperModules/j/jujutsu/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/k/kitty/module.nix b/wrapperModules/k/kitty/module.nix new file mode 100644 index 00000000..3894413e --- /dev/null +++ b/wrapperModules/k/kitty/module.nix @@ -0,0 +1,198 @@ +{ + wlib, + config, + pkgs, + lib, + ... +}: +let + inherit (lib) + types + mkIf + mkOption + mkOrder + optionalString + literalExpression + ; + + fontType = types.submodule { + options = { + name = mkOption { + type = types.str; + example = "DejaVu Sans"; + description = "The family name of the font within the package."; + }; + + size = mkOption { + type = types.nullOr types.number; + default = null; + example = 12; + description = "The size of the font."; + }; + }; + }; + + settingsValueType = + with types; + oneOf [ + str + bool + int + float + ]; + + toKittyConfig = lib.generators.toKeyValue { + mkKeyValue = + key: value: + let + yesNo = v: if v then "yes" else "no"; + value' = (if builtins.isBool value then yesNo else toString) value; + in + "${key} ${value'}"; + }; + + toKittyKeybindings = lib.generators.toKeyValue { + # kitty keybindings are written as `map ` + mkKeyValue = key: command: "map ${key} ${command}"; + }; + + toKittyMouseBindings = lib.generators.toKeyValue { + mkKeyValue = key: command: "mouse_map ${key} ${command}"; + }; + + toKittyActionAliases = lib.generators.toKeyValue { + mkKeyValue = alias_name: action: "action_alias ${alias_name} ${action}"; + }; + + toKittyEnv = lib.generators.toKeyValue { + mkKeyValue = name: value: "env ${name}=${value}"; + }; +in +{ + imports = [ wlib.modules.default ]; + + options = { + settings = mkOption { + type = types.attrsOf settingsValueType; + default = { }; + example = literalExpression '' + { + scrollback_lines = 10000; + enable_audio_bell = false; + update_check_interval = 0; + } + ''; + description = '' + Key/value pairs written into `kitty.conf`. + See . + ''; + }; + + themeFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Apply a Kitty color theme. This option takes the file name of a theme + in `kitty-themes`, without the `.conf` suffix. See + for a + list of themes. + ''; + example = "SpaceGray_Eighties"; + }; + + font = mkOption { + type = types.nullOr fontType; + default = null; + description = "The font to use."; + }; + + actionAliases = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Define action aliases."; + example = lib.literalExpression '' + { + "launch_tab" = "launch --cwd=current --type=tab"; + "launch_window" = "launch --cwd=current --type=os-window"; + } + ''; + }; + + keybindings = mkOption { + type = types.attrsOf types.str; + default = { }; + example = literalExpression '' + { + "ctrl+c" = "copy_or_interrupt"; + "ctrl+f>2" = "set_font_size 20"; + } + ''; + description = "Mapping of keybindings to actions."; + }; + + mouseBindings = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Mapping of mouse bindings to actions."; + example = literalExpression '' + { + "ctrl+left click" = "ungrabbed mouse_handle_click selection link prompt"; + "left click" = "ungrabbed no-op"; + }; + ''; + }; + + environment = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Environment variables to set or override."; + example = literalExpression '' + { + "LS_COLORS" = "1"; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Additional configuration appended verbatim to kitty.conf."; + }; + }; + + config = { + package = lib.mkDefault pkgs.kitty; + + extraConfig = lib.mkMerge [ + (mkIf (config.font != null) ( + mkOrder 510 '' + font_family ${config.font.name} + ${optionalString (config.font.size != null) "font_size ${toString config.font.size}"} + '' + )) + (mkIf (config.themeFile != null) ( + mkOrder 520 '' + include ${pkgs.kitty-themes}/share/kitty-themes/themes/${config.themeFile}.conf + '' + )) + (mkIf (config.actionAliases != { }) (mkOrder 550 (toKittyActionAliases config.actionAliases))) + (mkIf (config.keybindings != { }) (mkOrder 560 (toKittyKeybindings config.keybindings))) + (mkIf (config.mouseBindings != { }) (mkOrder 570 (toKittyMouseBindings config.mouseBindings))) + (mkIf (config.environment != { }) (mkOrder 580 (toKittyEnv config.environment))) + (mkIf (config.settings != { }) (mkOrder 540 (toKittyConfig config.settings))) + ]; + + constructFiles.kittyConfig = { + relPath = "${config.binName}.conf"; + content = '' + # Generated by nix-wrapper-modules. + # See https://sw.kovidgoyal.net/kitty/conf.html + ${config.extraConfig} + ''; + }; + + flags."--config" = config.constructFiles.kittyConfig.path; + + meta.maintainers = [ wlib.maintainers.rachitvrma ]; + }; +} diff --git a/wrapperModules/m/mako/check.nix b/wrapperModules/m/mako/check.nix index 875c4530..7313b067 100644 --- a/wrapperModules/m/mako/check.nix +++ b/wrapperModules/m/mako/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/m/mako/module.nix b/wrapperModules/m/mako/module.nix index c31c6837..61186a95 100644 --- a/wrapperModules/m/mako/module.nix +++ b/wrapperModules/m/mako/module.nix @@ -13,9 +13,10 @@ in imports = [ wlib.modules.default ]; options = { "--config" = lib.mkOption { - type = wlib.types.file pkgs; - default.path = config.constructFiles.generatedConfig.path; - default.content = ""; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedConfig.path; + }; + default = { }; description = '' Path to the generated Mako configuration file. @@ -36,6 +37,13 @@ in ] ); default = { }; + example = { + icon-location = "left"; + "urgency=high" = { + border-color = "#bf616a"; + default-timeout = 0; + }; + }; description = '' Configuration settings for mako. Can include both global settings and sections. All available options can be found here: @@ -57,7 +65,10 @@ in if config."--config".content or "" != "" then config."--config".content else - lib.generators.toINIWithGlobalSection { } { globalSection = config.settings; }; + lib.generators.toINIWithGlobalSection { } { + globalSection = lib.filterAttrs (_: value: !builtins.isAttrs value) config.settings; + sections = lib.filterAttrs (_: value: builtins.isAttrs value) config.settings; + }; relPath = "${config.binName}-config.ini"; }; config.package = lib.mkDefault pkgs.mako; diff --git a/wrapperModules/m/mangowc/check.nix b/wrapperModules/m/mangowc/check.nix new file mode 100644 index 00000000..8caf7391 --- /dev/null +++ b/wrapperModules/m/mangowc/check.nix @@ -0,0 +1,83 @@ +{ + pkgs, + self, + ... +}: +let + mangowcWrapped = self.wrappers.mangowc.wrap { + inherit pkgs; + + settings = { + # Window effects + blur = 1; + blur_optimized = 1; + blur_params = { + radius = 5; + num_passes = 2; + }; + border_radius = 6; + focused_opacity = 1.0; + + # Animations - use underscores for multi-part keys + animations = 1; + animation_type_open = "slide"; + animation_type_close = "slide"; + animation_duration_open = 400; + animation_duration_close = 800; + + # Or use nested attrs (will be flattened with underscores) + animation_curve = { + open = "0.46,1.0,0.29,1"; + close = "0.08,0.92,0,1"; + }; + + # Use lists for duplicate keys like bind and tagrule + bind = [ + "SUPER,r,reload_config" + "Alt,space,spawn,rofi -show drun" + "Alt,Return,spawn,foot" + "ALT,R,setkeymode,resize" # Enter resize mode + ]; + + tagrule = [ + "id:1,layout_name:tile" + "id:2,layout_name:scroller" + ]; + + # Keymodes (submaps) for modal keybindings + keymode = { + resize = { + bind = [ + "NONE,Left,resizewin,-10,0" + "NONE,Escape,setkeymode,default" + ]; + }; + }; + }; + + sourcedFiles = [ + ./config.conf + ]; + + autostart_sh = '' + # spawn terminal on startup + ${pkgs.lib.getExe pkgs.foot} + ''; + + extraConfig = '' + # menu and terminal + bind=Alt,space,spawn,rofi -show drun + bind=Alt,Return,spawn,${pkgs.lib.getExe pkgs.foot} + ''; + }; +in +if builtins.elem pkgs.stdenv.hostPlatform.system self.wrappers.mangowc.meta.platforms then + pkgs.runCommand "mangowc-test" { } '' + cat ${mangowcWrapped}/bin/mango + cat ${mangowcWrapped}/config.conf + "${mangowcWrapped}/bin/mango" -v | grep -q "${mangowcWrapped.version}" + "${mangowcWrapped}/bin/mango" -p + touch $out + '' +else + null diff --git a/wrapperModules/m/mangowc/config.conf b/wrapperModules/m/mangowc/config.conf new file mode 100644 index 00000000..babd264a --- /dev/null +++ b/wrapperModules/m/mangowc/config.conf @@ -0,0 +1,256 @@ +# Taken from https://github.com/mangowm/mango/ + +# Window effect +blur=0 +blur_layer=0 +blur_optimized=1 +blur_params_num_passes = 2 +blur_params_radius = 5 +blur_params_noise = 0.02 +blur_params_brightness = 0.9 +blur_params_contrast = 0.9 +blur_params_saturation = 1.2 + +shadows = 0 +layer_shadows = 0 +shadow_only_floating = 1 +shadows_size = 10 +shadows_blur = 15 +shadows_position_x = 0 +shadows_position_y = 0 +shadowscolor= 0x000000ff + +border_radius=6 +no_radius_when_single=0 +focused_opacity=1.0 +unfocused_opacity=1.0 + +# Animation Configuration(support type:zoom,slide) +# tag_animation_direction: 1-horizontal,0-vertical +animations=1 +layer_animations=1 +animation_type_open=slide +animation_type_close=slide +animation_fade_in=1 +animation_fade_out=1 +tag_animation_direction=1 +zoom_initial_ratio=0.4 +zoom_end_ratio=0.8 +fadein_begin_opacity=0.5 +fadeout_begin_opacity=0.8 +animation_duration_move=500 +animation_duration_open=400 +animation_duration_tag=350 +animation_duration_close=800 +animation_duration_focus=0 +animation_curve_open=0.46,1.0,0.29,1 +animation_curve_move=0.46,1.0,0.29,1 +animation_curve_tag=0.46,1.0,0.29,1 +animation_curve_close=0.08,0.92,0,1 +animation_curve_focus=0.46,1.0,0.29,1 +animation_curve_opafadeout=0.5,0.5,0.5,0.5 +animation_curve_opafadein=0.46,1.0,0.29,1 + +# Scroller Layout Setting +scroller_structs=20 +scroller_default_proportion=0.8 +scroller_focus_center=0 +scroller_prefer_center=0 +edge_scroller_pointer_focus=1 +scroller_default_proportion_single=1.0 +scroller_proportion_preset=0.5,0.8,1.0 + +# Master-Stack Layout Setting +new_is_master=1 +default_mfact=0.55 +default_nmaster=1 +smartgaps=0 + +# Overview Setting +hotarea_size=10 +enable_hotarea=1 +ov_tab_mode=0 +overviewgappi=5 +overviewgappo=30 + +# Misc +no_border_when_single=0 +axis_bind_apply_timeout=100 +focus_on_activate=1 +idleinhibit_ignore_visible=0 +sloppyfocus=1 +warpcursor=1 +focus_cross_monitor=0 +focus_cross_tag=0 +enable_floating_snap=0 +snap_distance=30 +cursor_size=24 +drag_tile_to_tile=1 + +# keyboard +repeat_rate=25 +repeat_delay=600 +numlockon=0 +xkb_rules_layout=us + +# Trackpad +# need relogin to make it apply +disable_trackpad=0 +tap_to_click=1 +tap_and_drag=1 +drag_lock=1 +trackpad_natural_scrolling=0 +disable_while_typing=1 +left_handed=0 +middle_button_emulation=0 +swipe_min_threshold=1 + +# mouse +# need relogin to make it apply +mouse_natural_scrolling=0 + +# Appearance +gappih=5 +gappiv=5 +gappoh=10 +gappov=10 +scratchpad_width_ratio=0.8 +scratchpad_height_ratio=0.9 +borderpx=4 +rootcolor=0x201b14ff +bordercolor=0x444444ff +focuscolor=0xc9b890ff +maximizescreencolor=0x89aa61ff +urgentcolor=0xad401fff +scratchpadcolor=0x516c93ff +globalcolor=0xb153a7ff +overlaycolor=0x14a57cff + +# layout support: +# tile,scroller,grid,deck,monocle,center_tile,vertical_tile,vertical_scroller +tagrule=id:1,layout_name:tile +tagrule=id:2,layout_name:tile +tagrule=id:3,layout_name:tile +tagrule=id:4,layout_name:tile +tagrule=id:5,layout_name:tile +tagrule=id:6,layout_name:tile +tagrule=id:7,layout_name:tile +tagrule=id:8,layout_name:tile +tagrule=id:9,layout_name:tile + +# Key Bindings +# key name refer to `xev` or `wev` command output, +# mod keys name: super,ctrl,alt,shift,none + +# reload config +bind=SUPER,r,reload_config + +# menu and terminal +bind=Alt,space,spawn,rofi -show drun +bind=Alt,Return,spawn,foot + +# exit +bind=SUPER,m,quit +bind=ALT,q,killclient, + +# switch window focus +bind=SUPER,Tab,focusstack,next +bind=ALT,Left,focusdir,left +bind=ALT,Right,focusdir,right +bind=ALT,Up,focusdir,up +bind=ALT,Down,focusdir,down + +# swap window +bind=SUPER+SHIFT,Up,exchange_client,up +bind=SUPER+SHIFT,Down,exchange_client,down +bind=SUPER+SHIFT,Left,exchange_client,left +bind=SUPER+SHIFT,Right,exchange_client,right + +# switch window status +bind=SUPER,g,toggleglobal, +bind=ALT,Tab,toggleoverview, +bind=ALT,backslash,togglefloating, +bind=ALT,a,togglemaximizescreen, +bind=ALT,f,togglefullscreen, +bind=ALT+SHIFT,f,togglefakefullscreen, +bind=SUPER,i,minimized, +bind=SUPER,o,toggleoverlay, +bind=SUPER+SHIFT,I,restore_minimized +bind=ALT,z,toggle_scratchpad + +# scroller layout +bind=ALT,e,set_proportion,1.0 +bind=ALT,x,switch_proportion_preset, + +# switch layout +bind=SUPER,n,switch_layout + +# tag switch +bind=SUPER,Left,viewtoleft,0 +bind=CTRL,Left,viewtoleft_have_client,0 +bind=SUPER,Right,viewtoright,0 +bind=CTRL,Right,viewtoright_have_client,0 +bind=CTRL+SUPER,Left,tagtoleft,0 +bind=CTRL+SUPER,Right,tagtoright,0 + +bind=Ctrl,1,view,1,0 +bind=Ctrl,2,view,2,0 +bind=Ctrl,3,view,3,0 +bind=Ctrl,4,view,4,0 +bind=Ctrl,5,view,5,0 +bind=Ctrl,6,view,6,0 +bind=Ctrl,7,view,7,0 +bind=Ctrl,8,view,8,0 +bind=Ctrl,9,view,9,0 + +# tag: move client to the tag and focus it +# tagsilent: move client to the tag and not focus it +# bind=Alt,1,tagsilent,1 +bind=Alt,1,tag,1,0 +bind=Alt,2,tag,2,0 +bind=Alt,3,tag,3,0 +bind=Alt,4,tag,4,0 +bind=Alt,5,tag,5,0 +bind=Alt,6,tag,6,0 +bind=Alt,7,tag,7,0 +bind=Alt,8,tag,8,0 +bind=Alt,9,tag,9,0 + +# monitor switch +bind=alt+shift,Left,focusmon,left +bind=alt+shift,Right,focusmon,right +bind=SUPER+Alt,Left,tagmon,left +bind=SUPER+Alt,Right,tagmon,right + +# gaps +bind=ALT+SHIFT,X,incgaps,1 +bind=ALT+SHIFT,Z,incgaps,-1 +bind=ALT+SHIFT,R,togglegaps + +# movewin +bind=CTRL+SHIFT,Up,movewin,+0,-50 +bind=CTRL+SHIFT,Down,movewin,+0,+50 +bind=CTRL+SHIFT,Left,movewin,-50,+0 +bind=CTRL+SHIFT,Right,movewin,+50,+0 + +# resizewin +bind=CTRL+ALT,Up,resizewin,+0,-50 +bind=CTRL+ALT,Down,resizewin,+0,+50 +bind=CTRL+ALT,Left,resizewin,-50,+0 +bind=CTRL+ALT,Right,resizewin,+50,+0 + +# Mouse Button Bindings +# btn_left and btn_right can't bind none mod key +mousebind=SUPER,btn_left,moveresize,curmove +mousebind=NONE,btn_middle,togglemaximizescreen,0 +mousebind=SUPER,btn_right,moveresize,curresize + + +# Axis Bindings +axisbind=SUPER,UP,viewtoleft_have_client +axisbind=SUPER,DOWN,viewtoright_have_client + + +# layer rule +layerrule=animation_type_open:zoom,layer_name:rofi +layerrule=animation_type_close:zoom,layer_name:rofi diff --git a/wrapperModules/m/mangowc/lib.nix b/wrapperModules/m/mangowc/lib.nix new file mode 100644 index 00000000..f4703e1f --- /dev/null +++ b/wrapperModules/m/mangowc/lib.nix @@ -0,0 +1,313 @@ +# Taken from https://github.com/mangowm/mango/blob/main/nix/lib.nix +lib: +let + inherit (lib) + attrNames + filterAttrs + foldl + generators + partition + removeAttrs + ; + + inherit (lib.strings) + concatMapStrings + hasPrefix + ; + + /** + Convert a structured Nix attribute set into Mango's configuration format. + + This function takes a nested attribute set and converts it into Mango-compatible + configuration syntax, supporting top, bottom, and regular command sections. + + Commands are flattened using the `flattenAttrs` function, and attributes are formatted as + `key = value` pairs. Lists are expanded as duplicate keys to match Mango's expected format. + + Configuration: + + * `topCommandsPrefixes` - A list of prefixes to define **top** commands (default: `[]`). + * `bottomCommandsPrefixes` - A list of prefixes to define **bottom** commands (default: `[]`). + + Attention: + + - The function ensures top commands appear **first** and bottom commands **last**. + - The generated configuration is a **single string**, suitable for writing to a config file. + - Lists are converted into multiple entries, ensuring compatibility with Mango. + + # Inputs + + Structured function argument: + + : topCommandsPrefixes (optional, default: `[]`) + : A list of prefixes that define **top** commands. Any key starting with one of these + prefixes will be placed at the beginning of the configuration. + : bottomCommandsPrefixes (optional, default: `[]`) + : A list of prefixes that define **bottom** commands. Any key starting with one of these + prefixes will be placed at the end of the configuration. + + Value: + + : The attribute set to be converted to Hyprland configuration format. + + # Type + + ``` + toMango :: AttrSet -> AttrSet -> String + ``` + + # Examples + :::{.example} + + ## Basic mangowc configuration + + ```nix + let + config = { + blur = 1; + blur_params_radius = 5; + border_radius = 6; + animations = 1; + animation_duration_open = 400; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + animations = 1 + animation_duration_open = 400 + blur = 1 + blur_params_radius = 5 + border_radius = 6 + ``` + + ## Using nested attributes + + ```nix + let + config = { + blur = 1; + blur_params = { + radius = 5; + num_passes = 2; + noise = 0.02; + }; + animation_curve = { + open = "0.46,1.0,0.29,1"; + close = "0.08,0.92,0,1"; + }; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + animation_curve_close = 0.08,0.92,0,1 + animation_curve_open = 0.46,1.0,0.29,1 + blur = 1 + blur_params_noise = 0.02 + blur_params_num_passes = 2 + blur_params_radius = 5 + ``` + + ## Using lists for duplicate keys + + ```nix + let + config = { + bind = [ + "SUPER,r,reload_config" + "Alt,space,spawn,rofi -show drun" + "Alt,Return,spawn,foot" + ]; + tagrule = [ + "id:1,layout_name:tile" + "id:2,layout_name:scroller" + ]; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + bind = SUPER,r,reload_config + bind = Alt,space,spawn,rofi -show drun + bind = Alt,Return,spawn,foot + tagrule = id:1,layout_name:tile + tagrule = id:2,layout_name:scroller + ``` + + ## Using keymodes (submaps) + + ```nix + let + config = { + bind = [ + "SUPER,Q,killclient" + "ALT,R,setkeymode,resize" + ]; + keymode = { + resize = { + bind = [ + "NONE,Left,resizewin,-10,0" + "NONE,Right,resizewin,10,0" + "NONE,Escape,setkeymode,default" + ]; + }; + }; + }; + in lib.toMango {} config + ``` + + **Output:** + ``` + bind = SUPER,Q,killclient + bind = ALT,R,setkeymode,resize + + keymode = resize + bind = NONE,Left,resizewin,-10,0 + bind = NONE,Right,resizewin,10,0 + bind = NONE,Escape,setkeymode,default + ``` + + ::: + */ + toMango = + { + topCommandsPrefixes ? [ ], + bottomCommandsPrefixes ? [ ], + }: + attrs: + let + toMango' = + attrs: + let + # Specially configured `toKeyValue` generator with support for duplicate keys + # and a legible key-value separator. + mkCommands = generators.toKeyValue { + mkKeyValue = generators.mkKeyValueDefault { } " = "; + listsAsDuplicateKeys = true; + indent = ""; # No indent, since we don't have nesting + }; + + # Extract keymode definitions if they exist + keymodes = attrs.keymode or { }; + attrsWithoutKeymodes = removeAttrs attrs [ "keymode" ]; + + # Generate keymode blocks + # Format: keymode=name\nbind=...\nbind=...\n + mkKeymodeBlock = + name: modeAttrs: + let + modeCommands = flattenAttrs (p: k: "${p}_${k}") modeAttrs; + in + "keymode = ${name}\n${mkCommands modeCommands}"; + + keymodeBlocks = + if keymodes == { } then + "" + else + "\n" + concatMapStrings (name: mkKeymodeBlock name keymodes.${name} + "\n") (attrNames keymodes); + + # Flatten the attrset, combining keys in a "path" like `"a_b_c" = "x"`. + # Uses `flattenAttrs` with an underscore separator. + commands = flattenAttrs (p: k: "${p}_${k}") attrsWithoutKeymodes; + + # General filtering function to check if a key starts with any prefix in a given list. + filterCommands = list: n: foldl (acc: prefix: acc || hasPrefix prefix n) false list; + + # Partition keys into top commands and the rest + result = partition (filterCommands topCommandsPrefixes) (attrNames commands); + topCommands = filterAttrs (n: _: builtins.elem n result.right) commands; + remainingCommands = removeAttrs commands result.right; + + # Partition remaining commands into bottom commands and regular commands + result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong; + bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands; + regularCommands = removeAttrs remainingCommands result2.right; + in + # Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands. + # Keymodes are appended at the end. + concatMapStrings mkCommands [ + topCommands + regularCommands + bottomCommands + ] + + keymodeBlocks; + in + toMango' attrs; + + /** + Flatten a nested attribute set into a flat attribute set, using a custom key separator function. + + This function recursively traverses a nested attribute set and produces a flat attribute set + where keys are joined using a user-defined function (`pred`). It allows transforming deeply + nested structures into a single-level attribute set while preserving key-value relationships. + + Configuration: + + * `pred` - A function `(string -> string -> string)` defining how keys should be concatenated. + + # Inputs + + Structured function argument: + + : pred (required) + : A function that determines how parent and child keys should be combined into a single key. + It takes a `prefix` (parent key) and `key` (current key) and returns the joined key. + + Value: + + : The nested attribute set to be flattened. + + # Type + + ``` + flattenAttrs :: (String -> String -> String) -> AttrSet -> AttrSet + ``` + + # Examples + :::{.example} + + ```nix + let + nested = { + a = "3"; + b = { c = "4"; d = "5"; }; + }; + + separator = (prefix: key: "${prefix}.${key}"); # Use dot notation + in lib.flattenAttrs separator nested + ``` + + **Output:** + ```nix + { + "a" = "3"; + "b.c" = "4"; + "b.d" = "5"; + } + ``` + + ::: + */ + flattenAttrs = + pred: attrs: + let + flattenAttrs' = + prefix: attrs: + builtins.foldl' ( + acc: key: + let + value = attrs.${key}; + newKey = if prefix == "" then key else pred prefix key; + in + acc // (if builtins.isAttrs value then flattenAttrs' newKey value else { "${newKey}" = value; }) + ) { } (builtins.attrNames attrs); + in + flattenAttrs' "" attrs; +in +{ + inherit flattenAttrs toMango; +} diff --git a/wrapperModules/m/mangowc/module.nix b/wrapperModules/m/mangowc/module.nix new file mode 100644 index 00000000..69fc4493 --- /dev/null +++ b/wrapperModules/m/mangowc/module.nix @@ -0,0 +1,259 @@ +{ + config, + wlib, + lib, + pkgs, + ... +}: +{ + imports = [ wlib.modules.default ]; + + options = + let + inherit (lib) + literalExpression + mkOption + mkOptionDefault + types + ; + in + { + settings = mkOption { + type = + with types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "Mango configuration value"; + }; + in + valueType; + default = { }; + description = '' + Mango configuration written in Nix. Entries with the same key + should be written as lists. Variables and colors names should be + quoted. See for more examples. + + Note: This option uses a structured format that is converted to Mango's + configuration syntax. Nested attributes are flattened with underscore separators. + For example: `animation.duration_open = 400` becomes `animation_duration_open = 400` + + Keymodes (submaps) are supported via the special `keymode` attribute. Each keymode + is a nested attribute set under `keymode` that contains its own bindings. + ''; + example = literalExpression '' + { + # Window effects + blur = 1; + blur_optimized = 1; + blur_params = { + radius = 5; + num_passes = 2; + }; + border_radius = 6; + focused_opacity = 1.0; + + # Animations - use underscores for multi-part keys + animations = 1; + animation_type_open = "slide"; + animation_type_close = "slide"; + animation_duration_open = 400; + animation_duration_close = 800; + + # Or use nested attrs (will be flattened with underscores) + animation_curve = { + open = "0.46,1.0,0.29,1"; + close = "0.08,0.92,0,1"; + }; + + # Use lists for duplicate keys like bind and tagrule + bind = [ + "SUPER,r,reload_config" + "Alt,space,spawn,rofi -show drun" + "Alt,Return,spawn,foot" + "ALT,R,setkeymode,resize" # Enter resize mode + ]; + + tagrule = [ + "id:1,layout_name:tile" + "id:2,layout_name:scroller" + ]; + + # Keymodes (submaps) for modal keybindings + keymode = { + resize = { + bind = [ + "NONE,Left,resizewin,-10,0" + "NONE,Escape,setkeymode,default" + ]; + }; + }; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration lines to add to the end of the generated config file. + This is useful for advanced configurations that don't fit the structured + settings format, or for options that aren't yet supported by the module. + ''; + example = '' + # Advanced config that doesn't fit structured format + special_option = 1 + ''; + }; + + topPrefixes = mkOption { + type = with types; listOf str; + default = [ ]; + description = '' + List of prefixes for attributes that should appear at the top of the config file. + Attributes starting with these prefixes will be sorted to the beginning. + ''; + example = [ "source" ]; + }; + + bottomPrefixes = mkOption { + type = with types; listOf str; + default = [ ]; + description = '' + List of prefixes for attributes that should appear at the bottom of the config file. + Attributes starting with these prefixes will be sorted to the end. + ''; + example = [ "source" ]; + }; + + autostart_sh = mkOption { + description = '' + Shell script to run on mango startup. No shebang needed. + + When this option is set, the script will be written to + `~/.config/mango/autostart.sh` and an `exec-once` line + will be automatically added to the config to execute it. + ''; + type = types.lines; + default = ""; + example = '' + waybar & + dunst & + ''; + }; + + sourcedFiles = mkOption { + type = types.listOf wlib.types.stringable; + description = '' + Paths to files that will be sourced at the top of the generated config file. + ''; + default = [ ]; + example = literalExpression '' + [ + ./config.conf + ./binds.conf + ./theme.conf + ] + ''; + }; + + configFile = mkOption { + type = wlib.types.file { + path = mkOptionDefault config.constructFiles.generatedConfig.path; + }; + default = { }; + description = '' + The config file that mango will set as its primary config file. + By default, this file will be generated from whatever the other options are set to. + + Note: If `configFile.path` is set, it will be used INSTEAD of the generated configuration. The generated file will still be created, however, and you can source it. If you don't want the generated config to get overwritten, add the path you want to use to the `sourcedFiles` option and don't set `configFile.path`. + + If `configFile.content` is set, it will replace the contents of the generated config file entirely. If you don't want the generated config to get overwritten, set the `extraConfig` option and don't set `configFile.content`. + ''; + example = literalExpression '' + { + path = ./config.conf; + # or + content = "bind=Alt,space,spawn,rofi -show drun"; + } + ''; + }; + + extraContent = mkOption { + type = types.lines; + default = ""; + internal = true; + }; + }; + + config = { + # Gives an error when using a bad config. + drv.installPhase = '' + runHook preInstall + ${lib.getExe config.package} -c ${config.configFile.path} -p + runHook postInstall + ''; + + constructFiles.generatedConfig = { + relPath = "config.conf"; + content = + if config.configFile.content or "" != "" then + config.configFile.content + else + let + settingsString = + let + inherit (import ./lib.nix lib) toMango; + in + toMango { + topCommandsPrefixes = config.topPrefixes; + bottomCommandsPrefixes = config.bottomPrefixes; + } config.settings; + isImpurePath = s: builtins.isString s && !builtins.hasContext s; + sourcedFileToSourceExpression = + sourcedFile: + if isImpurePath sourcedFile then "source-optional=${sourcedFile}" else "source=${sourcedFile}"; + extraConfig = + if config.extraContent or "" != "" then + lib.warn "wrapperModules.mangowc: config.extraContent is deprecated, please use config.extraConfig instead" ( + config.extraContent + ) + else + config.extraConfig; + in + (lib.strings.concatMapStringsSep "\n" sourcedFileToSourceExpression config.sourcedFiles) + + "\n" + + settingsString + + "\n" + + extraConfig + + "\n" + + lib.optionalString ( + config.autostart_sh != "" + ) "\nexec-once=${config.constructFiles.autostart_sh.path}\n"; + }; + + constructFiles.autostart_sh = { + relPath = "autostart_sh"; + content = config.autostart_sh; + builder = '' + mkdir -p "$(dirname "$2")" && printf '%s\n' ${lib.escapeShellArg "#!${pkgs.bash}${pkgs.bash.shellPath}"} > "$2" && cat "$1" >> "$2" && chmod +x "$2" + ''; + }; + + flags."-c" = config.configFile.path; + package = lib.mkDefault pkgs.mangowc; + passthru.providedSessions = config.package.passthru.providedSessions; + + meta.platforms = lib.platforms.linux; + meta.maintainers = [ wlib.maintainers.pengolord ]; + }; +} diff --git a/wrapperModules/m/mdbook/module.nix b/wrapperModules/m/mdbook/module.nix index 5c8267a1..8b91c2fb 100644 --- a/wrapperModules/m/mdbook/module.nix +++ b/wrapperModules/m/mdbook/module.nix @@ -105,8 +105,7 @@ let assert (node.depth or null != null) || throw "Type error: node must have depth given by sortBook function"; let - genStr = str: num: builtins.concatStringsSep "" (builtins.genList (_: str) num); - i = genStr " " node.depth; + i = wlib.repeatStr " " node.depth; in if node.data == "title" then assert (node.name != null) || throw "Type error: title node must have name"; @@ -189,7 +188,7 @@ let linkCmds = builtins.concatStringsSep "\n" (map mkLink sortedBook); }; - tomltype = lib.types.json // { + tomltype = (lib.types.json or (pkgs.formats.json { }).type) // { description = "nullable TOML value"; }; diff --git a/wrapperModules/m/mpv/check.nix b/wrapperModules/m/mpv/check.nix index aecfab6e..4c54d9f6 100644 --- a/wrapperModules/m/mpv/check.nix +++ b/wrapperModules/m/mpv/check.nix @@ -1,33 +1,168 @@ { pkgs, self, + ... }: let - mpvWrapped = - (self.wrappers.mpv.apply { - inherit pkgs; - scripts = [ - pkgs.mpvScripts.visualizer - ]; - "mpv.conf".content = '' - ao=null - vo=null + mpvWithPkgs = self.wrappers.mpv.apply { inherit pkgs; }; + mpvWithoutConfigDirectory = mpvWithPkgs.wrap { + script.visualizer.path = pkgs.mpvScripts.visualizer; + "mpv.conf".content = '' + ao=null + vo=null + ''; + }; + mpvWithConfigDirectory = mpvWithPkgs.wrap { + script.visualizer.path = pkgs.mpvScripts.visualizer; + configDir = { + "script-opts/visualizer.conf".content = '' + mode="force" ''; - }).wrapper; + }; + "mpv.conf".content = '' + ao=null + vo=null + ''; + }; + mpvWithScriptOptsAppend = mpvWithPkgs.wrap { + script = { + "visualizer.lua" = { + path = pkgs.mpvScripts.visualizer; + opts = { + mode = "force"; + custom_opt = "test_value"; + }; + }; + }; + "mpv.conf".content = '' + ao=null + vo=null + ''; + }; + mpvWithScriptPath = mpvWithPkgs.wrap { + script = { + "my-script.lua" = { + path = pkgs.mpvScripts.visualizer + "/share/mpv/scripts/visualizer.lua"; + opts = { + mode = "force"; + }; + }; + }; + "mpv.conf".content = '' + ao=null + vo=null + ''; + }; + mpvWithNixpkgsScriptViaPath = mpvWithPkgs.wrap { + script = { + visualizer = { + path = pkgs.mpvScripts.visualizer; + opts = { + mode = "force"; + }; + }; + }; + "mpv.conf".content = '' + ao=null + vo=null + ''; + }; in pkgs.runCommand "mpv-test" { } '' - res="$(${mpvWrapped}/bin/mpv --version)" + res="$(${mpvWithoutConfigDirectory}/bin/mpv --version)" if ! echo "$res" | grep "mpv"; then echo "failed to run wrapped package!" - echo "wrapper content for ${mpvWrapped}/bin/mpv" - cat "${mpvWrapped}/bin/mpv" + echo "wrapper content for ${mpvWithoutConfigDirectory}/bin/mpv" + cat "${mpvWithoutConfigDirectory}/bin/mpv" exit 1 fi - if ! cat "${mpvWrapped.configuration.package}/bin/mpv" | LC_ALL=C grep -a -F "share/mpv/scripts/visualizer.lua"; then + if ! cat "${mpvWithoutConfigDirectory.configuration.package}/bin/mpv" | LC_ALL=C grep -a -F "share/mpv/scripts/visualizer.lua"; then echo "failed to find added script when inspecting overriden package value" - echo "overriden package value ${mpvWrapped.configuration.package}/bin/mpv" - cat "${mpvWrapped.configuration.package}/bin/mpv" + echo "overriden package value ${mpvWithoutConfigDirectory.configuration.package}/bin/mpv" + cat "${mpvWithoutConfigDirectory.configuration.package}/bin/mpv" + exit 1 + fi + + res="$(${mpvWithConfigDirectory}/bin/mpv --version)" + if ! echo "$res" | grep "mpv"; then + echo "failed to run wrapped package with config directory!" + echo "wrapper content for ${mpvWithConfigDirectory}/bin/mpv" + cat "${mpvWithConfigDirectory}/bin/mpv" + exit 1 + fi + if ! cat "${mpvWithConfigDirectory.configuration.package}/bin/mpv" | LC_ALL=C grep -a -F "share/mpv/scripts/visualizer.lua"; then + echo "failed to find added script when inspecting overriden package value with config directory" + echo "overriden package value ${mpvWithConfigDirectory.configuration.package}/bin/mpv" + cat "${mpvWithConfigDirectory.configuration.package}/bin/mpv" + exit 1 + fi + if ! grep -q "force" "${mpvWithConfigDirectory}/mpv-config/script-opts/visualizer.conf"; then + echo "failed to read script options from config directory" + exit 1 + fi + + res="$(${mpvWithScriptOptsAppend}/bin/mpv --version)" + if ! echo "$res" | grep "mpv"; then + echo "failed to run wrapped package with script-opts-append!" + echo "wrapper content for ${mpvWithScriptOptsAppend}/bin/mpv" + cat "${mpvWithScriptOptsAppend}/bin/mpv" + exit 1 + fi + if ! grep -q "script-opts-append" "${mpvWithScriptOptsAppend}/bin/mpv"; then + echo "failed to find --script-opts-append flag in wrapper" + cat "${mpvWithScriptOptsAppend}/bin/mpv" + exit 1 + fi + if ! grep -q "visualizer-mode=force" "${mpvWithScriptOptsAppend}/bin/mpv"; then + echo "failed to find visualizer_mode=force in --script-opts-append" + cat "${mpvWithScriptOptsAppend}/bin/mpv" + exit 1 + fi + if ! grep -q "visualizer-custom_opt=test_value" "${mpvWithScriptOptsAppend}/bin/mpv"; then + echo "failed to find visualizer_custom_opt=test_value in --script-opts-append" + cat "${mpvWithScriptOptsAppend}/bin/mpv" + exit 1 + fi + + res="$(${mpvWithScriptPath}/bin/mpv --version)" + if ! echo "$res" | grep "mpv"; then + echo "failed to run wrapped package with script path!" + echo "wrapper content for ${mpvWithScriptPath}/bin/mpv" + cat "${mpvWithScriptPath}/bin/mpv" + exit 1 + fi + if ! grep -q "scripts-append" "${mpvWithScriptPath}/bin/mpv"; then + echo "failed to find --scripts-append flag in wrapper with path" + cat "${mpvWithScriptPath}/bin/mpv" + exit 1 + fi + if ! grep -q "my_script-mode=force" "${mpvWithScriptPath}/bin/mpv"; then + echo "failed to find my_script-mode=force in --script-opts-append with path" + cat "${mpvWithScriptPath}/bin/mpv" + exit 1 + fi + + res="$(${mpvWithNixpkgsScriptViaPath}/bin/mpv --version)" + if ! echo "$res" | grep "mpv"; then + echo "failed to run wrapped package with nixpkgs script via path!" + echo "wrapper content for ${mpvWithNixpkgsScriptViaPath}/bin/mpv" + cat "${mpvWithNixpkgsScriptViaPath}/bin/mpv" + exit 1 + fi + if grep -q "scripts-append" "${mpvWithNixpkgsScriptViaPath}/bin/mpv"; then + echo "FAIL: nixpkgs script should NOT use --scripts-append" + cat "${mpvWithNixpkgsScriptViaPath}/bin/mpv" + exit 1 + fi + if ! cat "${mpvWithNixpkgsScriptViaPath.configuration.package}/bin/mpv" | LC_ALL=C grep -a -F "share/mpv/scripts/visualizer.lua"; then + echo "FAIL: nixpkgs script should be in override (package bin/mpv)" + cat "${mpvWithNixpkgsScriptViaPath.configuration.package}/bin/mpv" + exit 1 + fi + if ! grep -q "visualizer-mode=force" "${mpvWithNixpkgsScriptViaPath}/bin/mpv"; then + echo "FAIL: nixpkgs script opts should still use --script-opts-append" + cat "${mpvWithNixpkgsScriptViaPath}/bin/mpv" exit 1 fi touch $out diff --git a/wrapperModules/m/mpv/module.nix b/wrapperModules/m/mpv/module.nix index 116f786e..4d8c58eb 100644 --- a/wrapperModules/m/mpv/module.nix +++ b/wrapperModules/m/mpv/module.nix @@ -5,24 +5,150 @@ pkgs, ... }: +let + removeExt = + s: + let + m = builtins.match "^(.*)\\.[^.]*$" s; + in + if m == null then s else builtins.elemAt m 0; + isAlphaNum = + v: + let + isUpper = c: c >= "A" && c <= "Z"; + isLower = c: c >= "a" && c <= "z"; + isDigit = c: c >= "0" && c <= "9"; + in + isUpper v || isLower v || isDigit v || v == "_"; + sanitizeScriptName = lib.flip lib.pipe [ + baseNameOf + removeExt + lib.stringToCharacters + (map (v: if isAlphaNum v then v else "_")) + (builtins.concatStringsSep "") + ]; + + partitioned = + let + partitioned = wlib.partitionAttrs ( + name: v: builtins.isString (v.path.passthru.scriptName or null) + ) (lib.filterAttrs (n: v: v.enable) config.script); + in + { + nixpkgsScripts = lib.mapAttrsToList (n: v: v.path) partitioned.right; + userScripts = partitioned.wrong; + }; +in { imports = [ wlib.modules.default ]; options = { scripts = lib.mkOption { type = lib.types.listOf lib.types.package; default = [ ]; + internal = true; + apply = + x: + lib.warnIf (x != [ ]) + "nix-wrapper-modules#mpv deprecation warning: `config.scripts` is deprecated, use `config.script..path = pkgs.mpvScripts.` instead" + x; + description = '' + deprecated: use `config.script..path = pkgs.mpvScripts.` + ''; + }; + script = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + enable = lib.mkEnableOption name // { + default = true; + }; + opts = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.nullOr ( + lib.types.oneOf [ + lib.types.number + lib.types.bool + wlib.types.stringable + ] + ) + ); + default = { }; + description = '' + Script options passed via `--script-opts-append`. + Keys are prefixed with the sanitized script name. + ''; + }; + path = lib.mkOption { + type = lib.types.nullOr wlib.types.stringable; + default = null; + description = '' + Path to an existing script file. + Takes precedence over `content` if both are set. + + If the value is a derivation with `passthru.scriptName` set, + it will assume this is a package from `pkgs.mpvScripts` and handle it accordingly. + ''; + }; + content = lib.mkOption { + type = lib.types.nullOr lib.types.lines; + default = null; + description = '' + Inline script file content. + Used when `path` is null. + ''; + }; + }; + } + ) + ); + default = { }; description = '' - A list of MPV user scripts to include via package override. + MPV script files and their options. - Each entry should be a derivation providing a Lua script or plugin - compatible with MPV’s `scripts/` directory. - These are appended to MPV’s build with `pkgs.mpv.override`. + Each key is the script name (used for sanitized option prefixes). + The `path` attribute specifies an existing script file (wins over content). + The `content` attribute specifies inline script content. + The `opts` attribute specifies script options passed via `--script-opts-append`. + + Usage example: + ```nix + script = { + modernz = { + path = pkgs.mpvScripts.modernz; + opts = { + window_top_bar = false; + }; + }; + "visualizer.lua" = { + path = pkgs.mpvScripts.visualizer + "/share/mpv/scripts/visualizer.lua"; + opts = { + mode = "force"; + }; + }; + "my_script.lua".content = "print('hello world')"; + }; + ``` + This generates: `--script-opts-append=modernz_window_top_bar=false` + ''; + example = lib.literalMD '' + ```nix + script = { + modernz.path = pkgs.mpvScripts.modernz; + modernz.opts = { + window_top_bar = false; + seekbarfg_color = "#FFFFFF"; + }; + }; + ``` ''; }; "mpv.input" = lib.mkOption { - type = wlib.types.file pkgs; - default.path = config.constructFiles.generatedInput.path; - default.content = ""; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedInput.path; + }; + default = { }; description = '' The MPV input configuration file. @@ -32,9 +158,10 @@ ''; }; "mpv.conf" = lib.mkOption { - type = wlib.types.file pkgs; - default.path = config.constructFiles.generatedConfig.path; - default.content = ""; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedConfig.path; + }; + default = { }; description = '' The main MPV configuration file. @@ -43,29 +170,171 @@ It is included by MPV using the `--include` flag. ''; }; + configDir = lib.mkOption { + type = lib.types.either wlib.types.stringable ( + lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + enable = lib.mkEnableOption name // { + default = true; + }; + path = lib.mkOption { + type = lib.types.nullOr wlib.types.stringable; + default = null; + description = '' + Path to an existing config file. + Takes precedence over `content` if both are set. + ''; + }; + content = lib.mkOption { + type = lib.types.nullOr lib.types.lines; + default = null; + description = '' + Inline config file content. + Used when `path` is null. + ''; + }; + }; + } + ) + ) + ); + default = { }; + description = '' + Additional files to be included in the MPV config directory. + + By using this option, mpv will no longer look for script-opts in the default + $XDG_CONFIG_HOME/mpv/script-opts location, and all additional files will have + to be specified in this option. + + Each entry of the attrset is the relative path to the file and their content respectively. + ''; + example = lib.literalMD '' + ```nix + { + "script-opts/modernz.conf".content = ''' + window_top_bar=no + seekbarfg_color=#FFFFFF + '''; + }; + ``` + ''; + apply = + x: + lib.warnIf (x ? "mpv.input" || x ? "mpv.conf") + ''mpv.input is set via `config."mpv.input"`, not `config.configDir."mpv.input"`, and the same is true of `config."mpv.conf"` and `config.configDir."mpv.conf"`!'' + x; + }; }; + config.flagSeparator = "="; config.flags = { - "--input-conf" = config."mpv.input".path; - "--include" = config."mpv.conf".path; - }; - config.constructFiles.generatedConfig = { - relPath = "${config.binName}-config/mpv.conf"; - content = config."mpv.conf".content; - }; - config.constructFiles.generatedInput = { - relPath = "${config.binName}-config/mpv.input"; - content = config."mpv.input".content; - }; - config.overrides = [ + "--input-conf" = { + data = config."mpv.input".path; + sep = "="; + }; + "--include" = lib.mkIf (config.configDir == { } || !builtins.isAttrs config.configDir) { + data = [ config."mpv.conf".path ]; + sep = "="; + }; + "--config-dir" = lib.mkIf (config.configDir != { }) ( + if !builtins.isAttrs config.configDir then + config.configDir + else + dirOf config.constructFiles.generatedConfig.path + ); + } + // ( + let + scriptsData = lib.pipe partitioned.userScripts [ + (lib.filterAttrs (n: v: v.path != null || v.content != null)) + (lib.mapAttrsToList (n: _: config.constructFiles."scripts/${n}".path)) + ]; + scriptOptsData = lib.concatMap ( + v: + let + sanitized = sanitizeScriptName v.name; + in + if v.value.opts == { } then + [ ] + else + builtins.concatLists ( + lib.mapAttrsToList ( + k: v: + if v == null then [ ] else [ "${sanitized}-${k}=${if lib.isStringLike v then v else toString v}" ] + ) v.value.opts + ) + ) (lib.mapAttrsToList lib.nameValuePair config.script); + in { - name = "MPV_SCRIPTS"; - type = "override"; - data = prev: { - scripts = (prev.scripts or [ ]) ++ config.scripts; + "--scripts-append" = + lib.mkIf (scriptsData != [ ] && (config.configDir == { } || !builtins.isAttrs config.configDir)) + { + sep = "="; + ifs = ":"; + data = scriptsData; + }; + "--script-opts-append" = lib.mkIf (scriptOptsData != [ ]) { + sep = "="; + ifs = ","; + data = scriptOptsData; }; } - ]; + ); + + config.constructFiles = + lib.pipe config.configDir [ + (lib.filterAttrs (_: v: v.enable && (v.path != null || v.content != null))) + (builtins.mapAttrs ( + name: v: { + content = if builtins.isString (v.content or null) then v.content else ""; + output = lib.mkOverride 0 config.constructFiles.generatedConfig.output; + relPath = lib.mkOverride 0 "${dirOf config.constructFiles.generatedConfig.relPath}/${name}"; + ${if v.path or null != null then "builder" else null} = + ''mkdir -p "$(dirname "$2")" && ln -s "${v.path}" "$2"''; + } + )) + ] + // lib.pipe partitioned.userScripts [ + (lib.filterAttrs (_: v: v.path != null || v.content != null)) + (lib.mapAttrs' ( + name: v: + lib.nameValuePair "scripts/${name}" { + content = if builtins.isString (v.content or null) then v.content else ""; + output = lib.mkOverride 0 config.constructFiles.generatedConfig.output; + relPath = lib.mkOverride 0 "${dirOf config.constructFiles.generatedConfig.relPath}/scripts/${name}"; + ${if v.path or null != null then "builder" else null} = + ''mkdir -p "$(dirname "$2")" && ln -s "${v.path}" "$2"''; + } + )) + ] + // { + generatedConfig = { + relPath = "${config.binName}-config/mpv.conf"; + content = config."mpv.conf".content; + }; + generatedInput = { + relPath = lib.mkOverride 0 "${dirOf config.constructFiles.generatedConfig.relPath}/input.conf"; + output = lib.mkOverride 0 config.constructFiles.generatedConfig.output; + content = config."mpv.input".content; + }; + }; + + config.passthru.generatedConfig = dirOf config.constructFiles.generatedConfig.outPath; + + config.overrides = + lib.mkIf (config.scripts or [ ] != [ ] || partitioned.nixpkgsScripts or [ ] != [ ]) + [ + { + name = "MPV_SCRIPTS"; + type = "override"; + data = prev: { + scripts = (prev.scripts or [ ]) ++ config.scripts ++ partitioned.nixpkgsScripts; + }; + } + ]; config.package = lib.mkDefault pkgs.mpv; config.meta.maintainers = [ wlib.maintainers.birdee ]; } diff --git a/wrapperModules/n/neovim/default-config.nix b/wrapperModules/n/neovim/default-config.nix index 3c6ed61e..2fe67e30 100644 --- a/wrapperModules/n/neovim/default-config.nix +++ b/wrapperModules/n/neovim/default-config.nix @@ -59,52 +59,54 @@ ''; }; }; - config.suffixVar = + config.prefixVar = let autodeps = config.specCollect ( - acc: v: acc ++ lib.optionals (v.runtimeDeps or false == "suffix") (v.data.runtimeDeps or [ ]) + acc: v: acc ++ lib.optionals (v.runtimeDeps or false == "prefix") (v.data.runtimeDeps or [ ]) ) [ ]; + isPrefixedRuby = + config.hosts.ruby.nvim-host.enable or false && config.hosts.ruby.nvim-host.prefixRuby or false; + isPrefixedNode = + config.hosts.node.nvim-host.enable or false && config.hosts.node.nvim-host.prefixNode or false; in - lib.mkIf - ( - autodeps != [ ] - || config.hosts.ruby.nvim-host.enable or false - || config.hosts.node.nvim-host.enable or false - ) - ( - lib.optional (autodeps != [ ]) { - name = "NIXPKGS_AUTODEPS_SUFFIX"; - data = [ - "PATH" - ":" - "${lib.makeBinPath autodeps}" - ]; - } - ++ lib.optional (config.hosts.ruby.nvim-host.enable or false) { - name = "RUBY_HOST_PATH_ADDITIONS"; - data = [ - "PATH" - ":" - "${config.hosts.ruby.wrapper}/bin" - ]; - } - ++ lib.optional (config.hosts.node.nvim-host.enable or false) { - name = "NODE_HOST_PATH_ADDITIONS"; - data = [ - "PATH" - ":" - "${pkgs.nodejs}/bin" - ]; - } - ); - config.prefixVar = + lib.mkIf (autodeps != [ ] || isPrefixedRuby || isPrefixedNode) ( + lib.optional (autodeps != [ ]) { + name = "NIXPKGS_AUTODEPS_PREFIX"; + data = [ + "PATH" + ":" + "${lib.makeBinPath autodeps}" + ]; + } + ++ lib.optional isPrefixedRuby { + name = "RUBY_HOST_PATH_ADDITIONS"; + data = [ + "PATH" + ":" + "${config.hosts.ruby.wrapper}/bin" + ]; + } + ++ lib.optional isPrefixedNode { + name = "NODE_HOST_PATH_ADDITIONS"; + data = [ + "PATH" + ":" + "${pkgs.nodejs}/bin" + ]; + } + ); + config.suffixVar = let autodeps = config.specCollect ( - acc: v: acc ++ lib.optionals (v.runtimeDeps or false == "prefix") (v.data.runtimeDeps or [ ]) + acc: v: acc ++ lib.optionals (v.runtimeDeps or false == "suffix") (v.data.runtimeDeps or [ ]) ) [ ]; + isSuffixedRuby = + config.hosts.ruby.nvim-host.enable or false && !(config.hosts.ruby.nvim-host.prefixRuby or true); + isSuffixedNode = + config.hosts.node.nvim-host.enable or false && !(config.hosts.node.nvim-host.prefixNode or true); in - lib.mkIf (autodeps != [ ]) [ - { + lib.mkIf (autodeps != [ ] || isSuffixedRuby || isSuffixedNode) ( + lib.optional (autodeps != [ ]) { name = "NIXPKGS_AUTODEPS_PREFIX"; data = [ "PATH" @@ -112,7 +114,23 @@ "${lib.makeBinPath autodeps}" ]; } - ]; + ++ lib.optional isSuffixedRuby { + name = "RUBY_HOST_PATH_ADDITIONS"; + data = [ + "PATH" + ":" + "${config.hosts.ruby.wrapper}/bin" + ]; + } + ++ lib.optional isSuffixedNode { + name = "NODE_HOST_PATH_ADDITIONS"; + data = [ + "PATH" + ":" + "${pkgs.nodejs}/bin" + ]; + } + ); config.specMaps = lib.mkOrder 490 [ { name = "NIXPKGS_PLUGIN_DEPS"; @@ -359,6 +377,11 @@ { imports = [ wlib.modules.default ]; config.package = pkgs.neovim-node-client or pkgs.nodePackages.neovim; + options.nvim-host.prefixNode = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Prepend the pkgs.nodejs binary to neovim's PATH, rather than appending it"; + }; # NOTE: nvim runs the thing with `node vim.g.node_host_prog`, we can't wrap it # maybe we could replace the shebang with a wrapped node at some point? # You can wrap it for when it gets linked into ${placeholder "out"}/bin though @@ -384,6 +407,11 @@ default = "${pkgs.path}/pkgs/applications/editors/neovim/ruby_provider"; description = "The path to the ruby gem directory with the neovim gem as required by `pkgs.bundlerEnv`"; }; + options.nvim-host.prefixRuby = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Prepend the wrapped ruby binary to neovim's PATH, rather than appending it"; + }; config.exePath = "bin/neovim-ruby-host"; config.binName = "neovim-ruby-host"; }; diff --git a/wrapperModules/n/neovim/normalize.nix b/wrapperModules/n/neovim/normalize.nix index 29aa2ffd..88360ff5 100644 --- a/wrapperModules/n/neovim/normalize.nix +++ b/wrapperModules/n/neovim/normalize.nix @@ -232,17 +232,29 @@ in (builtins.concatStringsSep "\n") ]; - buildPackDir = lib.pipe allPlugins [ - (map ( - v: - "ln -s ${lib.escapeShellArg v.data} ${lib.escapeShellArg "${if v.lazy then opt_dir else start_dir}/${v.name}"}" - )) - (builtins.concatStringsSep "\n") - ]; + buildPackDir = lib.pipe allPlugins ( + let + maptocmd = + dir: v: + v + // { + value = "ln -s ${lib.escapeShellArg v.data} ${lib.escapeShellArg "${dir}/${v.name}"}"; + }; + in + [ + (lib.partition (p: p.lazy)) + ( + { right, wrong }: + builtins.attrValues (builtins.listToAttrs (map (maptocmd start_dir) wrong)) + ++ builtins.attrValues (builtins.listToAttrs (map (maptocmd opt_dir) right)) + ) + (builtins.concatStringsSep "\n") + ] + ); plugins = lib.pipe allPlugins ( let - foldplugins = (builtins.foldl' (acc: v: acc // { ${v.name} = v.data; }) { }); + foldplugins = ps: builtins.listToAttrs (map (v: lib.nameValuePair v.name v.data) ps); in [ (lib.partition (p: p.lazy)) diff --git a/wrapperModules/n/neovim/pre_desc.md b/wrapperModules/n/neovim/pre_desc.md index ff0a8946..dbd0adfe 100644 --- a/wrapperModules/n/neovim/pre_desc.md +++ b/wrapperModules/n/neovim/pre_desc.md @@ -1,6 +1,6 @@ Please see the template for an introductory example usage! -To initialize it, run `nix flake init -t github:BirdeeHub/nix-wrapper-modules#neovim` +To initialize it, run [`nix flake init -t github:BirdeeHub/nix-wrapper-modules#neovim`](https://github.com/BirdeeHub/nix-wrapper-modules/tree/main/templates/neovim) If you are using `zsh`, you may need to escape the `#` character with a backslash. @@ -33,6 +33,31 @@ This module fully supports remote plugin hosts. By the same mechanism, it also allows arbitrary other items to be bundled into the context of your `neovim` derivation, such as `neovide`, via an option which accepts wrapper modules for maximum flexibility. +A basic usage of this module might look something like this: + +```nix +{ wlib, config, pkgs, lib, ... }: + imports = [ wlib.wrapperModules.neovim ]; + specs.general = with pkgs.vimPlugins; [ + # plugins which are loaded at startup ... + ]; + specs.lazy = { + lazy = true; + data = with pkgs.vimPlugins; [ + # plugins which are not loaded until you vim.cmd.packadd them ... + ]; + }; + info = { + values = "for lua"; + which = "will be placed in the generated info plugin for access"; + }; + extraPackages = with pkgs; [ + # lsps, formatters, etc... + ]; + settings.config_directory = ./.; # or lib.generators.mkLuaInline "vim.fn.stdpath('config')"; +} +``` + Please also check out the [Tips and Tricks](#tips-and-tricks) section for more information! ## Options: diff --git a/wrapperModules/n/neovim/symlinkScript.nix b/wrapperModules/n/neovim/symlinkScript.nix index 1db55cbc..c63128b2 100644 --- a/wrapperModules/n/neovim/symlinkScript.nix +++ b/wrapperModules/n/neovim/symlinkScript.nix @@ -46,6 +46,35 @@ let lib.optionalString (config.settings.compile_generated_lua or false != "debug") ", true" }))' " }"; + patchDesktop = input: output: '' + if [ -e "${package}/share/applications/${input}.desktop" ]; then + mkdir -p '${placeholder outputName}/share/applications' + rm -f '${placeholder outputName}/share/applications/${input}.desktop' + substitute ${ + lib.escapeShellArgs [ + "${package}/share/applications/${input}.desktop" + "${placeholder outputName}/share/applications/${output}.desktop" + "--replace-fail" + "Name=Neovim" + "Name=${binName}" + "--replace-fail" + "TryExec=nvim" + "TryExec=${wrapperPaths.placeholder}" + "--replace-fail" + "Icon=nvim" + "Icon=${package}/share/icons/hicolor/128x128/apps/nvim.png" + ] + } + sed ${ + lib.escapeShellArgs [ + '' + /^Exec=nvim/c\ + Exec=${wrapperPaths.placeholder} %F'' + "${placeholder outputName}/share/applications/${output}.desktop" + ] + } > ./tmp_desk && mv -f ./tmp_desk "${placeholder outputName}/share/applications/${output}.desktop" + fi + ''; in finalDrv // { @@ -82,32 +111,6 @@ finalDrv cp -r ${package}/nix-support/* ${placeholder outputName}/nix-support '' - + lib.optionalString stdenv.isLinux '' - mkdir -p '${placeholder outputName}/share/applications' - substitute ${ - lib.escapeShellArgs [ - "${package}/share/applications/nvim.desktop" - "${placeholder outputName}/share/applications/${binName}.desktop" - "--replace-fail" - "Name=Neovim" - "Name=${binName}" - "--replace-fail" - "TryExec=nvim" - "TryExec=${wrapperPaths.placeholder}" - "--replace-fail" - "Icon=nvim" - "Icon=${package}/share/icons/hicolor/128x128/apps/nvim.png" - ] - } - sed ${ - lib.escapeShellArgs [ - '' - /^Exec=nvim/c\ - Exec=${wrapperPaths.placeholder} %F'' - "${placeholder outputName}/share/applications/${binName}.desktop" - ] - } > ./tmp_desk && mv -f ./tmp_desk "${placeholder outputName}/share/applications/${binName}.desktop" - '' + '' # Create symlinks for aliases @@ -164,5 +167,8 @@ finalDrv "" ) outputs} - ''; + '' + + lib.optionalString stdenv.isLinux ( + patchDesktop "nvim" binName + patchDesktop "org.neovim.nvim" "org.neovim.${binName}" + ); } diff --git a/wrapperModules/n/niri/check.nix b/wrapperModules/n/niri/check.nix index d1370a9e..4b509744 100644 --- a/wrapperModules/n/niri/check.nix +++ b/wrapperModules/n/niri/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let @@ -10,7 +11,7 @@ let settings = { binds = { "Mod+T".spawn-sh = "alacritty"; - "Mod+J".focus-column-or-monitor-left = null; + "Mod+J".focus-column-or-monitor-left = _: { }; "Mod+N".spawn = [ "alacritty" "msg" @@ -50,7 +51,7 @@ let ]; layout = { - focus-ring.off = null; + focus-ring.off = _: { }; border = { width = 3; active-color = "#f5c2e7"; @@ -69,20 +70,20 @@ let "foo" = { open-on-output = "DP-3"; }; - "bar" = null; + "bar" = _: { }; }; outputs = { "DP-3" = { - position = { - _attrs = { + position = _: { + props = { x = 1440; y = 1080; }; }; background-color = "#003300"; hot-corners = { - off = null; + off = _: { }; }; }; }; @@ -94,6 +95,12 @@ let "build" ] ]; + + spawn-sh-at-startup = [ + "sleep 1 && echo 'hello world'" + "kitty" + ]; + hotkey-overlay.skip-at-startup = [ ]; prefer-no-csd = true; overview.zoom = 0.25; diff --git a/wrapperModules/n/niri/module.nix b/wrapperModules/n/niri/module.nix index 2edb28d5..91dc131f 100644 --- a/wrapperModules/n/niri/module.nix +++ b/wrapperModules/n/niri/module.nix @@ -6,117 +6,141 @@ ... }: let - # implements kdl with niri semantic knowledge to convert the data-format - - # allow modifiers to be set for blocks - leftpad = v: lib.strings.concatMapStrings (v: " ${v}\n") (lib.strings.splitString "\n" v); - # attrs must be quoted - mkAttrs = - attrs: - if lib.isAttrs attrs then - lib.concatMapAttrsStringSep " " (n: v: if isNull v then ''"${n}"'' else ''"${n}"=${toVal v}'') attrs - else - ""; - mkBlock = - n: v: + # deprecates `{ _attrs = ??; ... }` and `null` to `_: { props = ??; content = ...; }` and `_: {}` respectively + # remove on June 1, 2026 + convertAndWarn = let - attrs = mkAttrs (n._attrs or null); + endOfWarningMessage = "\n" + '' + Warning will be removed on June 1, 2026, + but you can remove this deprecation layer ahead of time + after fixing your configuration by setting `v2-settings = true`. + ''; + recurse = + path: v: + if builtins.isAttrs v then + let + hasAttrs = v ? _attrs; + rest = lib.removeAttrs v [ "_attrs" ]; + processedRest = lib.mapAttrs (n: val: recurse (path ++ [ n ]) val) rest; + in + if hasAttrs then + lib.warn + ( + "wrapperModules.niri: Deprecated `{ _attrs = ??; ... }` at ${lib.concatStringsSep "." path}. Use `_: { props = ??; content = ...; }` instead." + + endOfWarningMessage + ) + (_: { + props = recurse (path ++ [ "_attrs" ]) v._attrs; + content = processedRest; + }) + else + processedRest + else if builtins.isList v && builtins.all builtins.isAttrs v then + map (i: recurse (path ++ [ "[${toString i}]" ]) i) v + else if v == null then + lib.warn ( + "wrapperModules.niri: Deprecated `null` at ${lib.concatStringsSep "." path}. Use `_: {}` instead." + + endOfWarningMessage + ) (_: { }) + else + v; in - if v != "" then - '' - ${n.name or n} ${attrs} { - ${leftpad v} - }'' - else - "${n.name or n} ${attrs}"; - # surround strings with qoutes - toVal = - v: - if lib.isString v then - ''"${v}"'' - else if lib.isBool v then - (if v then "true" else "false") - else - toString v; - mkKeyVal = - k: v: "${k} ${if lib.isList v then lib.strings.concatStringsSep " " (map toVal v) else toVal v}"; - attrsToKdl = - a: - lib.concatMapAttrsStringSep "\n" ( - n: v: - # turn null values into flags - if isNull v then - n - else if lib.isAttrs v then - mkBlock { - name = n; - _attrs = v._attrs or null; - } (attrsToKdl (lib.removeAttrs v [ "_attrs" ])) - else if lib.isList v && lib.all lib.isAttrs v then - mkBlock n (lib.concatMapStringsSep "\n" attrsToKdl v) - else - mkKeyVal n v - ) a; + if config.v2-settings then v: v else v: recurse [ ] v; + mkRule = - block: r: + # "window-rules" "layer-rules" + node: r: let - matches = map (m: "match ${mkAttrs m}") (r.matches or [ ]); - excludes = map (m: "exclude ${mkAttrs m}") (r.excludes or [ ]); - misc = attrsToKdl ( + matches = map (m: { match = _: { props = m; }; }) (r.matches or [ ]); + excludes = map (m: { exclude = _: { props = m; }; }) (r.excludes or [ ]); + other = lib.mapAttrsToList (n: v: { ${n} = v; }) ( lib.attrsets.removeAttrs r [ "matches" "excludes" ] ); in - mkBlock block ( - lib.strings.concatLines ( - lib.lists.flatten [ - matches - excludes - misc - ] - ) - ); - mkWorkspaces = - w: - map attrsToKdl ( - lib.mapAttrsToList (n: v: { - # use the attr name as attribute for the workspace node - workspace = - if isNull v then - n - else + { + ${node} = matches ++ excludes ++ other; + }; + attrAsArg = + # "workspace" "output" + node: + lib.mapAttrsToList ( + n: v: { + # use the attr name as arg for the named node + ${node} = + s: + if lib.isFunction v then + let + res = v s; + in + res + // { + props = + if res ? props then + if builtins.isAttrs res.props then + [ + n + res.props + ] + else if builtins.isList res.props then + [ n ] ++ res.props + else + n + else + n; + } + else if builtins.isAttrs v then { - _attrs = { - ${n} = null; - }; + content = v; + props = n; } - // v; - }) w - ); - mkOutputs = - w: - map attrsToKdl ( - lib.mapAttrsToList (n: v: { - # use the attr name as attribute for the workspace node - output = { - _attrs = { - ${n} = null; - }; - } - // v; - }) w + else + { props = n; }; + } ); in { imports = [ wlib.modules.default ]; options = { + v2-settings = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + If you have converted your configuration from the old version of the niri module's kdl translation to the new one already, + you may set this to true to stop it from checking for the old version. + + On July 1, 2026, when that version is removed, this option will warn that it no longer has any effect. + + The change was as follows: + + For doing + + ```kdl + node "some" "args" "y"=100 { + } + ``` + + `{ _attrs = ??; ... }` was changed to `_: { props = ??; content = ...; }` + + And in the context of declaring a node which is just a name with no children or props: + + `null` was changed to `_: {}` because `null` is an actual value you may want to provide. + + Functions were the only type of value we could not translate. + + The argument to the function is provided by calling the function with `lib.fix`. + ''; + }; settings = lib.mkOption { description = '' Niri configuration settings. See + + This is a freeform submodule. If you do not see your option listed, + try setting it using the format specified by `wlib.toKdl` ''; default = { }; type = lib.types.submodule { @@ -126,39 +150,43 @@ in default = { }; type = lib.types.attrs; description = "Bindings of niri"; - example = { - "Mod+T".spawn-sh = "alacritty"; - "Mod+J".focus-column-or-monitor-left = null; - "Mod+N".spawn = [ - "alacritty" - "msg" - "create-windown" - ]; - "Mod+0".focus-workspace = 0; - "Mod+Escape" = { - _attrs = { - allow-inhibiting = false; + apply = convertAndWarn; + example = lib.literalMD '' + ```nix + binds = { + "Mod+T".spawn-sh = "alacritty"; + "Mod+J".focus-column-or-monitor-left = _: { }; + "Mod+N".spawn = [ "alacritty" "msg" "create-windown" ]; + "Mod+0".focus-workspace = 0; + "Mod+Escape" = _: { + props.allow-inhibiting = false; + content.toggle-keyboard-shortcuts-inhibit = _: { }; # <- _: { } is translated to nothing }; - toggle-keyboard-shortcuts-inhibit = null; }; - }; + ``` + ''; }; layout = lib.mkOption { default = { }; type = lib.types.attrs; description = "Layout definitions"; - example = { - focus-ring.off = null; - border = { - width = 3; - active-color = "#f5c2e7"; - inactive-color = "#313244"; + apply = convertAndWarn; + example = lib.literalMD '' + ```nix + layout = { + focus-ring.off = _: { }; + border = { + width = 3; + active-color = "#f5c2e7"; + inactive-color = "#313244"; + }; + preset-column-widths = [ + { proportion = 0.5; } + { proportion = 0.666667; } + ]; }; - preset-column-widths = [ - { proportion = 0.5; } - { proportion = 0.666667; } - ]; - }; + ``` + ''; }; spawn-at-startup = lib.mkOption { default = [ ]; @@ -175,10 +203,22 @@ in ] ]; }; + spawn-sh-at-startup = lib.mkOption { + default = [ ]; + type = lib.types.listOf lib.types.str; + description = '' + List of sh commands as strings to run at startup. + ''; + example = [ + "sleep 1 && echo 'hello world'" + "kitty" + ]; + }; window-rules = lib.mkOption { default = [ ]; type = lib.types.listOf lib.types.attrs; description = "List of window rules"; + apply = convertAndWarn; example = [ { matches = [ { app-id = ".*"; } ]; @@ -194,6 +234,7 @@ in default = [ ]; type = lib.types.listOf lib.types.attrs; description = "List of layer rules"; + apply = convertAndWarn; example = [ { matches = [ { namespace = "^notifications$"; } ]; @@ -206,60 +247,120 @@ in default = { }; type = lib.types.attrsOf (lib.types.nullOr lib.types.anything); description = "Named workspace definitions"; - example = { - "foo" = { - open-on-output = "DP-3"; + apply = convertAndWarn; + example = lib.literalMD '' + ```nix + workspaces = { + "foo" = { + open-on-output = "DP-3"; + }; + "bar" = _: { }; }; - "bar" = null; - }; + ``` + ''; }; outputs = lib.mkOption { default = { }; type = lib.types.attrs; description = "Output configuration"; - example = { - "DP-3" = { - background-color = "#003300"; - hot-corners = { - off = null; + apply = convertAndWarn; + example = lib.literalMD '' + ```nix + { + "DP-3" = { + position = _: { + props = { + x = 1440; + y = 1080; + }; + }; + background-color = "#003300"; + hot-corners = { + off = _: { }; + }; }; - }; - }; + } + ``` + ''; }; extraConfig = lib.mkOption { default = ""; - type = lib.types.str; + type = lib.types.lines; description = '' Escape hatch string option added to the config file for - options that might not be representable otherwise + options that might not be representable otherwise, + due to `config.settings` in this module being required to be an attribute set. ''; }; }; }; }; + extraSettings = lib.mkOption { + type = lib.types.listOf (lib.types.attrsOf wlib.types.attrsRecursive); + default = [ ]; + description = '' + Allows for auto translated kdl values for options not included in `config.settings`, + but for which repeated definitions are significant. + + Syntax for this option is the list form for the `wlib.toKdl` function + ''; + example = lib.literalMD '' + ```nix + config.extraSettings = [ + { include = ./some/pure/path; } + # If `include optional=true "~/some/impure/path"` is not valid in your version of niri, you may want to use their flake! + { include = [ { optional = true; } "~/some/impure/path" ]; } + ]; + ``` + ''; + }; "config.kdl" = lib.mkOption { - type = wlib.types.file pkgs; - default.path = config.constructFiles.generatedConfig.path; - default.content = ""; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedConfig.path; + }; + default = { }; description = '' Configuration file for Niri. See + + If `config."config.kdl".content` is non-empty, its content will be used instead of the generated + config from `config.settings` in the generated config file in the derivation. + + You may also set `config."config.kdl".path` to your own path. + + This will still allow the generated config to be created from `config.settings` + + You could use the include feature to include it. ''; - example = '' - input { - keyboard { - numlock + example = lib.literalMD '' + ```nix + # Overwrite the generated config + config."config.kdl".content = /* kdl */ ''' + input { + keyboard { + numlock + } + touchpad { + tap + natural-scroll + } + focus-follows-mouse "max-scroll-amount"="0%" { + } } + '''; + ``` + ''; + }; + disableConfigValidation = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + When `true`, the wrapper will not run `niri validate` on the nix-provided config file. - touchpad { - tap - natural-scroll - } + This is useful for debugging the output of the generated config file. - focus-follows-mouse = { - _attrs = { max-scroll-amount = "0%"; }; - }; - } + It also allows you to pass an impure path via `config."config.kdl".path`, + as nix no longer needs to know about this path at build time. ''; }; }; @@ -267,12 +368,13 @@ in "share/applications/*.desktop" "share/systemd/user/niri.service" ]; - config.drv.installPhase = '' + # NOTE: gives users a nice error message about invalid configs, with actual knowledge of niri's config format + config.drv.installPhase = lib.mkIf (!config.disableConfigValidation) '' runHook preInstall ${lib.getExe config.package} validate -c ${config.constructFiles.generatedConfig.path} runHook postInstall ''; - config.package = pkgs.niri; + config.package = lib.mkDefault pkgs.niri; config.env.NIRI_CONFIG = config."config.kdl".path; config.constructFiles.generatedConfig = { relPath = "${config.binName}-config.kdl"; @@ -280,35 +382,33 @@ in if config."config.kdl".content or "" != "" then config."config.kdl".content else - lib.strings.concatLines ( - lib.lists.flatten [ + wlib.toKdl (_: { + version = 1; + content = builtins.concatLists [ (map (mkRule "window-rule") config.settings.window-rules) (map (mkRule "layer-rule") config.settings.layer-rules) - (map ( - v: - (lib.strings.concatStringsSep " " ( - lib.lists.flatten [ + (map (v: { spawn-at-startup = _: { props = v; }; }) config.settings.spawn-at-startup) + (map (v: { spawn-sh-at-startup = _: { props = v; }; }) config.settings.spawn-sh-at-startup) + (attrAsArg "workspace" config.settings.workspaces) + (attrAsArg "output" config.settings.outputs) + [ + (convertAndWarn ( + lib.removeAttrs config.settings [ + "window-rules" + "layer-rules" "spawn-at-startup" - (map (v: ''"${v}"'') (lib.flatten [ v ])) + "spawn-sh-at-startup" + "workspaces" + "outputs" + "extraConfig" ] )) - + "\n" - ) config.settings.spawn-at-startup) - (mkWorkspaces config.settings.workspaces) - (mkOutputs config.settings.outputs) - (attrsToKdl ( - lib.removeAttrs config.settings [ - "window-rules" - "layer-rules" - "spawn-at-startup" - "workspaces" - "outputs" - "extraConfig" - ] - )) - config.settings.extraConfig - ] - ); + ] + config.extraSettings + ]; + }) + + "\n" + + config.settings.extraConfig; }; config.meta.maintainers = [ wlib.maintainers.patwid diff --git a/wrapperModules/n/noctalia-shell/module.nix b/wrapperModules/n/noctalia-shell/module.nix index 4be09924..d5568a21 100644 --- a/wrapperModules/n/noctalia-shell/module.nix +++ b/wrapperModules/n/noctalia-shell/module.nix @@ -101,7 +101,7 @@ in ''; }; settings = lib.mkOption { - type = lib.types.json; + type = lib.types.json or (pkgs.formats.json { }).type; default = { }; example = lib.literalExpression '' { @@ -127,7 +127,7 @@ in }; colors = lib.mkOption { - type = lib.types.json; + type = lib.types.json or (pkgs.formats.json { }).type; default = { }; example = lib.literalExpression '' { @@ -175,7 +175,7 @@ in }; plugins = lib.mkOption { - type = lib.types.json; + type = lib.types.json or (pkgs.formats.json { }).type; default = { }; example = lib.literalExpression '' { @@ -202,7 +202,7 @@ in }; pluginSettings = lib.mkOption { - type = with lib.types; attrsOf json; + type = lib.types.attrsOf (lib.types.json or (pkgs.formats.json { }).type); default = { }; example = lib.literalExpression '' { @@ -275,7 +275,7 @@ in ''; }; settings = lib.mkOption { - type = lib.types.json; + type = lib.types.json or (pkgs.formats.json { }).type; default = { }; description = '' Settings to add to `$NOCTALIA_CONFIG_DIR/plugins/plugin-name/settings.json` @@ -383,16 +383,14 @@ in (lib.filterAttrs (_: plugin: plugin.enabled && plugin.settings != { })) (lib.mapAttrs (_: plugin: plugin.settings)) (v: lib.recursiveUpdate v config.pluginSettings) - (wlib.mapAttrsToList0 ( - i: name: value: - lib.nameValuePair name { - key = "plugin_${toString i}"; + (builtins.mapAttrs ( + name: value: { + key = "plugin_${name}"; relPath = lib.mkOverride 0 "${config.generatedConfigDirname}/plugins/${name}/settings.json"; output = lib.mkOverride 0 config.configDrvOutput; content = builtins.toJSON value; builder = ''mkdir -p "$(dirname "$2")" && cp -f "$1" "$2"''; } )) - builtins.listToAttrs ]; } diff --git a/wrapperModules/n/notmuch/check.nix b/wrapperModules/n/notmuch/check.nix index 467fe962..5d53e138 100644 --- a/wrapperModules/n/notmuch/check.nix +++ b/wrapperModules/n/notmuch/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/n/notmuch/module.nix b/wrapperModules/n/notmuch/module.nix index 18df2bea..b6af6a70 100644 --- a/wrapperModules/n/notmuch/module.nix +++ b/wrapperModules/n/notmuch/module.nix @@ -34,9 +34,10 @@ in ''; }; configFile = lib.mkOption { - type = wlib.types.file pkgs; - default.path = config.constructFiles.generatedConfig.path; - default.content = ""; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedConfig.path; + }; + default = { }; description = '' Path or inline definition of the generated Notmuch configuration file. diff --git a/wrapperModules/n/nushell/check.nix b/wrapperModules/n/nushell/check.nix index 70be36f9..fcea4536 100644 --- a/wrapperModules/n/nushell/check.nix +++ b/wrapperModules/n/nushell/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/n/nushell/module.nix b/wrapperModules/n/nushell/module.nix index 4bb490f8..1a22597e 100644 --- a/wrapperModules/n/nushell/module.nix +++ b/wrapperModules/n/nushell/module.nix @@ -9,9 +9,10 @@ imports = [ wlib.modules.default ]; options = { "env.nu" = lib.mkOption { - type = wlib.types.file pkgs; - default.content = ""; - default.path = config.constructFiles.generatedEnv.path; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedEnv.path; + }; + default = { }; description = '' The Nushell environment configuration file. @@ -21,9 +22,10 @@ ''; }; "config.nu" = lib.mkOption { - type = wlib.types.file pkgs; - default.content = ""; - default.path = config.constructFiles.generatedConfig.path; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedConfig.path; + }; + default = { }; description = '' The main Nushell configuration file. diff --git a/wrapperModules/r/rofi/check.nix b/wrapperModules/r/rofi/check.nix index c67b9aab..6215e4b2 100644 --- a/wrapperModules/r/rofi/check.nix +++ b/wrapperModules/r/rofi/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/r/rofi/module.nix b/wrapperModules/r/rofi/module.nix index 6b472a24..5ed45ecf 100644 --- a/wrapperModules/r/rofi/module.nix +++ b/wrapperModules/r/rofi/module.nix @@ -103,16 +103,18 @@ in imports = [ wlib.modules.default ]; options = { "config.rasi" = lib.mkOption { - type = wlib.types.file pkgs; - default.path = config.constructFiles.generatedConfig.path; - default.content = - toRasi { - configuration = config.settings; - } - + (lib.optionalString (config.theme != null) (toRasi { - "@theme" = - if builtins.isAttrs config.theme then config.constructFiles.generatedTheme.path else config.theme; - })); + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles.generatedConfig.path; + content = + toRasi { + configuration = config.settings; + } + + (lib.optionalString (config.theme != null) (toRasi { + "@theme" = + if builtins.isAttrs config.theme then config.constructFiles.generatedTheme.path else config.theme; + })); + }; + default = { }; description = '' The main Rofi configuration file (`config.rasi`). diff --git a/wrapperModules/t/tmux/module.nix b/wrapperModules/t/tmux/module.nix index 5a52290f..c964e437 100644 --- a/wrapperModules/t/tmux/module.nix +++ b/wrapperModules/t/tmux/module.nix @@ -21,31 +21,14 @@ let configPlugins = plugins: - ( - let - pluginName = p: if lib.types.package.check p then p.pname else p.plugin.pname; - pluginRTP = p: if lib.types.package.check p then p.rtp else p.plugin.rtp; - pluginConfigPre = p: if lib.types.package.check p then "" else p.configBefore or ""; - pluginConfigPost = p: if lib.types.package.check p then "" else p.configAfter or ""; - in - if plugins == [ ] || !(builtins.isList plugins) then - "" - else - '' - # ============================================== # - ${ - (lib.concatMapStringsSep "\n\n" (p: '' - # ${pluginName p} - # --------------------- - ${pluginConfigPre p} - run-shell ${pluginRTP p} - ${pluginConfigPost p} - # --------------------- - '') plugins) - } - # ============================================== # - '' - ); + lib.concatMapStringsSep "\n\n" (p: '' + # ${toString p.name} + # --------------------- + ${p.configBefore} + run-shell ${p.rtp} + ${p.configAfter} + # --------------------- + '') (wlib.dag.unwrapSort "tmux plugins" plugins); tmux_bool_conv = v: if v then "on" else "off"; in { @@ -83,33 +66,95 @@ in default = [ ]; description = "List of tmux plugins to source."; type = lib.types.listOf ( - lib.types.oneOf [ - lib.types.package - (lib.types.submodule { - options = { - plugin = lib.mkOption { - type = lib.types.package; - description = '' - the tmux plugin to source - ''; - }; - configBefore = lib.mkOption { - type = lib.types.lines; - default = ""; - description = '' - configuration to run before the plugin is sourced - ''; - }; - configAfter = lib.mkOption { - type = lib.types.lines; - default = ""; - description = '' - configuration to run after the plugin is sourced - ''; - }; - }; - }) - ] + wlib.types.specWith { + dontConvertFunctions = true; + modules = [ + ( + { config, ... }: + { + # NOTE: set here because if you put them in the actual default field, + # nixpkgs doc generator will try to show them. + # Ours actually won't, for our doc generator putting them in the normal place would be fine. + config.name = lib.mkOptionDefault (config.plugin.pname or null); + config.rtp = lib.mkOptionDefault ( + if config.relPath != null then + "${config.plugin}/${config.relPath}" + else + config.plugin.rtp + or "${config.plugin}${lib.optionalString (config.name != null) "/${config.name}.tmux"}" + ); + options = { + plugin = lib.mkOption { + type = wlib.types.stringable; + description = '' + the tmux plugin to source + + Used to determine `plugins.*.rtp` field + ''; + }; + relPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + If this is not `null`, then `plugins.*.rtp` is set to `"''${plugin}/''${relPath}"` by default + ''; + }; + rtp = lib.mkOption { + type = wlib.types.stringable; + description = '' + The path actually sourced via `run-shell` within the plugin provided to the plugin field. + + If `relPath` is provided, this is set to `"''${plugin}/''${relPath}"` + + If the plugin has an `rtp` attribute, as the plugins from `pkgs.tmuxPlugins` do, + then that is used as the default if `relPath` is not provided. + + If it does not, `"''${plugin}/''${name}.tmux"` is used. + + If it does not have a name either, then the provided `plugin` path is used directly. + ''; + }; + name = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + Name of the plugin, can be targeted by the before and after fields of other plugin specs + + Defaults to `plugin.pname` + ''; + }; + before = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Plugins to source this plugin before + ''; + }; + after = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Plugins to source this plugin after + ''; + }; + configBefore = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + configuration to run before the plugin is sourced + ''; + }; + configAfter = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + configuration to run after the plugin is sourced + ''; + }; + }; + } + ) + ]; + } ); }; prefix = lib.mkOption { @@ -279,10 +324,16 @@ in bind-key -N "Kill the current pane" x kill-pane ''} + # ============================================== # + ${config.configBefore} + # ============================================== # + ${configPlugins config.plugins} + # ============================================== # + ${config.configAfter} '' }"; @@ -290,7 +341,7 @@ in runShell = lib.mkIf config.secureSocket [ ''export TMUX_TMPDIR=''${TMUX_TMPDIR:-''${XDG_RUNTIME_DIR:-"/run/user/$(id -u)"}}'' ]; - package = pkgs.tmux; + package = lib.mkDefault pkgs.tmux; meta.maintainers = [ wlib.maintainers.birdee ]; }; } diff --git a/wrapperModules/t/tofi/module.nix b/wrapperModules/t/tofi/module.nix new file mode 100644 index 00000000..a1a986ea --- /dev/null +++ b/wrapperModules/t/tofi/module.nix @@ -0,0 +1,70 @@ +{ + wlib, + lib, + pkgs, + config, + ... +}: +{ + imports = [ wlib.modules.default ]; + options = { + settings = lib.mkOption { + type = + with lib.types; + attrsOf (oneOf [ + bool + int + float + wlib.types.stringable + ]); + default = { }; + description = '' + Settings for {command}`tofi`. + See {manpage}`tofi(5)` for available options. + ''; + example = { + width = "100%"; + height = "100%"; + num-results = 5; + border-width = 0; + outline-width = 0; + padding-left = "35%"; + padding-top = "35%"; + result-spacing = 25; + background-color = "#000A"; + }; + }; + }; + config = { + package = lib.mkDefault pkgs.tofi; + + constructFiles.generatedConfig = { + relPath = "${config.binName}-config"; + content = + let + valueToString = + v: + if builtins.isBool v then + lib.boolToString v + else if builtins.isPath v then + "${v}" + else + toString v; + keyValueToLine = k: v: "${k} = ${valueToString v}\n"; + settingsWithoutInclude = builtins.removeAttrs config.settings [ "include" ]; + lines = lib.mapAttrsToList keyValueToLine settingsWithoutInclude; + # If set the "include" option should be last so that included options are not overwritten + includeLine = lib.optional (config.settings ? include) ( + keyValueToLine "include" config.settings.include + ); + in + lib.concatStrings (lines ++ includeLine); + }; + + flags = { + "--config" = config.constructFiles.generatedConfig.path; + }; + + meta.maintainers = [ wlib.maintainers.nikitawootten ]; + }; +} diff --git a/wrapperModules/v/vim/check.nix b/wrapperModules/v/vim/check.nix index d21c6ee1..fd0f3c40 100644 --- a/wrapperModules/v/vim/check.nix +++ b/wrapperModules/v/vim/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/w/waybar/check.nix b/wrapperModules/w/waybar/check.nix index 313fc8d1..a264032c 100644 --- a/wrapperModules/w/waybar/check.nix +++ b/wrapperModules/w/waybar/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let waybarWrapped = self.wrappers.waybar.wrap { diff --git a/wrapperModules/w/wezterm/check.nix b/wrapperModules/w/wezterm/check.nix index 4cff4292..50d25d5c 100644 --- a/wrapperModules/w/wezterm/check.nix +++ b/wrapperModules/w/wezterm/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let weztermWrapped = self.wrappers.wezterm.wrap ( diff --git a/wrapperModules/w/wezterm/module.nix b/wrapperModules/w/wezterm/module.nix index a2c14470..df782396 100644 --- a/wrapperModules/w/wezterm/module.nix +++ b/wrapperModules/w/wezterm/module.nix @@ -26,8 +26,11 @@ ''; }; options."wezterm.lua" = lib.mkOption { - type = wlib.types.file pkgs; - default.content = "return require('nix-info')"; + type = wlib.types.file { + path = lib.mkOptionDefault config.constructFiles."wezterm.lua".path; + content = lib.mkOptionDefault "return require('nix-info')"; + }; + default = { }; description = "The wezterm config file. provide `.content`, or `.path`"; }; options.luaInfo = lib.mkOption { @@ -49,6 +52,10 @@ This will help prevent indexing errors when querying nested values which may not exist. ''; }; + config.constructFiles."wezterm.lua" = { + relPath = "${config.binName}-init.lua"; + content = config."wezterm.lua".content; + }; config.constructFiles.nixLuaInit = { relPath = "${config.binName}-rc.lua"; content = diff --git a/wrapperModules/w/wlr-which-key/module.nix b/wrapperModules/w/wlr-which-key/module.nix new file mode 100644 index 00000000..e1b8241b --- /dev/null +++ b/wrapperModules/w/wlr-which-key/module.nix @@ -0,0 +1,53 @@ +{ + config, + wlib, + lib, + pkgs, + ... +}: +let + yamlFmt = pkgs.formats.yaml { }; +in +{ + imports = [ wlib.modules.default ]; + + options = { + configPath = lib.mkOption { + type = wlib.types.stringable; + default = config.constructFiles.cfg.path; + description = "Path to YAML configuration file."; + }; + + settings = lib.mkOption { + type = yamlFmt.type; + default = { }; + description = '' + Configuration for wlr-which-key. + See + ''; + }; + + initialKeys = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Space separated key sequence to execute after launching menu. + ''; + }; + }; + + config.package = lib.mkDefault pkgs.wlr-which-key; + + config.flags."--initial-keys" = lib.mkIf (config.initialKeys != "") config.initialKeys; + config.addFlag = [ config.configPath ]; + + config.constructFiles.cfg = { + content = lib.generators.toYAML { } config.settings; + relPath = "${config.binName}.yaml"; + }; + + config.meta = { + maintainers = [ wlib.maintainers.nouritsu ]; + platforms = lib.platforms.linux; + }; +} diff --git a/wrapperModules/x/xplr/check.nix b/wrapperModules/x/xplr/check.nix index 712f104e..847a5ba2 100644 --- a/wrapperModules/x/xplr/check.nix +++ b/wrapperModules/x/xplr/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let xplr = self.wrappers.xplr.wrap ( diff --git a/wrapperModules/x/xplr/module.nix b/wrapperModules/x/xplr/module.nix index 303f0105..07dc09f6 100644 --- a/wrapperModules/x/xplr/module.nix +++ b/wrapperModules/x/xplr/module.nix @@ -9,28 +9,13 @@ let luaType = (pkgs.formats.lua { }).type; enabledDagOf = wlib.types.dagOf // { modules = [ - ( - { config, ... }: - { - options.enable = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Enable the value"; - }; - options.disabled = lib.mkOption { - internal = true; - type = lib.types.nullOr lib.types.bool; - default = null; - }; - config.enable = lib.mkIf (config.disabled != null) ( - lib.warn '' - wrapperModules.xplr: - `plugins..disabled` is deprecated. Use `plugins..enable` instead - `luaInit..disabled` is deprecated. Use `luaInit..enable` instead - '' (!config.disabled) - ); - } - ) + { + options.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable the value"; + }; + } ]; }; configDagOf = enabledDagOf // { diff --git a/wrapperModules/y/yazi/check.nix b/wrapperModules/y/yazi/check.nix index 15b7aa73..0cb310cb 100644 --- a/wrapperModules/y/yazi/check.nix +++ b/wrapperModules/y/yazi/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let yaziWrapper = self.wrappers.yazi.wrap { inherit pkgs; }; diff --git a/wrapperModules/y/yazi/module.nix b/wrapperModules/y/yazi/module.nix index 8bb5e4e9..a9ab8a7e 100644 --- a/wrapperModules/y/yazi/module.nix +++ b/wrapperModules/y/yazi/module.nix @@ -447,6 +447,50 @@ in ''; }; + options.plugins = lib.mkOption { + type = lib.types.attrsOf (lib.types.nullOr wlib.types.stringable); + default = { }; + description = '' + An attribute set of plugin names and their paths + ''; + example = lib.literalMD '' + ```nix + with pkgs.yaziPlugins; { + smart-enter = smart-enter; + drag = inputs.drag; + gvfs = inputs.gvfs-yazi; + git = git; + starship = starship; + full-border = full-border; + }; + ``` + ''; + }; + options.flavors = lib.mkOption { + type = lib.types.attrsOf (lib.types.nullOr wlib.types.stringable); + default = { }; + description = '' + An attribute set of flavor names and their paths + ''; + }; + config.buildCommand.makePluginsAndFlavors = { + before = [ "constructFiles" ]; # <- by default constructFiles is the first of the 3 in modules.default + data = + let + toLink = + dir: n: v: + lib.optionalString (v != null) + "ln -s ${lib.escapeShellArg v} ${lib.escapeShellArg "${config.generatedConfig.placeholder}/${dir}/${n}.yazi"}"; + in + '' + mkdir -p ${lib.escapeShellArg "${config.generatedConfig.placeholder}/plugins"} + mkdir -p ${lib.escapeShellArg "${config.generatedConfig.placeholder}/flavors"} + '' + + lib.concatMapAttrsStringSep "\n" (toLink "plugins") config.plugins + + "\n" + + lib.concatMapAttrsStringSep "\n" (toLink "flavors") config.flavors; + }; + config.package = lib.mkDefault pkgs.yazi; config.env.YAZI_CONFIG_HOME = config.generatedConfig.placeholder; # make `finalpackage.generatedConfig` point to the generated config so it can be used from outside of the wrapper module diff --git a/wrapperModules/y/yt-dlp/check.nix b/wrapperModules/y/yt-dlp/check.nix index 769b7fc0..cac06e71 100644 --- a/wrapperModules/y/yt-dlp/check.nix +++ b/wrapperModules/y/yt-dlp/check.nix @@ -1,4 +1,4 @@ -{ pkgs, self }: +{ pkgs, self, ... }: let ytWrapped = self.wrappers.yt-dlp.wrap { inherit pkgs; diff --git a/wrapperModules/z/zathura/module.nix b/wrapperModules/z/zathura/module.nix index a780cb16..20256db8 100644 --- a/wrapperModules/z/zathura/module.nix +++ b/wrapperModules/z/zathura/module.nix @@ -53,6 +53,15 @@ in for the full list of options. ''; }; + extraSettings = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Extra lines appended to zathurarc, e.g. + `"include /home/user/.config/zathura/zathura-colors"` + See {manpage}`zathurarc(5)` for the full list of options. + ''; + }; mappings = lib.mkOption { type = with lib.types; attrsOf str; default = { }; @@ -72,6 +81,7 @@ in zathura_cb zathura_djvu zathura_ps + zathura_pdf_mupdf ]; description = '' Add plugins to zathura runtime. @@ -96,9 +106,12 @@ in wrapperVariants.zathura-sandbox = lib.mkIf pkgs.stdenv.hostPlatform.isLinux { }; constructFiles.renderedRc = { relPath = "config/${config.binName}rc"; - content = lib.concatStringsSep "\n" ( - lib.mapAttrsToList formatLine config.settings ++ lib.mapAttrsToList formatMapLine config.mappings - ); + content = '' + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList formatLine config.settings ++ lib.mapAttrsToList formatMapLine config.mappings + )} + ${config.extraSettings} + ''; }; meta.maintainers = [ wlib.maintainers.rachitvrma ]; }; diff --git a/wrapperModules/z/zsh/check.nix b/wrapperModules/z/zsh/check.nix index a2d2f8b4..80a44b4f 100644 --- a/wrapperModules/z/zsh/check.nix +++ b/wrapperModules/z/zsh/check.nix @@ -1,6 +1,7 @@ { pkgs, self, + ... }: let diff --git a/wrapperModules/z/zsh/module.nix b/wrapperModules/z/zsh/module.nix index a066108a..c4cb8a29 100644 --- a/wrapperModules/z/zsh/module.nix +++ b/wrapperModules/z/zsh/module.nix @@ -4,7 +4,7 @@ wlib, pkgs, ... -}: +}@top: let inherit (lib) types; rcfile = lib.types.submodule { @@ -58,24 +58,13 @@ in You may also wish to set this as your default shell via a nixos module. - To do this, you will need to set the following options in a nixos module: - ```nix - # nixos provides configuration in /etc/ - # It will also throw an error if we set zsh as the default shell without setting this. - programs.zsh.enable = true; - # installs shell completions from environment.systemPackages derivations - environment.pathsToLink = [ "/share/zsh" ]; - # you need to install it. - environment.systemPackages = [ your-zsh-wrapper ]; - ``` - - Once you do that, you can set the shell as your default shell! - - ```nix - # set one or both - users.defaultUserShell = your-zsh-wrapper; - users.users.''${username}.shell = your-zsh-wrapper; + { config, ... }: { + imports = [ (wlib.installModule { name = "zsh"; value = ./yourzshwrappermodule.nix; }) ]; + wrappers.zsh.enable = true; + wrappers.zsh.asSystemDefault = true; + users.users.''${username}.shell = config.wrappers.zsh.wrapper; + } ``` - Note: @@ -220,4 +209,22 @@ in default = { }; type = rcfile; }; + config.install.modules.nixos = + { config, lib, ... }: + let + cfg = top.config.install.getWrapperConfig config; + in + { + config = lib.mkMerge [ + (top.config.install.mkWrapperExtension "${./module.nix} zsh as defaultUserShell" { + _file = ./module.nix; + options.asSystemDefault = lib.mkEnableOption "zsh as defaultUserShell"; + }) + (lib.mkIf cfg.enable { + environment.pathsToLink = [ "/share/zsh" ]; + users.defaultUserShell = lib.mkIf cfg.asSystemDefault cfg.wrapper; + programs.zsh.enable = lib.mkIf cfg.asSystemDefault true; + }) + ]; + }; }