Compare commits
137
Commits
@@ -24,8 +24,8 @@ Also make sure to read the guidelines found at
|
||||
- [ ] Code formatted with `nix fmt` or
|
||||
`nix-shell -A dev --run treefmt`.
|
||||
|
||||
- [ ] Code tested through `nix run .#tests -- test-all` or
|
||||
`nix-shell --pure tests -A run.all`.
|
||||
- [ ] Code tested through `nix build .#test-all`
|
||||
or a targeted `nix run .#tests -- <pattern>`.
|
||||
|
||||
- [ ] Test cases updated/added. See [example](https://github.com/nix-community/home-manager/commit/f3fbb50b68df20da47f9b0def5607857fcc0d021#diff-b61a6d542f9036550ba9c401c80f00ef).
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
- modules/programs/aiac.nix
|
||||
- modules/programs/aichat.nix
|
||||
- modules/programs/aider-chat.nix
|
||||
- modules/programs/claude-code.nix
|
||||
- modules/programs/claude-code/**/*
|
||||
- modules/programs/codex.nix
|
||||
- modules/programs/fabric-ai.nix
|
||||
- modules/programs/antigravity-cli.nix
|
||||
|
||||
@@ -31,10 +31,10 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ vars.CI_APP_ID }}
|
||||
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
- name: Create backport PRs
|
||||
id: backport
|
||||
uses: korthout/backport-action@v4
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixpkgs-unstable
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
app-id: ${{ vars.CI_APP_ID }}
|
||||
private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
parse: ${{ steps.changes.outputs.parse }}
|
||||
tests: ${{ steps.changes.outputs.tests }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: changes
|
||||
with:
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
id: get-nixpkgs
|
||||
run: |
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
echo "email=$id+$name@users.noreply.github.com"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token || github.token }}
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
if: github.repository_owner == 'nix-community'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Get Nixpkgs revision from flake.lock
|
||||
id: get-nixpkgs
|
||||
run: |
|
||||
|
||||
@@ -11,9 +11,13 @@ list *pattern:
|
||||
test *pattern:
|
||||
nix run .#tests -- {{pattern}}
|
||||
|
||||
# List integration tests
|
||||
list_integration_tests:
|
||||
nix run .#tests -- -t -l
|
||||
|
||||
# Run integration tests
|
||||
integration_tests:
|
||||
nix run .#tests -- -t -l
|
||||
nix run .#tests -- -t integration-test-
|
||||
|
||||
# Create a news entry
|
||||
create-news-entry:
|
||||
|
||||
+62
-24
@@ -26,7 +26,7 @@
|
||||
github = "0xdsqr";
|
||||
githubId = 99584622;
|
||||
name = "Dave Dennis";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
"3ulalia" = {
|
||||
email = "3ulalia@proton.me";
|
||||
@@ -39,8 +39,14 @@
|
||||
email = "git.t@betsumei.com";
|
||||
github = "74k1";
|
||||
githubId = 49000471;
|
||||
keys = [
|
||||
{
|
||||
fingerprint = "46F3 422F 63A3 1369 7EAB 83D5 1CF1 55F7 6F21 3503";
|
||||
}
|
||||
];
|
||||
matrix = "@74k1:matrix.org";
|
||||
name = "Tim";
|
||||
source = "home-manager";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
"9p4" = {
|
||||
email = "vcs@ersei.net";
|
||||
@@ -75,14 +81,14 @@
|
||||
github = "AndreasMager";
|
||||
githubId = 5646732;
|
||||
name = "Andreas Mager";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
Austreelis = {
|
||||
email = "github@accounts.austreelis.net";
|
||||
github = "Austreelis";
|
||||
githubId = 56743515;
|
||||
name = "Morgane Austreelis";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
CarlosLoboxyz = {
|
||||
email = "86011416+CarlosLoboxyz@users.noreply.github.com";
|
||||
@@ -139,7 +145,7 @@
|
||||
github = "Fendse";
|
||||
githubId = 46252070;
|
||||
name = "Sara Johnsson";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
Flameopathic = {
|
||||
email = "flameopathic@gmail.com";
|
||||
@@ -202,7 +208,7 @@
|
||||
githubId = 80165193;
|
||||
matrix = "@janik0:matrix.org";
|
||||
name = "Janik";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
JasmineLowen = {
|
||||
email = "robwalter96@gmail.com";
|
||||
@@ -228,7 +234,7 @@
|
||||
github = "Joker9944";
|
||||
githubId = 9194199;
|
||||
name = "Felix von Arx";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
JustinLovinger = {
|
||||
email = "git@justinlovinger.com";
|
||||
@@ -305,7 +311,7 @@
|
||||
github = "MForster";
|
||||
githubId = 4067975;
|
||||
name = "Michael Forster";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
Misterio77 = {
|
||||
email = "eu@misterio.me";
|
||||
@@ -384,12 +390,25 @@
|
||||
name = "Philipp Mildenberger";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
ReStranger = {
|
||||
github = "ReStranger";
|
||||
githubId = 69393944;
|
||||
name = "ReStranger";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
ReeSilva = {
|
||||
email = "reesilva@proton.me";
|
||||
github = "ReeSilva";
|
||||
githubId = 3605032;
|
||||
name = "Renato da Silva";
|
||||
source = "home-manager";
|
||||
};
|
||||
Rosuavio = {
|
||||
email = "RosarioPulella@gmail.com";
|
||||
github = "Rosuavio";
|
||||
githubId = 7164552;
|
||||
name = "Rosario Pulella";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
S0AndS0 = {
|
||||
email = "S0AndS0@digital-mercenaries.com";
|
||||
@@ -578,6 +597,13 @@
|
||||
name = "Alin";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
amadejkastelic = {
|
||||
email = "amadejkastelic7@gmail.com";
|
||||
github = "amadejkastelic";
|
||||
githubId = 26391003;
|
||||
name = "Amadej Kastelic";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
ambroisie = {
|
||||
email = "bruno.nixpkgs@belanyi.fr";
|
||||
github = "ambroisie";
|
||||
@@ -645,7 +671,7 @@
|
||||
github = "b1kku";
|
||||
githubId = 77858854;
|
||||
name = "Bikku";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
bamhm182 = {
|
||||
email = "bamhm182@gmail.com";
|
||||
@@ -855,7 +881,7 @@
|
||||
github = "ckgxrg-salt";
|
||||
githubId = 165614491;
|
||||
name = "ckgxrg";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
cmacrae = {
|
||||
email = "hi@cmacr.ae";
|
||||
@@ -884,6 +910,13 @@
|
||||
name = "Phillip Cloud";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
csanthiago = {
|
||||
email = "git@csanthiago.dev";
|
||||
github = "csanthiago";
|
||||
githubId = 8346803;
|
||||
name = "csanthiago";
|
||||
source = "home-manager";
|
||||
};
|
||||
cwyc = {
|
||||
email = "hello@cwyc.page";
|
||||
github = "cwyc";
|
||||
@@ -964,13 +997,6 @@
|
||||
name = "Robin Stumm";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
diniamo = {
|
||||
email = "diniamo53@gmail.com";
|
||||
github = "diniamo";
|
||||
githubId = 55629891;
|
||||
name = "diniamo";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
donovanglover = {
|
||||
github = "donovanglover";
|
||||
githubId = 2374245;
|
||||
@@ -1445,7 +1471,7 @@
|
||||
github = "kaleocheng";
|
||||
githubId = 7939352;
|
||||
name = "Kaleo Cheng";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
kalhauge = {
|
||||
email = "kalhauge@users.noreply.github.com";
|
||||
@@ -1739,7 +1765,7 @@
|
||||
github = "miku4k";
|
||||
githubId = 89653242;
|
||||
name = "Miku B";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
minijackson = {
|
||||
email = "minijackson@riseup.net";
|
||||
@@ -1838,7 +1864,7 @@
|
||||
github = "natecox";
|
||||
githubId = 2782695;
|
||||
name = "Nate Cox";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
nbp = {
|
||||
email = "nixos@nbp.name";
|
||||
@@ -2059,6 +2085,18 @@
|
||||
name = "Rasmus Kirk";
|
||||
source = "home-manager";
|
||||
};
|
||||
ratakor = {
|
||||
email = "ratakor@disroot.org";
|
||||
github = "ratakor";
|
||||
githubId = 45130910;
|
||||
keys = [
|
||||
{
|
||||
fingerprint = "B60F 8F80 D6CD C5D2 58CF 14C3 241B 1CBE 567B 287E";
|
||||
}
|
||||
];
|
||||
name = "Ratakor";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
ravenjoad = {
|
||||
email = "raven@hallsby.com";
|
||||
github = "ravenjoad";
|
||||
@@ -2170,7 +2208,7 @@
|
||||
github = "s0racat";
|
||||
githubId = 125882337;
|
||||
name = "soracat";
|
||||
source = "nixpkgs";
|
||||
source = "home-manager";
|
||||
};
|
||||
sableseyler = {
|
||||
email = "sable@seyleri.us";
|
||||
@@ -2196,7 +2234,7 @@
|
||||
github = "semi710";
|
||||
githubId = 60490474;
|
||||
name = "Nikhil Singh";
|
||||
source = "home-manager";
|
||||
source = "nixpkgs";
|
||||
};
|
||||
shikanime = {
|
||||
email = "william.phetsinorath@shikanime.studio";
|
||||
@@ -2459,7 +2497,7 @@
|
||||
source = "nixpkgs";
|
||||
};
|
||||
yaaaarn = {
|
||||
email = "30006414+yaaaarn@users.noreply.github.com";
|
||||
email = "nix@yarncat.moe";
|
||||
github = "yaaaarn";
|
||||
githubId = 30006414;
|
||||
name = "yarncat";
|
||||
|
||||
@@ -67,31 +67,7 @@ stdenv.mkDerivation {
|
||||
mkdir -p out
|
||||
cp -r book/* out/
|
||||
|
||||
makeRedirect() {
|
||||
local target=$1
|
||||
local destination=$2
|
||||
printf '%s\n' \
|
||||
'<!doctype html>' \
|
||||
'<html lang="en">' \
|
||||
' <head>' \
|
||||
' <meta charset="utf-8">' \
|
||||
" <meta http-equiv=\"refresh\" content=\"0; url=$destination\">" \
|
||||
" <link rel=\"canonical\" href=\"$destination\">" \
|
||||
' <title>Redirecting...</title>' \
|
||||
' </head>' \
|
||||
' <body>' \
|
||||
" <p>Redirecting to <a href=\"$destination\">$destination</a>.</p>" \
|
||||
' </body>' \
|
||||
'</html>' \
|
||||
> "out/$target"
|
||||
}
|
||||
|
||||
makeRedirect index.xhtml index.html
|
||||
makeRedirect options.html options/home-manager/index.html
|
||||
makeRedirect options.xhtml options/home-manager/index.html
|
||||
makeRedirect nixos-options.xhtml options/nixos/index.html
|
||||
makeRedirect nix-darwin-options.xhtml options/nix-darwin/index.html
|
||||
makeRedirect release-notes.xhtml release-notes/release-notes.html
|
||||
python3 ${./mdbook/legacy-redirects.py} source out
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
Contributions to Home Manager are very welcome. To make the process as
|
||||
smooth as possible for both you and the Home Manager maintainers we
|
||||
provide some guidelines that we ask you to follow. See [Getting
|
||||
started](#sec-contrib-getting-started) for information on how to set up
|
||||
a suitable development environment and [Guidelines](#sec-guidelines) for
|
||||
started](contributing/getting-started.md#sec-contrib-getting-started) for information on how to set up
|
||||
a suitable development environment and [Guidelines](contributing/guidelines.md#sec-guidelines) for
|
||||
the actual guidelines.
|
||||
|
||||
This text is mainly directed at those who would like to make code
|
||||
|
||||
@@ -8,7 +8,7 @@ how to do this.
|
||||
Once you have a fork of Home Manager you should create a branch starting
|
||||
at the most recent `master` branch. Give your branch a reasonably
|
||||
descriptive name. Commit your changes to this branch and when you are
|
||||
happy with the result and it fulfills [Guidelines](#sec-guidelines) then
|
||||
happy with the result and it fulfills [Guidelines](guidelines.md#sec-guidelines) then
|
||||
push the branch to GitHub and [create a pull
|
||||
request](https://help.github.com/articles/creating-a-pull-request/).
|
||||
|
||||
@@ -21,7 +21,7 @@ the `home-manager` command use it by either
|
||||
$ home-manager -I home-manager=$HOME/devel/home-manager
|
||||
```
|
||||
|
||||
or, if using [flakes](#sec-flakes-standalone):
|
||||
or, if using [flakes](../nix-flakes/standalone.md#sec-flakes-standalone):
|
||||
|
||||
``` shell
|
||||
$ home-manager --override-input home-manager ~/devel/home-manager
|
||||
|
||||
@@ -89,7 +89,7 @@ configuration location with an environment variable, for example
|
||||
|
||||
If at all possible, make sure to add new tests and expand existing tests
|
||||
so that your change will keep working in the future. See
|
||||
[Tests](#sec-tests) for more information about the Home Manager test
|
||||
[Tests](tests.md#sec-tests) for more information about the Home Manager test
|
||||
suite.
|
||||
|
||||
All contributed code *must* pass the test suite.
|
||||
@@ -116,7 +116,7 @@ the man page version of the module options looks good:
|
||||
|
||||
``` shell
|
||||
$ nix-build -A docs.manPages
|
||||
$ man ./result/share/man/man5/home-configuration.nix.5.gz
|
||||
$ man ./result/share/man/man5/home-configuration.nix.5
|
||||
```
|
||||
|
||||
## Module Auto-importing {#sec-module-auto-importing}
|
||||
@@ -182,7 +182,7 @@ consistent commit message format as described in
|
||||
|
||||
If your contribution includes a change that should be communicated to
|
||||
users of Home Manager then you can add a news entry. The entry must be
|
||||
formatted as described in [News](#sec-news).
|
||||
formatted as described in [News](news.md#sec-news).
|
||||
|
||||
When new modules are added a news entry should be included.
|
||||
|
||||
@@ -190,7 +190,7 @@ News entries and release notes serve different purposes. Release notes
|
||||
should be updated separately when a change affects users migrating
|
||||
between stable releases, such as state version default changes, required
|
||||
migration steps, or broad behavior changes. See
|
||||
[Release Notes](#sec-contributing-release-notes) and [News](#sec-news)
|
||||
[Release Notes](release-notes.md#sec-contributing-release-notes) and [News](news.md#sec-news)
|
||||
for more details.
|
||||
|
||||
## Use conditional modules and news {#sec-guidelines-conditional-modules}
|
||||
@@ -200,12 +200,16 @@ of the supported platforms. The most common example of platform specific
|
||||
modules are those that define systemd user services, which only works on
|
||||
Linux systems.
|
||||
|
||||
If you add a module that is platform specific then make sure to include
|
||||
a condition in the `loadModule` function call. This will make the module
|
||||
accessible only on systems where the condition evaluates to `true`.
|
||||
If you add a module that is platform specific then make sure the module
|
||||
guards platform-specific configuration with an appropriate condition, for
|
||||
example `pkgs.stdenv.hostPlatform.isLinux` or
|
||||
`pkgs.stdenv.hostPlatform.isDarwin`. Modules in `modules/programs/` and
|
||||
`modules/services/` are auto-imported, so the platform condition should live
|
||||
in the module behavior and in any platform-specific tests rather than in a
|
||||
separate module discovery call.
|
||||
|
||||
Similarly, if you are adding a news entry then it should be shown only
|
||||
to users that may find it relevant, see [News](#sec-news) for a
|
||||
to users that may find it relevant, see [News](news.md#sec-news) for a
|
||||
description of conditional news.
|
||||
|
||||
## Mind the license {#sec-guidelines-licensing}
|
||||
|
||||
@@ -12,7 +12,7 @@ communication about noteworthy changes, such as a new module, a new
|
||||
feature, or a specific deprecation. Release notes are read from the
|
||||
website documentation and should summarize what users need to know before
|
||||
or during a stable-release upgrade. See
|
||||
[Release Notes](#sec-contributing-release-notes) for guidance on changes
|
||||
[Release Notes](release-notes.md#sec-contributing-release-notes) for guidance on changes
|
||||
that affect stable-release upgrades.
|
||||
|
||||
If you do have a change worthy of a news entry then please add one in
|
||||
@@ -34,11 +34,12 @@ but you should follow some basic guidelines:
|
||||
This will create a new file inside the `modules/misc/news` directory
|
||||
with some placeholder information that you can edit.
|
||||
|
||||
- The entry condition should be as specific as possible. For example,
|
||||
if you are changing or deprecating a specific option then you could
|
||||
restrict the news to those users who actually use this option.
|
||||
Prefer a targeted condition over skipping useful news only to avoid
|
||||
notifying unaffected users.
|
||||
- The entry condition should be as specific as possible for changes
|
||||
affecting existing functionality. For example, if you are changing
|
||||
or deprecating a specific option then you could restrict the news to
|
||||
those users who actually use this option. Prefer a targeted
|
||||
condition over skipping useful news only to avoid notifying
|
||||
unaffected users.
|
||||
|
||||
- Wrap the news message so that it will fit in the typical terminal,
|
||||
that is, at most 80 characters wide. Ideally a bit less.
|
||||
@@ -64,9 +65,9 @@ but you should follow some basic guidelines:
|
||||
|
||||
A new module is available: 'services.foo'.
|
||||
|
||||
Since this news is specific to the module, its condition should use
|
||||
the module enable option to avoid spamming non-users of the module,
|
||||
for example `condition = config.services.foo.enable;`.
|
||||
Since this news announces newly available functionality, its
|
||||
condition should not require `config.services.foo.enable`; otherwise
|
||||
users who may want the new module will not see the news.
|
||||
|
||||
If the module is platform specific, e.g., a service module using
|
||||
systemd, then a condition like
|
||||
@@ -75,7 +76,6 @@ but you should follow some basic guidelines:
|
||||
condition = hostPlatform.isLinux;
|
||||
```
|
||||
|
||||
should be added, either by itself for platform-scoped news or in
|
||||
combination with the module enable option. Use the `create-news-entry`
|
||||
generator described above to scaffold this entry as part of your
|
||||
contribution.
|
||||
should be added to avoid showing the news on unsupported platforms.
|
||||
Use the `create-news-entry` generator described above to scaffold
|
||||
this entry as part of your contribution.
|
||||
|
||||
@@ -192,16 +192,22 @@ $ nix run .#tests -- alacritty
|
||||
# Run a specific test
|
||||
$ nix run .#tests -- test-alacritty-empty-settings
|
||||
|
||||
# Run integration tests
|
||||
# List integration tests
|
||||
$ nix run .#tests -- -t -l
|
||||
|
||||
# Run all integration tests
|
||||
$ nix run .#tests -- -t integration-test-
|
||||
|
||||
# Interactive test selection (requires fzf)
|
||||
$ python3 tests/tests.py -i
|
||||
$ nix run .#tests -- -i
|
||||
|
||||
# Pass additional nix build flags
|
||||
$ nix run .#tests -- alacritty -- --verbose
|
||||
```
|
||||
|
||||
Integration tests are only exposed on Linux. On other platforms, integration
|
||||
test discovery may report no matching tests.
|
||||
|
||||
## Manual test commands {#sec-tests-manual}
|
||||
|
||||
For advanced usage or CI environments, you can also run tests manually using nix build commands.
|
||||
@@ -215,7 +221,7 @@ $ nix-build --pure --option allow-import-from-derivation false tests -A build.al
|
||||
in the project root. List all test cases through
|
||||
|
||||
``` shell
|
||||
$ nix-build --pure tests --option allow-import-from-derivation false -A list
|
||||
$ nix run .#tests -- -l
|
||||
```
|
||||
|
||||
and run an individual test, for example `alacritty-empty-settings`,
|
||||
@@ -230,13 +236,13 @@ and may cause failures. To run against the Nixpkgs from the `flake.lock` file,
|
||||
use instead e.g.
|
||||
|
||||
``` shell
|
||||
$ nix build --option allow-import-from-derivation false .#test-all
|
||||
$ nix build .#test-all
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
``` shell
|
||||
$ nix build --option allow-import-from-derivation false .#test-alacritty-empty-settings
|
||||
$ nix build .#test-alacritty-empty-settings
|
||||
```
|
||||
|
||||
Some tests may be marked with `enableLegacyIfd`, those may be run by run with e.g.
|
||||
|
||||
@@ -31,7 +31,7 @@ this package. This can typically be done in two ways.
|
||||
|
||||
should work OK. With flakes, pass the unstable package set as
|
||||
described in
|
||||
[How do I install packages from Nixpkgs unstable?](#_how_do_i_install_packages_from_nixpkgs_unstable)
|
||||
[How do I install packages from Nixpkgs unstable?](unstable.md#_how_do_i_install_packages_from_nixpkgs_unstable)
|
||||
and then use the extra module argument:
|
||||
|
||||
``` nix
|
||||
|
||||
@@ -6,26 +6,26 @@ Home Manager can be used in three primary ways:
|
||||
NixOS and Darwin, this is the only available choice. It is also
|
||||
recommended for people on NixOS or Darwin that want to manage their
|
||||
home directory independently of the system as a whole. See
|
||||
[Standalone installation](#sec-install-standalone) for instructions
|
||||
[Standalone installation](installation/standalone.md#sec-install-standalone) for instructions
|
||||
on how to perform this installation.
|
||||
|
||||
2. As a module within a NixOS system configuration. This allows the
|
||||
user profiles to be built together with the system when running
|
||||
`nixos-rebuild`. See [NixOS module](#sec-install-nixos-module) for a
|
||||
`nixos-rebuild`. See [NixOS module](installation/nixos.md#sec-install-nixos-module) for a
|
||||
description of this setup.
|
||||
|
||||
3. As a module within a
|
||||
[nix-darwin](https://github.com/nix-darwin/nix-darwin/) system
|
||||
configuration. This allows the user profiles to be built together
|
||||
with the system when running `darwin-rebuild`. See [nix-darwin
|
||||
module](#sec-install-nix-darwin-module) for a description of this
|
||||
module](installation/nix-darwin.md#sec-install-nix-darwin-module) for a description of this
|
||||
setup.
|
||||
|
||||
:::{.note}
|
||||
In this chapter we describe how to install Home Manager in the standard
|
||||
way using channels. If you prefer to use [Nix
|
||||
Flakes](https://wiki.nixos.org/wiki/Flakes) then please see the instructions
|
||||
in [nix flakes](#ch-nix-flakes).
|
||||
in [nix flakes](nix-flakes.md#ch-nix-flakes).
|
||||
:::
|
||||
|
||||
```{=include=} sections
|
||||
|
||||
@@ -52,6 +52,9 @@ home-manager.users.eve = { pkgs, ... }: {
|
||||
and after a `darwin-rebuild switch` the user eve's environment should
|
||||
include a basic Bash configuration and the packages atool and httpie.
|
||||
|
||||
Home Manager activation runs as part of nix-darwin activation for each
|
||||
configured user.
|
||||
|
||||
If you do not plan on having Home Manager manage your shell
|
||||
configuration then you must add either
|
||||
|
||||
@@ -103,13 +106,23 @@ Nixpkgs.
|
||||
:::
|
||||
|
||||
:::{.note}
|
||||
Home Manager will pass `osConfig` as a module argument to any modules
|
||||
you create. This contains the system's nix-darwin configuration.
|
||||
Home Manager passes extra module arguments to each
|
||||
`home-manager.users.<name>` module:
|
||||
|
||||
``` nix
|
||||
{ lib, pkgs, osConfig, ... }:
|
||||
{ lib, pkgs, osConfig, darwinConfig, osClass, modulesPath, ... }:
|
||||
```
|
||||
|
||||
Here `osConfig` contains the system's nix-darwin configuration and
|
||||
`darwinConfig` is a Darwin-specific alias for the same value. The `lib`
|
||||
argument is Home Manager's extended library. You can pass additional module
|
||||
arguments with `home-manager.extraSpecialArgs`.
|
||||
:::
|
||||
|
||||
Once installed you can see [Using Home Manager](#ch-usage) for a more detailed
|
||||
:::{.note}
|
||||
Use `home-manager.sharedModules` to add Home Manager modules to every user
|
||||
declared under `home-manager.users`.
|
||||
:::
|
||||
|
||||
Once installed you can see [Using Home Manager](../usage.md#ch-usage) for a more detailed
|
||||
description of Home Manager and how to use it.
|
||||
|
||||
@@ -85,12 +85,21 @@ httpie.
|
||||
|
||||
:::{.note}
|
||||
If `nixos-rebuild switch` does not result in the environment you expect,
|
||||
you can take a look at the output of the Home Manager activation script
|
||||
output using
|
||||
then the service to inspect depends on the activation mode.
|
||||
|
||||
By default, Home Manager activates each configured user during boot and
|
||||
system rebuilds through a NixOS system service:
|
||||
|
||||
``` shell
|
||||
$ systemctl status "home-manager-$USER.service"
|
||||
```
|
||||
|
||||
If `home-manager.startAsUserService = true` is set, Home Manager instead
|
||||
activates through the user's systemd service:
|
||||
|
||||
``` shell
|
||||
$ systemctl --user status home-manager.service
|
||||
```
|
||||
:::
|
||||
|
||||
If you do not plan on having Home Manager manage your shell
|
||||
@@ -144,13 +153,23 @@ Nixpkgs.
|
||||
:::
|
||||
|
||||
:::{.note}
|
||||
Home Manager will pass `osConfig` as a module argument to any modules
|
||||
you create. This contains the system's NixOS configuration.
|
||||
Home Manager passes extra module arguments to each
|
||||
`home-manager.users.<name>` module:
|
||||
|
||||
``` nix
|
||||
{ lib, pkgs, osConfig, ... }:
|
||||
{ lib, pkgs, osConfig, nixosConfig, osClass, modulesPath, ... }:
|
||||
```
|
||||
|
||||
Here `osConfig` contains the system's NixOS configuration and `nixosConfig`
|
||||
is a NixOS-specific alias for the same value. The `lib` argument is Home
|
||||
Manager's extended library. You can pass additional module arguments with
|
||||
`home-manager.extraSpecialArgs`.
|
||||
:::
|
||||
|
||||
Once installed you can see [Using Home Manager](#ch-usage) for a more detailed
|
||||
:::{.note}
|
||||
Use `home-manager.sharedModules` to add Home Manager modules to every user
|
||||
declared under `home-manager.users`.
|
||||
:::
|
||||
|
||||
Once installed you can see [Using Home Manager](../usage.md#ch-usage) for a more detailed
|
||||
description of Home Manager and how to use it.
|
||||
|
||||
@@ -71,5 +71,5 @@ checkout of the repository then you can use the
|
||||
[home-manager.path](#opt-programs.home-manager.path) option to specify the absolute
|
||||
path to the repository.
|
||||
|
||||
Once installed you can see [Using Home Manager](#ch-usage) for a more detailed
|
||||
Once installed you can see [Using Home Manager](../usage.md#ch-usage) for a more detailed
|
||||
description of Home Manager and how to use it.
|
||||
|
||||
@@ -12,18 +12,18 @@ flake in three ways:
|
||||
NixOS and Darwin, this is the only available choice. It is also
|
||||
recommended for people on NixOS or Darwin that want to manage their
|
||||
home directory independently of the system as a whole. See
|
||||
[Standalone setup](#sec-flakes-standalone) for instructions on how
|
||||
[Standalone setup](nix-flakes/standalone.md#sec-flakes-standalone) for instructions on how
|
||||
to perform this installation.
|
||||
|
||||
2. As a module within a NixOS system configuration. This allows the
|
||||
user profiles to be built together with the system when running
|
||||
`nixos-rebuild`. See [NixOS module](#sec-flakes-nixos-module) for a
|
||||
`nixos-rebuild`. See [NixOS module](nix-flakes/nixos.md#sec-flakes-nixos-module) for a
|
||||
description of this setup.
|
||||
|
||||
3. As a module within a [nix-darwin](https://github.com/nix-darwin/nix-darwin/)
|
||||
system configuration. This allows the user profiles to be built
|
||||
together with the system when running `darwin-rebuild`. See
|
||||
[nix-darwin module](#sec-flakes-nix-darwin-module) for a
|
||||
[nix-darwin module](nix-flakes/nix-darwin.md#sec-flakes-nix-darwin-module) for a
|
||||
description of this setup.
|
||||
|
||||
Advanced users may want Home Manager's `nixpkgs` input to follow the
|
||||
|
||||
@@ -45,8 +45,12 @@ module argument from within the module graph itself. For values that originate
|
||||
outside the module graph, such as flake inputs, prefer
|
||||
`home-manager.extraSpecialArgs`.
|
||||
|
||||
and it is also rebuilt with the nix-darwin generations. The rebuild
|
||||
command here may be `darwin-rebuild switch --flake ~/.config/darwin`.
|
||||
Use `home-manager.sharedModules` for Home Manager modules or settings that
|
||||
should be imported by every user declared in `home-manager.users`.
|
||||
|
||||
The Home Manager configuration is also rebuilt with the nix-darwin
|
||||
generations. The rebuild command here may be
|
||||
`darwin-rebuild switch --flake ~/.config/darwin`.
|
||||
|
||||
You can use the above `flake.nix` as a template in `~/.config/darwin` by
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ module argument from within the module graph itself. For values that originate
|
||||
outside the module graph, such as flake inputs, prefer
|
||||
`home-manager.extraSpecialArgs`.
|
||||
|
||||
Use `home-manager.sharedModules` for Home Manager modules or settings that
|
||||
should be imported by every user declared in `home-manager.users`.
|
||||
|
||||
The Home Manager configuration is then part of the NixOS configuration
|
||||
and is automatically rebuilt with the system when using the appropriate
|
||||
command for the system, such as
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
|
||||
Unlike the channel-based setup, `home.nix` will be evaluated when
|
||||
the flake is built, so it must be present before bootstrap of Home
|
||||
Manager from the flake. See [Configuration Example](#sec-usage-configuration) for
|
||||
Manager from the flake. See [Configuration Example](../usage/configuration.md#sec-usage-configuration) for
|
||||
introduction about writing a Home Manager configuration.
|
||||
|
||||
@@ -43,7 +43,7 @@ option will state the expected type, for
|
||||
[programs.emacs.enable](#opt-programs.emacs.enable) you will see "Type: boolean". You
|
||||
there also find information about the default value and a description of
|
||||
the option. You can find the complete option documentation in
|
||||
[Home Manager Configuration Options](#ch-options) or directly in the terminal by running
|
||||
[Home Manager Configuration Options](options.md#ch-options) or directly in the terminal by running
|
||||
|
||||
``` shell
|
||||
man home-configuration.nix
|
||||
|
||||
@@ -39,7 +39,7 @@ This error is about a file that Home Manager wants to manage as a
|
||||
symbolic link in your home directory. It is separate from package
|
||||
profile collisions, which usually mention `installPackages` or a
|
||||
`collision between .../bin/...` path. For package collisions, see
|
||||
[Why is there a collision error when switching generation?](#_why_is_there_a_collision_error_when_switching_generation).
|
||||
[Why is there a collision error when switching generation?](../faq/collision.md#_why_is_there_a_collision_error_when_switching_generation).
|
||||
|
||||
## Resolving file collisions {#sec-usage-dotfiles-collisions}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ without changing release branches.
|
||||
If you want to move from one release branch to another, such as
|
||||
`release-25.05` to `release-25.11`, or between a release branch and
|
||||
`master`, see
|
||||
[Upgrading to a new Home Manager release](#sec-upgrade-release).
|
||||
[Upgrading to a new Home Manager release](upgrading.md#sec-upgrade-release).
|
||||
|
||||
## Flake-Based Configurations {#sec-updating-flakes}
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ affected configuration.
|
||||
|
||||
### Common Issues {#sec-upgrade-release-state-troubleshooting-common-issues}
|
||||
|
||||
Check the [Home Manager Release Notes](#ch-release-notes) for breaking changes.
|
||||
Check the [Home Manager Release Notes](../release-notes/release-notes.md#ch-release-notes) for breaking changes.
|
||||
|
||||
1. **Version Mismatch Warning**: If you see warnings about version
|
||||
mismatches, ensure your Home Manager branch matches the Nixpkgs
|
||||
@@ -199,4 +199,4 @@ the expected release branch.
|
||||
## Additional Resources {#sec-upgrade-release-resources}
|
||||
|
||||
- [NixOS Stable Release Notes](https://nixos.org/manual/nixos/stable/release-notes)
|
||||
- [Home Manager Release Notes](#ch-release-notes)
|
||||
- [Home Manager Release Notes](../release-notes/release-notes.md#ch-release-notes)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ANCHOR = re.compile(r'<a id="([^"]+)"></a>')
|
||||
SPECIAL_PAGES = {
|
||||
Path("manual.md"): "index.html",
|
||||
Path("preface.md"): "index.html",
|
||||
Path("options.md"): "options/home-manager/index.html",
|
||||
Path("nixos-options.md"): "options/nixos/index.html",
|
||||
Path("nix-darwin-options.md"): "options/nix-darwin/index.html",
|
||||
}
|
||||
OPTION_PREFIXES = {
|
||||
"opt-": "options/home-manager",
|
||||
"nixos-opt-": "options/nixos",
|
||||
"nix-darwin-opt-": "options/nix-darwin",
|
||||
}
|
||||
|
||||
ROUTER = """
|
||||
(function () {
|
||||
var h = location.hash || "", a = h.slice(1), t = __DEFAULT__;
|
||||
var m = __ANCHORS__, p = __PREFIXES__;
|
||||
function o(a) {
|
||||
for (var k in p) {
|
||||
if (a.indexOf(k) !== 0) continue;
|
||||
var s = a.slice(k.length).replace(/[<>]/g, "_").split("."), q = s[0];
|
||||
if ((q === "programs" || q === "services") && s.length > 1) q += "/" + s[1];
|
||||
return p[k] + "/" + q + ".html";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (a) t = Object.prototype.hasOwnProperty.call(m, a) ? m[a] : o(a) || t;
|
||||
if (t === null) return;
|
||||
var n = t + (location.search || "") + h;
|
||||
var c = location.pathname.split("/").pop() + location.search + h;
|
||||
if (n !== c) location.replace(n);
|
||||
}());
|
||||
""".strip()
|
||||
|
||||
|
||||
def page_for(source, path):
|
||||
relative = path.relative_to(source)
|
||||
return SPECIAL_PAGES.get(relative, relative.with_suffix(".html").as_posix())
|
||||
|
||||
|
||||
def anchors_for(source, roots):
|
||||
anchors = {}
|
||||
for root in roots:
|
||||
files = [root] if root.is_file() else sorted(root.rglob("*.md"))
|
||||
for path in files:
|
||||
page = page_for(source, path)
|
||||
for anchor in ANCHOR.findall(path.read_text(encoding="utf-8")):
|
||||
anchors.setdefault(anchor, page)
|
||||
return anchors
|
||||
|
||||
|
||||
def script(default, anchors):
|
||||
js = (
|
||||
ROUTER.replace("__DEFAULT__", json.dumps(default))
|
||||
.replace(
|
||||
"__ANCHORS__", json.dumps(anchors, sort_keys=True, separators=(",", ":"))
|
||||
)
|
||||
.replace("__PREFIXES__", json.dumps(OPTION_PREFIXES, separators=(",", ":")))
|
||||
)
|
||||
return "\n".join(
|
||||
[
|
||||
" <script>",
|
||||
" //<![CDATA[",
|
||||
*[f" {line}" for line in js.splitlines()],
|
||||
" //]]>",
|
||||
" </script>",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def write_redirect(output, name, destination, anchors=None):
|
||||
escaped = html.escape(destination, quote=True)
|
||||
(output / name).write_text(
|
||||
f"""<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
{script(destination, anchors or {})}
|
||||
<meta http-equiv="refresh" content="0; url={escaped}" />
|
||||
<link rel="canonical" href="{escaped}" />
|
||||
<title>Redirecting...</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="{escaped}">{escaped}</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def inject_root_router(output, anchors):
|
||||
index = output / "index.html"
|
||||
text = index.read_text(encoding="utf-8")
|
||||
index.write_text(text.replace("<head>", f"<head>\n{script(None, anchors)}", 1))
|
||||
|
||||
|
||||
def main():
|
||||
source, output = map(Path, sys.argv[1:])
|
||||
|
||||
manual_roots = [
|
||||
path
|
||||
for path in source.iterdir()
|
||||
if path.name != "options" and (path.is_dir() or path.suffix == ".md")
|
||||
]
|
||||
manual_anchors = anchors_for(source, manual_roots)
|
||||
release_anchors = anchors_for(source, [source / "release-notes"])
|
||||
|
||||
inject_root_router(output, manual_anchors)
|
||||
for name, destination, anchors in [
|
||||
("index.xhtml", "index.html", manual_anchors),
|
||||
("options.html", "options/home-manager/index.html", {}),
|
||||
("options.xhtml", "options/home-manager/index.html", {}),
|
||||
("nixos-options.xhtml", "options/nixos/index.html", {}),
|
||||
("nix-darwin-options.xhtml", "options/nix-darwin/index.html", {}),
|
||||
("release-notes.xhtml", "release-notes/release-notes.html", release_anchors),
|
||||
]:
|
||||
write_redirect(output, name, destination, anchors)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -36,7 +36,7 @@ This release has the following notable changes:
|
||||
optionally also activate it. The recommended installation method for
|
||||
a standalone Home Manager setup with Nix flakes uses this new
|
||||
command. The standard installation method remains the same but uses
|
||||
the new command internally. See [sec-flakes-standalone](#sec-flakes-standalone) for
|
||||
the new command internally. See [sec-flakes-standalone](../nix-flakes/standalone.md#sec-flakes-standalone) for
|
||||
more.
|
||||
|
||||
## State Version Changes {#sec-release-23.05-state-version-changes}
|
||||
|
||||
@@ -19,7 +19,7 @@ This release has the following notable changes:
|
||||
- The Home Manager test suite has been removed from the main Nix Flake
|
||||
since it caused unnecessary evaluations and downloads. Instead the
|
||||
tests are available through a Nix Flake file inside the `tests`
|
||||
directory. See [](#sec-tests) for example commands.
|
||||
directory. See [Tests](../contributing/tests.md#sec-tests) for example commands.
|
||||
|
||||
## State Version Changes {#sec-release-25.05-state-version-changes}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ This release has the following notable changes:
|
||||
term removed all together. If you have developed tooling that
|
||||
directly call the generated activation script, then you are
|
||||
encouraged to adapt to the new behavior. See the
|
||||
[Activation](#sec-internals-activation) section in the manual for
|
||||
[Activation](../internals/activation.md#sec-internals-activation) section in the manual for
|
||||
details on how to call the activation script.
|
||||
|
||||
- The `home-manager switch` command now offers a `--rollback` option.
|
||||
@@ -25,7 +25,7 @@ This release has the following notable changes:
|
||||
previously possible to accomplish this by manually activating an old
|
||||
generation, it always created a new profile generation. The new
|
||||
behavior mirrors the behavior of `nixos-rebuild switch --rollback`.
|
||||
See the [Rollbacks](#sec-usage-rollbacks) section for more.
|
||||
See the [Rollbacks](../usage/rollbacks.md#sec-usage-rollbacks) section for more.
|
||||
|
||||
- The `home-manager switch` command now offers a
|
||||
`--specialisation NAME` option. When given, the switch activates the
|
||||
|
||||
@@ -61,7 +61,7 @@ This release has the following notable changes:
|
||||
[modular services](https://nixos.org/manual/nixos/unstable/#modular-services).
|
||||
Service modules shipped with packages (e.g.
|
||||
`pkgs.<name>.passthru.services.default`) drop in unchanged and are lifted to
|
||||
user systemd units. See [Modular Services](#sec-usage-modular-services) for
|
||||
user systemd units. See [Modular Services](../usage/modular-services.md#sec-usage-modular-services) for
|
||||
details.
|
||||
|
||||
- Dedicated modules were added for several VSCode forks (`programs.cursor`,
|
||||
|
||||
@@ -14,8 +14,22 @@ This release has the following notable changes:
|
||||
and `programs.uv.python.prune` / `programs.uv.tool.prune` make the managed set
|
||||
fully declarative by removing versions and tools that are no longer listed.
|
||||
|
||||
- On Darwin, Home Manager launchd agents now support
|
||||
[](#opt-launchd.agents._name_.domain) to choose either the user's GUI or
|
||||
background launchd domain. Agents use the GUI domain by default. Set the
|
||||
domain to `user` for agents that should run without an active graphical login
|
||||
session.
|
||||
|
||||
## State Version Changes {#sec-release-26.11-state-version-changes}
|
||||
|
||||
The state version in this release includes the changes below. These
|
||||
changes are only active if the `home.stateVersion` option is set to
|
||||
"26.11" or later.
|
||||
|
||||
- The KDL options under `programs.zellij` will now correctly escape backslashes
|
||||
in string values. For example, the Nix string `"\\"` will now correctly
|
||||
generate the KDL string `"\\"`. Previously, this would have generated `"\"`,
|
||||
which is invalid KDL. The following options are affected:
|
||||
- `programs.zellij.settings`
|
||||
- `programs.zellij.themes`
|
||||
- `programs.zellij.layouts`
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1780365719,
|
||||
"narHash": "sha256-QfWfccTN+70ZQ4m2qlU9PiKfz2Yppq94058iJyARNwc=",
|
||||
"lastModified": 1783604885,
|
||||
"narHash": "sha256-tzMgSkV7kljEkqIjlgV6F+n+xD+/a35Db8bs7a4BFAo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ffa10e26ae11d676b2db836259889f1f571cb14f",
|
||||
"rev": "767b0d3ec98a143ad9ed7dfc0d5553510ac27133",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -64,6 +64,15 @@
|
||||
"x86_64-linux"
|
||||
];
|
||||
|
||||
releaseInfo = nixpkgs.lib.importJSON ./release.json;
|
||||
|
||||
docsFor =
|
||||
pkgs:
|
||||
import ./docs {
|
||||
inherit pkgs;
|
||||
inherit (releaseInfo) release isReleaseBranch;
|
||||
};
|
||||
|
||||
testChunks =
|
||||
system:
|
||||
let
|
||||
@@ -172,6 +181,7 @@
|
||||
buildbot = forCI (
|
||||
system:
|
||||
let
|
||||
docs = docsFor nixpkgs.legacyPackages.${system};
|
||||
allIntegrationTests = integrationTests system;
|
||||
workingIntegrationTests = nixpkgs.lib.filterAttrs (
|
||||
name: _:
|
||||
@@ -181,17 +191,20 @@
|
||||
]
|
||||
) allIntegrationTests;
|
||||
in
|
||||
(testChunks system) // workingIntegrationTests
|
||||
(testChunks system)
|
||||
// workingIntegrationTests
|
||||
// {
|
||||
docs-html = docs.manual.html;
|
||||
docs-json = docs.options.json;
|
||||
docs-jsonModuleMaintainers = docs.jsonModuleMaintainers;
|
||||
docs-manpages = docs.manPages;
|
||||
}
|
||||
);
|
||||
|
||||
packages = forAllPkgs (
|
||||
pkgs:
|
||||
let
|
||||
releaseInfo = nixpkgs.lib.importJSON ./release.json;
|
||||
docs = import ./docs {
|
||||
inherit pkgs;
|
||||
inherit (releaseInfo) release isReleaseBranch;
|
||||
};
|
||||
docs = docsFor pkgs;
|
||||
hmPkg = pkgs.callPackage ./home-manager { path = "${self}"; };
|
||||
in
|
||||
{
|
||||
|
||||
@@ -291,6 +291,7 @@ function doInspectOption() {
|
||||
for path in "${paths[@]}"; do
|
||||
nix-instantiate --eval --json \
|
||||
--arg nixos "$modulesExpr" \
|
||||
"${extraArgs[@]}" \
|
||||
--argstr path "$path" \
|
||||
--arg recursive "$recursive" \
|
||||
"$NIXOS_OPTION_NIX" \
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: Home Manager\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nix-community/home-manager/issues\n"
|
||||
"POT-Creation-Date: 2026-01-21 09:53+0100\n"
|
||||
"PO-Revision-Date: 2026-06-12 10:01+0000\n"
|
||||
"Last-Translator: Tommaso Soncin <soncintommaso@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-07-19 08:01+0000\n"
|
||||
"Last-Translator: Artur Sannikov <weblate.rbpxz@8shield.net>\n"
|
||||
"Language-Team: Italian <https://hosted.weblate.org/projects/home-manager/cli/"
|
||||
"it/>\n"
|
||||
"Language: it\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.7.dev0\n"
|
||||
"X-Generator: Weblate 2026.8.dev0\n"
|
||||
|
||||
#. translators: For example: "home-manager: missing argument for --cores"
|
||||
#: home-manager/home-manager:16
|
||||
@@ -104,7 +104,7 @@ msgstr ""
|
||||
|
||||
#: home-manager/home-manager:153
|
||||
msgid "Sanity checking Nix"
|
||||
msgstr "Controllando Nix"
|
||||
msgstr "Verifica di correttezza di Nix"
|
||||
|
||||
#: home-manager/home-manager:173
|
||||
msgid "Could not find suitable profile directory, tried %s and %s"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: Home Manager\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nix-community/home-manager/issues\n"
|
||||
"POT-Creation-Date: 2026-01-21 09:53+0100\n"
|
||||
"PO-Revision-Date: 2026-03-31 16:16+0000\n"
|
||||
"Last-Translator: NikSne <commits@niksne.ru>\n"
|
||||
"PO-Revision-Date: 2026-07-19 08:01+0000\n"
|
||||
"Last-Translator: Artur Sannikov <weblate.rbpxz@8shield.net>\n"
|
||||
"Language-Team: Russian <https://hosted.weblate.org/projects/home-manager/cli/"
|
||||
"ru/>\n"
|
||||
"Language: ru\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"X-Generator: Weblate 5.17-dev\n"
|
||||
"X-Generator: Weblate 2026.8.dev0\n"
|
||||
|
||||
#. translators: For example: "home-manager: missing argument for --cores"
|
||||
#: home-manager/home-manager:16
|
||||
@@ -205,7 +205,7 @@ msgstr "Не могу начать сборку в директории, защ
|
||||
|
||||
#: home-manager/home-manager:822
|
||||
msgid "The configuration does not contain the specialisation \"%s\""
|
||||
msgstr ""
|
||||
msgstr "В конфигурации отсутствует специализация «%s»"
|
||||
|
||||
#: home-manager/home-manager:876
|
||||
msgid "No generation with ID %s"
|
||||
|
||||
@@ -8,8 +8,9 @@ msgstr ""
|
||||
"Project-Id-Version: Home Manager\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nix-community/home-manager/issues\n"
|
||||
"POT-Creation-Date: 2026-01-21 09:53+0100\n"
|
||||
"PO-Revision-Date: 2026-05-27 11:11+0000\n"
|
||||
"Last-Translator: qb114514 <GNUqb114514@outlook.com>\n"
|
||||
"PO-Revision-Date: 2026-07-19 08:01+0000\n"
|
||||
"Last-Translator: reducedradius "
|
||||
"<137701630+flytothehighest@users.noreply.github.com>\n"
|
||||
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
|
||||
"projects/home-manager/cli/zh_Hans/>\n"
|
||||
"Language: zh_Hans\n"
|
||||
@@ -17,7 +18,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 2026.6.dev0\n"
|
||||
"X-Generator: Weblate 2026.8.dev0\n"
|
||||
|
||||
#. translators: For example: "home-manager: missing argument for --cores"
|
||||
#: home-manager/home-manager:16
|
||||
@@ -193,9 +194,8 @@ msgid "Cannot run build in read-only directory"
|
||||
msgstr "无法在只读目录中运行构建"
|
||||
|
||||
#: home-manager/home-manager:822
|
||||
#, fuzzy
|
||||
msgid "The configuration does not contain the specialisation \"%s\""
|
||||
msgstr "配置不包含 specialisation “%s”"
|
||||
msgstr "配置不包含专门化的 “%s”"
|
||||
|
||||
#: home-manager/home-manager:876
|
||||
msgid "No generation with ID %s"
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
# Extract maintainers from Home Manager modules using meta.maintainers
|
||||
# This script evaluates all Home Manager modules and extracts the merged maintainer information
|
||||
let
|
||||
pkgs = import <nixpkgs> { };
|
||||
flakeLock = builtins.fromJSON (builtins.readFile ../../flake.lock);
|
||||
nixpkgsNodeName = flakeLock.nodes.root.inputs.nixpkgs;
|
||||
nixpkgsLocked = flakeLock.nodes.${nixpkgsNodeName}.locked;
|
||||
|
||||
pkgs = import (fetchTarball {
|
||||
url = "https://github.com/${nixpkgsLocked.owner}/${nixpkgsLocked.repo}/archive/${nixpkgsLocked.rev}.tar.gz";
|
||||
sha256 = nixpkgsLocked.narHash;
|
||||
}) { };
|
||||
lib = import ../../modules/lib/stdlib-extended.nix pkgs.lib;
|
||||
releaseInfo = pkgs.lib.importJSON ../../release.json;
|
||||
|
||||
@@ -25,18 +32,23 @@ let
|
||||
file:
|
||||
let
|
||||
fileContent = import file;
|
||||
# Arguments expected by docs/home-manager-manual.nix.
|
||||
evaluated =
|
||||
if lib.isFunction fileContent then
|
||||
fileContent {
|
||||
inherit (pkgs) stdenv lib;
|
||||
documentation-highlighter = { };
|
||||
inherit lib;
|
||||
inherit (pkgs)
|
||||
callPackage
|
||||
mdbook
|
||||
python3
|
||||
stdenv
|
||||
;
|
||||
revision = "unknown";
|
||||
home-manager-options = {
|
||||
home-manager = { };
|
||||
nixos = { };
|
||||
nix-darwin = { };
|
||||
};
|
||||
nixos-render-docs = { };
|
||||
}
|
||||
else
|
||||
fileContent;
|
||||
@@ -59,27 +71,30 @@ let
|
||||
|
||||
allMaintainerObjects = extractMaintainerObjects maintainers ++ additionalMaintainerObjects;
|
||||
|
||||
allMaintainerNames = lib.filter (name: name != null) (
|
||||
map (maintainer: maintainer.github or maintainer.name or null) allMaintainerObjects
|
||||
);
|
||||
getMaintainerName = maintainer: maintainer.github or maintainer.name or null;
|
||||
|
||||
allMaintainerNames = lib.filter (name: name != null) (map getMaintainerName allMaintainerObjects);
|
||||
|
||||
hmMaintainers = import ../../modules/lib/maintainers.nix;
|
||||
hmMaintainerNames = lib.attrNames hmMaintainers;
|
||||
hmMaintainerNames = lib.filter (name: name != null) (
|
||||
map getMaintainerName (lib.attrValues hmMaintainers)
|
||||
);
|
||||
|
||||
maintainerDetails = lib.pipe allMaintainerObjects [
|
||||
(lib.filter (obj: (obj.github or obj.name or null) != null))
|
||||
(map (obj: {
|
||||
name = obj.github or obj.name;
|
||||
value = obj // {
|
||||
source =
|
||||
if categorizedMaintainers.home-manager ? ${obj.github} then
|
||||
"home-manager"
|
||||
else if categorizedMaintainers.nixpkgs ? ${obj.github} then
|
||||
"nixpkgs"
|
||||
else
|
||||
throw "${obj.github} is neither a home-manager or nixpkgs maintainer";
|
||||
};
|
||||
}))
|
||||
(map (
|
||||
obj:
|
||||
let
|
||||
maintainerName = obj.github or obj.name;
|
||||
in
|
||||
{
|
||||
name = maintainerName;
|
||||
value = obj // {
|
||||
source =
|
||||
if categorizedMaintainers.home-manager ? ${maintainerName} then "home-manager" else "nixpkgs";
|
||||
};
|
||||
}
|
||||
))
|
||||
lib.listToAttrs
|
||||
];
|
||||
|
||||
|
||||
@@ -13,12 +13,10 @@ let
|
||||
mkIf
|
||||
mkMerge
|
||||
mkDefault
|
||||
mkAliasOptionModule
|
||||
types
|
||||
literalExpression
|
||||
escapeShellArg
|
||||
hm
|
||||
getAttrFromPath
|
||||
any
|
||||
optional
|
||||
;
|
||||
@@ -140,46 +138,10 @@ in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.league ];
|
||||
|
||||
imports = [
|
||||
(mkAliasOptionModule
|
||||
[ "xsession" "pointerCursor" "package" ]
|
||||
[
|
||||
"home"
|
||||
"pointerCursor"
|
||||
"package"
|
||||
]
|
||||
)
|
||||
(mkAliasOptionModule
|
||||
[ "xsession" "pointerCursor" "name" ]
|
||||
[
|
||||
"home"
|
||||
"pointerCursor"
|
||||
"name"
|
||||
]
|
||||
)
|
||||
(mkAliasOptionModule
|
||||
[ "xsession" "pointerCursor" "size" ]
|
||||
[
|
||||
"home"
|
||||
"pointerCursor"
|
||||
"size"
|
||||
]
|
||||
)
|
||||
(mkAliasOptionModule
|
||||
[ "xsession" "pointerCursor" "defaultCursor" ]
|
||||
[
|
||||
"home"
|
||||
"pointerCursor"
|
||||
"x11"
|
||||
"defaultCursor"
|
||||
]
|
||||
)
|
||||
];
|
||||
|
||||
options = {
|
||||
home.pointerCursor = mkOption {
|
||||
type = types.nullOr pointerCursorModule;
|
||||
default = null;
|
||||
type = pointerCursorModule;
|
||||
default = { };
|
||||
description = ''
|
||||
Cursor configuration.
|
||||
|
||||
@@ -204,9 +166,10 @@ in
|
||||
let
|
||||
# Check if enable option was explicitly defined by the user
|
||||
enableDefined = any (x: x ? enable) opts.definitions;
|
||||
pointerCursorDefined = opts.highestPrio != (lib.mkOptionDefault { }).priority;
|
||||
|
||||
# Determine if cursor configuration should be enabled
|
||||
enable = if enableDefined then cfg.enable else cfg != null;
|
||||
enable = if enableDefined then cfg.enable else pointerCursorDefined;
|
||||
in
|
||||
mkMerge [
|
||||
(mkIf enable (mkMerge [
|
||||
@@ -240,7 +203,7 @@ in
|
||||
(mkIf cfg.dotIcons.enable {
|
||||
# Add symlink of cursor icon directory to $HOME/.icons, needed for
|
||||
# backwards compatibility with some applications. See:
|
||||
# https://specifications.freedesktop.org/icon-theme-spec/latest/ar01s03.html
|
||||
# https://specifications.freedesktop.org/icon-theme/latest/#directory_layout
|
||||
home.file.".icons/default/index.theme".source =
|
||||
"${defaultIndexThemePackage}/share/icons/default/index.theme";
|
||||
home.file.".icons/${cfg.name}".source = "${cfg.package}/share/icons/${cfg.name}";
|
||||
@@ -285,39 +248,12 @@ in
|
||||
]))
|
||||
|
||||
{
|
||||
warnings =
|
||||
(optional
|
||||
(any
|
||||
(
|
||||
x:
|
||||
getAttrFromPath (
|
||||
[
|
||||
"xsession"
|
||||
"pointerCursor"
|
||||
]
|
||||
++ [ x ]
|
||||
++ [ "isDefined" ]
|
||||
) options
|
||||
)
|
||||
[
|
||||
"package"
|
||||
"name"
|
||||
"size"
|
||||
"defaultCursor"
|
||||
]
|
||||
)
|
||||
''
|
||||
The option `xsession.pointerCursor` has been merged into `home.pointerCursor` and will be removed
|
||||
in the future. Please change to set `home.pointerCursor` directly and enable `home.pointerCursor.x11.enable`
|
||||
to generate x11 specific cursor configurations. You can refer to the documentation for more details.
|
||||
''
|
||||
)
|
||||
++ (optional (opts.highestPrio != (lib.mkOptionDefault { }).priority && cfg == null) ''
|
||||
Setting home.pointerCursor to null is deprecated.
|
||||
Please update your configuration to explicitly set:
|
||||
warnings = optional (pointerCursorDefined && !enableDefined) ''
|
||||
Relying on `home.pointerCursor` to enable cursor config generation is deprecated.
|
||||
Please update your configuration to explicitly set:
|
||||
|
||||
home.pointerCursor.enable = false;
|
||||
'');
|
||||
home.pointerCursor.enable = ${lib.boolToString enable};
|
||||
'';
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
+68
-22
@@ -196,8 +196,8 @@ in
|
||||
targetPath="$HOME/$relativePath"
|
||||
if [[ -e "$targetPath" && ! -L "$targetPath" ]] ; then
|
||||
if [[ -n "$HOME_MANAGER_BACKUP_COMMAND" ]] ; then
|
||||
verboseEcho "Running $HOME_MANAGER_BACKUP_COMMAND $targetPath."
|
||||
run $HOME_MANAGER_BACKUP_COMMAND "$targetPath" || errorEcho "Running `$HOME_MANAGER_BACKUP_COMMAND` on '$targetPath' failed."
|
||||
verboseEcho "Running '$HOME_MANAGER_BACKUP_COMMAND' on '$targetPath'."
|
||||
run $HOME_MANAGER_BACKUP_COMMAND "$targetPath" || errorEcho "Running '$HOME_MANAGER_BACKUP_COMMAND' on '$targetPath' failed."
|
||||
elif [[ -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
|
||||
# The target exists, back it up
|
||||
backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
|
||||
@@ -354,6 +354,63 @@ in
|
||||
# contain the recursion root, not the visited files.
|
||||
declare -A seenTargets
|
||||
|
||||
function setExecutableBit() {
|
||||
local target="$1"
|
||||
local executable="$2"
|
||||
|
||||
if [[ $executable == inherit ]]; then
|
||||
# Don't change file mode if it should match the source.
|
||||
:
|
||||
elif [[ $executable ]]; then
|
||||
chmod +x "$target"
|
||||
else
|
||||
chmod -x "$target"
|
||||
fi
|
||||
}
|
||||
|
||||
function insertFileEntry() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
local executable="$3"
|
||||
local isExecutable
|
||||
|
||||
[[ -x $source ]] && isExecutable=1 || isExecutable=""
|
||||
|
||||
# Link the file into the home file directory if possible,
|
||||
# i.e., if the executable bit of the source is the same we
|
||||
# expect for the target. Otherwise, we copy the file and
|
||||
# set the executable bit to the expected value.
|
||||
if [[ $executable == inherit || $isExecutable == $executable ]]; then
|
||||
ln -s "$source" "$target"
|
||||
else
|
||||
cp "$source" "$target"
|
||||
setExecutableBit "$target" "$executable"
|
||||
fi
|
||||
}
|
||||
|
||||
function setLinkedFileExecutableBit() {
|
||||
local target="$1"
|
||||
local executable="$2"
|
||||
local isExecutable
|
||||
|
||||
if [[ -d $target || ! -e $target ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
[[ -x $target ]] && isExecutable=1 || isExecutable=""
|
||||
|
||||
if [[ $executable == inherit || $isExecutable == $executable ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp "$target.XXXXXX")"
|
||||
|
||||
cp "$target" "$tmp"
|
||||
setExecutableBit "$tmp" "$executable"
|
||||
mv -f "$tmp" "$target"
|
||||
}
|
||||
|
||||
function insertFile() {
|
||||
local source="$1"
|
||||
local relTarget="$2"
|
||||
@@ -409,30 +466,19 @@ in
|
||||
else
|
||||
lndir -silent "$source" "$target"
|
||||
fi
|
||||
|
||||
if [[ $executable != inherit ]]; then
|
||||
local linkedFile
|
||||
|
||||
while IFS= read -r -d "" linkedFile; do
|
||||
setLinkedFileExecutableBit "$linkedFile" "$executable"
|
||||
done < <(find "$target" \( -type f -or -type l \) -print0)
|
||||
fi
|
||||
else
|
||||
ln -s "$source" "$target"
|
||||
fi
|
||||
else
|
||||
[[ -x $source ]] && isExecutable=1 || isExecutable=""
|
||||
|
||||
# Link the file into the home file directory if possible,
|
||||
# i.e., if the executable bit of the source is the same we
|
||||
# expect for the target. Otherwise, we copy the file and
|
||||
# set the executable bit to the expected value.
|
||||
if [[ $executable == inherit || $isExecutable == $executable ]]; then
|
||||
ln -s "$source" "$target"
|
||||
else
|
||||
cp "$source" "$target"
|
||||
|
||||
if [[ $executable == inherit ]]; then
|
||||
# Don't change file mode if it should match the source.
|
||||
:
|
||||
elif [[ $executable ]]; then
|
||||
chmod +x "$target"
|
||||
else
|
||||
chmod -x "$target"
|
||||
fi
|
||||
fi
|
||||
insertFileEntry "$source" "$target" "$executable"
|
||||
fi
|
||||
}
|
||||
''
|
||||
|
||||
@@ -32,7 +32,7 @@ for sourcePath in "$@" ; do
|
||||
warnEcho "Existing file '$targetPath' is in the way of '$sourcePath', will be skipped since they are the same"
|
||||
elif [[ ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_COMMAND" ]] ; then
|
||||
# Next, try to run the custom backup command. Assume this always succeeds.
|
||||
verboseEcho "Existing file '$targetPath' exists and differs from '$sourcePath'. `$HOME_MANAGER_BACKUP_COMMAND` will be used to backup the file."
|
||||
verboseEcho "Existing file '$targetPath' exists and differs from '$sourcePath'. '$HOME_MANAGER_BACKUP_COMMAND' will be used to backup the file."
|
||||
elif [[ ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
|
||||
# Next, try to move the file to a backup location if configured and possible
|
||||
backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
|
||||
|
||||
@@ -184,38 +184,62 @@ in
|
||||
|
||||
options = {
|
||||
home.username = mkOption {
|
||||
type = types.str;
|
||||
defaultText = literalExpression ''
|
||||
"$USER" for state version < 20.09,
|
||||
undefined for state version ≥ 20.09
|
||||
type = types.nonEmptyStr;
|
||||
defaultText = lib.literalMD ''
|
||||
`$USER`, for state version < 20.09,
|
||||
undefined, otherwise.
|
||||
'';
|
||||
example = "jane.doe";
|
||||
description = "The user's username.";
|
||||
description = ''
|
||||
The user's username.
|
||||
|
||||
If Home Manager is installed as a NixOS or nix-darwin submodule, it is
|
||||
set to `osConfig.users.users.<name>.name`.
|
||||
'';
|
||||
};
|
||||
|
||||
home.uid = mkOption {
|
||||
type = types.nullOr types.ints.unsigned;
|
||||
default = null;
|
||||
example = 1000;
|
||||
description = "The user's uid.";
|
||||
description = ''
|
||||
The user's uid.
|
||||
|
||||
If Home Manager is installed as a NixOS or nix-darwin submodule, it is
|
||||
set to `osConfig.users.users.<name>.uid`.
|
||||
'';
|
||||
};
|
||||
|
||||
home.homeDirectory = mkOption {
|
||||
type = types.path;
|
||||
defaultText = literalExpression ''
|
||||
"$HOME" for state version < 20.09,
|
||||
undefined for state version ≥ 20.09
|
||||
defaultText = lib.literalMD ''
|
||||
`$HOME`, for state version < 20.09
|
||||
undefined, otherwise.
|
||||
'';
|
||||
apply = toString;
|
||||
example = "/home/jane.doe";
|
||||
description = "The user's home directory. Must be an absolute path.";
|
||||
description = ''
|
||||
The user's home directory. Must be an absolute path.
|
||||
|
||||
If Home Manager is installed as a NixOS or nix-darwin submodule, it is
|
||||
set to `osConfig.users.users.<name>.home`.
|
||||
'';
|
||||
};
|
||||
|
||||
home.profileDirectory = mkOption {
|
||||
type = types.path;
|
||||
defaultText = literalExpression ''
|
||||
"''${home.homeDirectory}/.nix-profile" or
|
||||
"/etc/profiles/per-user/''${home.username}"
|
||||
default =
|
||||
if config.submoduleSupport.enable && config.submoduleSupport.externalPackageInstall then
|
||||
"/etc/profiles/per-user/${cfg.username}"
|
||||
else if config.nix.useXdg then
|
||||
"${config.xdg.stateHome}/nix/profile"
|
||||
else
|
||||
cfg.homeDirectory + "/.nix-profile";
|
||||
defaultText = lib.literalMD ''
|
||||
- `/etc/profiles/per-user/''${home.username}`, if Home Manager is installed as
|
||||
a submodule and `home-manager.useUserPackages` is enabled, else
|
||||
- `''${xdg.stateHome}/nix/profile`, if `nix.useXdg` is enabled, else
|
||||
- `''${home.homeDirectory}/.nix-profile`.
|
||||
'';
|
||||
readOnly = true;
|
||||
description = ''
|
||||
@@ -569,17 +593,6 @@ in
|
||||
};
|
||||
|
||||
config = {
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.home.username != "";
|
||||
message = "Username could not be determined";
|
||||
}
|
||||
{
|
||||
assertion = config.home.homeDirectory != "";
|
||||
message = "Home directory could not be determined";
|
||||
}
|
||||
];
|
||||
|
||||
warnings =
|
||||
let
|
||||
hmRelease = config.home.version.release;
|
||||
@@ -621,14 +634,6 @@ in
|
||||
lib.mkDefault (builtins.getEnv "HOME")
|
||||
);
|
||||
|
||||
home.profileDirectory =
|
||||
if config.submoduleSupport.enable && config.submoduleSupport.externalPackageInstall then
|
||||
"/etc/profiles/per-user/${cfg.username}"
|
||||
else if config.nix.useXdg then
|
||||
"${config.xdg.stateHome}/nix/profile"
|
||||
else
|
||||
cfg.homeDirectory + "/.nix-profile";
|
||||
|
||||
programs.bash.shellAliases = cfg.shellAliases;
|
||||
programs.zsh.shellAliases = cfg.shellAliases;
|
||||
programs.fish.shellAliases = cfg.shellAliases;
|
||||
|
||||
+260
-38
@@ -14,10 +14,26 @@ let
|
||||
dstDir = "${config.home.homeDirectory}/Library/LaunchAgents";
|
||||
|
||||
launchdConfig =
|
||||
{ name, ... }:
|
||||
{ name, config, ... }:
|
||||
{
|
||||
options = {
|
||||
enable = lib.mkEnableOption name;
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"gui"
|
||||
"user"
|
||||
];
|
||||
default = "gui";
|
||||
example = "user";
|
||||
description = ''
|
||||
The launchd domain to bootstrap this agent into.
|
||||
|
||||
The `gui` domain is appropriate for agents that need the user's
|
||||
Aqua session, such as window managers, hotkey daemons, and other
|
||||
graphical tools. The `user` domain is appropriate for background
|
||||
services that do not require a graphical login session.
|
||||
'';
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = lib.types.submodule (import ./launchd.nix);
|
||||
default = { };
|
||||
@@ -39,9 +55,14 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
config.Label = lib.mkDefault "${labelPrefix}${name}";
|
||||
};
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
config.Label = lib.mkDefault "${labelPrefix}${name}";
|
||||
}
|
||||
(lib.mkIf (config.domain == "user") {
|
||||
config.LimitLoadToSessionType = lib.mkDefault "Background";
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
# mutateConfig calls /bin/sh with /bin/wait4path to wait for /nix/store before
|
||||
@@ -74,6 +95,13 @@ let
|
||||
_n: v: lib.nameValuePair "${v.config.Label}.plist" (toAgent v.config)
|
||||
) (lib.filterAttrs (_n: v: v.enable) cfg.agents);
|
||||
|
||||
agentDomains = lib.mapAttrs' (
|
||||
_n: v:
|
||||
lib.nameValuePair "${v.config.Label}.domain" (
|
||||
pkgs.writeText "${v.config.Label}.domain" "${v.domain}\n"
|
||||
)
|
||||
) (lib.filterAttrs (_n: v: v.enable) cfg.agents);
|
||||
|
||||
agentsDrv = pkgs.runCommand "home-manager-agents" { } ''
|
||||
mkdir -p "$out"
|
||||
|
||||
@@ -87,6 +115,20 @@ let
|
||||
ln -s "$src" "$out/$dest"
|
||||
done
|
||||
'';
|
||||
|
||||
agentDomainsDrv = pkgs.runCommand "home-manager-agent-domains" { } ''
|
||||
mkdir -p "$out"
|
||||
|
||||
declare -A domains
|
||||
domains=(${
|
||||
lib.concatStringsSep " " (lib.mapAttrsToList (name: value: "['${name}']='${value}'") agentDomains)
|
||||
})
|
||||
|
||||
for dest in "''${!domains[@]}"; do
|
||||
src="''${domains[$dest]}"
|
||||
ln -s "$src" "$out/$dest"
|
||||
done
|
||||
'';
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
@@ -129,6 +171,7 @@ in
|
||||
(lib.mkIf isDarwin {
|
||||
home.extraBuilderCommands = ''
|
||||
ln -s "${agentsDrv}" $out/LaunchAgents
|
||||
ln -s "${agentDomainsDrv}" $out/LaunchAgentDomains
|
||||
'';
|
||||
|
||||
# NOTE: Launch Agent configurations can't be symlinked from the Nix store
|
||||
@@ -136,9 +179,53 @@ in
|
||||
home.activation.setupLaunchAgents =
|
||||
lib.hm.dag.entryAfter [ "writeBoundary" ] # Bash
|
||||
''
|
||||
# Get the OS version to see if we can use --wait
|
||||
version="$(/usr/bin/sw_vers --productVersion | cut -d. -f 1)"
|
||||
|
||||
# Disable errexit to ensure we process all agents even if some fail
|
||||
set +e
|
||||
|
||||
readAgentDomain() {
|
||||
local domainsDir="$1"
|
||||
local agentName="$2"
|
||||
local domainFile="$domainsDir/$agentName.domain"
|
||||
|
||||
if [[ -n "$domainsDir" && -f "$domainFile" ]]; then
|
||||
local domainName
|
||||
domainName="$(<"$domainFile")"
|
||||
case "$domainName" in
|
||||
gui|user)
|
||||
printf '%s\n' "$domainName"
|
||||
;;
|
||||
*)
|
||||
printf 'gui\n'
|
||||
;;
|
||||
esac
|
||||
else
|
||||
printf 'gui\n'
|
||||
fi
|
||||
}
|
||||
|
||||
resolveDomain() {
|
||||
local domainName="$1"
|
||||
|
||||
case "$domainName" in
|
||||
gui)
|
||||
printf 'gui/%s\n' "$UID"
|
||||
;;
|
||||
user)
|
||||
printf 'user/%s\n' "$UID"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
agentIsLoaded() {
|
||||
local domain="$1"
|
||||
local agentName="$2"
|
||||
|
||||
run /bin/launchctl print "$domain/$agentName" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Stop an agent if it's running
|
||||
bootoutAgent() {
|
||||
local domain="$1"
|
||||
@@ -146,21 +233,37 @@ in
|
||||
|
||||
verboseEcho "Stopping agent '$domain/$agentName'..."
|
||||
local bootout_output
|
||||
bootout_output=$(run /bin/launchctl bootout --wait "$domain/$agentName" 2>&1) || {
|
||||
# Only show warning if it's not the common "No such process" error
|
||||
if [[ "$bootout_output" != *"No such process"* ]]; then
|
||||
warnEcho "Failed to stop agent '$domain/$agentName': $bootout_output"
|
||||
else
|
||||
verboseEcho "Agent '$domain/$agentName' was not running"
|
||||
|
||||
if [[ "$version" -ge "26" ]]; then
|
||||
if bootout_output=$(run /bin/launchctl bootout --wait "$domain/$agentName" 2>&1); then
|
||||
# --wait already makes sure it was unloaded (BUT we can only use it in >=26)
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
else
|
||||
if bootout_output=$(run /bin/launchctl bootout "$domain/$agentName" 2>&1); then
|
||||
# Otherwise on <26 give the system a moment to fully unload the agent
|
||||
run sleep 1
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# At this point we know exit code is not 0 because otherwise we
|
||||
# would have returned by now
|
||||
|
||||
# Only show warning if it's not a common harmless error
|
||||
if [[ "$bootout_output" != *"No such process"* && "$bootout_output" != *"Domain does not support specified action"* ]]; then
|
||||
warnEcho "Failed to stop agent '$domain/$agentName': $bootout_output"
|
||||
return 1
|
||||
else
|
||||
verboseEcho "Agent '$domain/$agentName' was not running"
|
||||
return 2
|
||||
fi
|
||||
}
|
||||
|
||||
installAndBootstrapAgent() {
|
||||
installAgentFile() {
|
||||
local srcPath="$1"
|
||||
local dstPath="$2"
|
||||
local domain="$3"
|
||||
local agentName="$4"
|
||||
local agentName="$3"
|
||||
|
||||
verboseEcho "Installing agent file to $dstPath"
|
||||
if ! run install -Dm444 -T "$srcPath" "$dstPath"; then
|
||||
@@ -168,11 +271,17 @@ in
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
bootstrapAgent() {
|
||||
local domain="$1"
|
||||
local dstPath="$2"
|
||||
local agentName="$3"
|
||||
|
||||
verboseEcho "Starting agent '$domain/$agentName'"
|
||||
local bootstrap_output
|
||||
bootstrap_output=$(run /bin/launchctl bootstrap "$domain" "$dstPath" 2>&1) || {
|
||||
local error_code=$?
|
||||
|
||||
if [[ "$bootstrap_output" == *"Bootstrap failed: 5: Input/output error"* ]]; then
|
||||
errorEcho "Failed to start agent '$domain/$agentName' with I/O error (code 5)"
|
||||
errorEcho "This typically happens when the agent wasn't unloaded before attempting to bootstrap the new agent."
|
||||
@@ -187,42 +296,120 @@ in
|
||||
return 0
|
||||
}
|
||||
|
||||
restoreAgent() {
|
||||
local oldSrcPath="$1"
|
||||
local dstPath="$2"
|
||||
local oldDomain="$3"
|
||||
local agentName="$4"
|
||||
|
||||
if [[ ! -f "$oldSrcPath" ]]; then
|
||||
warnEcho "Cannot restore previous agent '$oldDomain/$agentName', old file '$oldSrcPath' is missing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
warnEcho "Restoring previous agent '$oldDomain/$agentName'"
|
||||
installAgentFile "$oldSrcPath" "$dstPath" "$agentName" \
|
||||
&& bootstrapAgent "$oldDomain" "$dstPath" "$agentName"
|
||||
}
|
||||
|
||||
processAgent() {
|
||||
local srcPath="$1"
|
||||
local dstDir="$2"
|
||||
local domain="$3"
|
||||
local oldDir="$3"
|
||||
local oldDomainsDir="$4"
|
||||
local newDomainsDir="$5"
|
||||
|
||||
local agentFile="''${srcPath##*/}"
|
||||
local agentName="''${agentFile%.plist}"
|
||||
local dstPath="$dstDir/$agentFile"
|
||||
local oldSrcPath=""
|
||||
local oldDomainName
|
||||
local newDomainName
|
||||
local oldDomain
|
||||
local newDomain
|
||||
local oldAgentBootedOut=0
|
||||
|
||||
# Skip if unchanged
|
||||
if cmp -s "$srcPath" "$dstPath"; then
|
||||
verboseEcho "Agent '$agentName' is already up-to-date"
|
||||
return 0
|
||||
oldDomainName="$(readAgentDomain "$oldDomainsDir" "$agentName")"
|
||||
newDomainName="$(readAgentDomain "$newDomainsDir" "$agentName")"
|
||||
oldDomain="$(resolveDomain "$oldDomainName")"
|
||||
newDomain="$(resolveDomain "$newDomainName")"
|
||||
if [[ -n "$oldDir" ]]; then
|
||||
oldSrcPath="$oldDir/$agentFile"
|
||||
fi
|
||||
|
||||
verboseEcho "Processing agent '$agentName'"
|
||||
# Skip if unchanged
|
||||
if cmp -s "$srcPath" "$dstPath" && [[ "$oldDomainName" == "$newDomainName" ]]; then
|
||||
if agentIsLoaded "$newDomain" "$agentName"; then
|
||||
verboseEcho "Agent '$newDomain/$agentName' is already up-to-date"
|
||||
return 0
|
||||
else
|
||||
verboseEcho "Agent '$newDomain/$agentName' is up-to-date but not loaded"
|
||||
fi
|
||||
fi
|
||||
|
||||
verboseEcho "Processing agent '$newDomain/$agentName'"
|
||||
|
||||
# Stop/Unload agent if it's already running
|
||||
if [[ -f "$dstPath" ]]; then
|
||||
bootoutAgent "$domain" "$agentName"
|
||||
bootoutAgent "$oldDomain" "$agentName"
|
||||
case "$?" in
|
||||
0)
|
||||
oldAgentBootedOut=1
|
||||
;;
|
||||
2)
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
installAndBootstrapAgent "$srcPath" "$dstPath" "$domain" "$agentName"
|
||||
# Note: We continue processing even if this agent fails
|
||||
return 0
|
||||
if [[ "$oldDomainName" != "$newDomainName" ]]; then
|
||||
bootoutAgent "$newDomain" "$agentName"
|
||||
case "$?" in
|
||||
0|2)
|
||||
;;
|
||||
*)
|
||||
if [[ "$oldAgentBootedOut" -eq 1 ]]; then
|
||||
restoreAgent "$oldSrcPath" "$dstPath" "$oldDomain" "$agentName"
|
||||
fi
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! installAgentFile "$srcPath" "$dstPath" "$agentName"; then
|
||||
if [[ "$oldAgentBootedOut" -eq 1 ]]; then
|
||||
restoreAgent "$oldSrcPath" "$dstPath" "$oldDomain" "$agentName"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
if bootstrapAgent "$newDomain" "$dstPath" "$agentName"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$oldAgentBootedOut" -eq 1 ]]; then
|
||||
restoreAgent "$oldSrcPath" "$dstPath" "$oldDomain" "$agentName"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
removeAgent() {
|
||||
local srcPath="$1"
|
||||
local dstDir="$2"
|
||||
local newDir="$3"
|
||||
local domain="$4"
|
||||
local oldDomainsDir="$4"
|
||||
|
||||
local agentFile="''${srcPath##*/}"
|
||||
local agentName="''${agentFile%.plist}"
|
||||
local dstPath="$dstDir/$agentFile"
|
||||
local domainName
|
||||
local domain
|
||||
|
||||
domainName="$(readAgentDomain "$oldDomainsDir" "$agentName")"
|
||||
domain="$(resolveDomain "$domainName")"
|
||||
|
||||
if [[ -e "$newDir/$agentFile" ]]; then
|
||||
verboseEcho "Agent '$agentName' still exists in new generation, skipping cleanup"
|
||||
@@ -241,23 +428,32 @@ in
|
||||
|
||||
# Stop and remove the agent
|
||||
bootoutAgent "$domain" "$agentName"
|
||||
case "$?" in
|
||||
0|2)
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
verboseEcho "Removing agent file '$dstPath'"
|
||||
if run rm -f $VERBOSE_ARG "$dstPath"; then
|
||||
verboseEcho "Successfully removed agent file for '$agentName'"
|
||||
else
|
||||
warnEcho "Failed to remove agent file '$dstPath'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
setupLaunchAgents() {
|
||||
local oldDir newDir dstDir domain
|
||||
local oldDir newDir oldDomainsDir newDomainsDir dstDir launchdStatus
|
||||
|
||||
newDir="$(readlink -m "$newGenPath/LaunchAgents")"
|
||||
newDomainsDir="$(readlink -m "$newGenPath/LaunchAgentDomains")"
|
||||
dstDir=${lib.escapeShellArg dstDir}
|
||||
domain="gui/$UID"
|
||||
launchdStatus=0
|
||||
|
||||
if [[ -n "''${oldGenPath:-}" ]]; then
|
||||
oldDir="$(readlink -m "$oldGenPath/LaunchAgents")"
|
||||
@@ -265,34 +461,60 @@ in
|
||||
verboseEcho "No previous LaunchAgents directory found"
|
||||
oldDir=""
|
||||
fi
|
||||
|
||||
oldDomainsDir="$(readlink -m "$oldGenPath/LaunchAgentDomains")"
|
||||
if [[ ! -d "$oldDomainsDir" ]]; then
|
||||
oldDomainsDir=""
|
||||
fi
|
||||
else
|
||||
oldDir=""
|
||||
oldDomainsDir=""
|
||||
fi
|
||||
|
||||
verboseEcho "Setting up LaunchAgents in $dstDir"
|
||||
[[ -d "$dstDir" ]] || run mkdir -p "$dstDir"
|
||||
if [[ ! -d "$dstDir" ]] && ! run mkdir -p "$dstDir"; then
|
||||
errorEcho "Failed to create LaunchAgents directory '$dstDir'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
verboseEcho "Processing new/updated LaunchAgents..."
|
||||
find -L "$newDir" -maxdepth 1 -name '*.plist' -type f | while read -r srcPath; do
|
||||
processAgent "$srcPath" "$dstDir" "$domain"
|
||||
done
|
||||
while IFS= read -r srcPath; do
|
||||
processAgent "$srcPath" "$dstDir" "$oldDir" "$oldDomainsDir" "$newDomainsDir" \
|
||||
|| launchdStatus=1
|
||||
done < <(find -L "$newDir" -maxdepth 1 -name '*.plist' -type f)
|
||||
|
||||
# Skip cleanup if there's no previous generation
|
||||
if [[ -z "$oldDir" || ! -d "$oldDir" ]]; then
|
||||
if [[ "$launchdStatus" -ne 0 ]]; then
|
||||
errorEcho "Failed to activate one or more LaunchAgents"
|
||||
fi
|
||||
|
||||
verboseEcho "LaunchAgents setup complete"
|
||||
return
|
||||
return "$launchdStatus"
|
||||
fi
|
||||
|
||||
verboseEcho "Cleaning up removed LaunchAgents..."
|
||||
find -L "$oldDir" -maxdepth 1 -name '*.plist' -type f | while read -r srcPath; do
|
||||
removeAgent "$srcPath" "$dstDir" "$newDir" "$domain"
|
||||
done
|
||||
while IFS= read -r srcPath; do
|
||||
removeAgent "$srcPath" "$dstDir" "$newDir" "$oldDomainsDir" \
|
||||
|| launchdStatus=1
|
||||
done < <(find -L "$oldDir" -maxdepth 1 -name '*.plist' -type f)
|
||||
|
||||
if [[ "$launchdStatus" -ne 0 ]]; then
|
||||
errorEcho "Failed to activate one or more LaunchAgents"
|
||||
fi
|
||||
|
||||
return "$launchdStatus"
|
||||
}
|
||||
|
||||
setupLaunchAgents
|
||||
launchdStatus=0
|
||||
setupLaunchAgents || launchdStatus=$?
|
||||
|
||||
# Restore errexit
|
||||
set -e
|
||||
|
||||
if [[ "$launchdStatus" -ne 0 ]]; then
|
||||
exit "$launchdStatus"
|
||||
fi
|
||||
'';
|
||||
})
|
||||
];
|
||||
|
||||
@@ -15,6 +15,8 @@ let
|
||||
removePrefix
|
||||
types
|
||||
;
|
||||
isAbsolutePath = hasPrefix "/";
|
||||
removeHomePrefix = removePrefix (homeDirectory + "/");
|
||||
in
|
||||
{
|
||||
# Constructs a type suitable for a `home.file` like option. The
|
||||
@@ -46,9 +48,9 @@ in
|
||||
apply =
|
||||
p:
|
||||
let
|
||||
absPath = if hasPrefix "/" p then p else "${basePath}/${p}";
|
||||
absPath = if isAbsolutePath p then p else "${basePath}/${p}";
|
||||
in
|
||||
removePrefix (homeDirectory + "/") absPath;
|
||||
removeHomePrefix absPath;
|
||||
defaultText = literalExpression "name";
|
||||
description = ''
|
||||
Path to target file relative to ${basePathDesc}.
|
||||
|
||||
@@ -744,7 +744,9 @@ in
|
||||
toHyprconf' initialIndent attrs;
|
||||
|
||||
toKDL =
|
||||
_:
|
||||
{
|
||||
escapeBackslashes ? false,
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
concatStringsSep
|
||||
@@ -766,7 +768,22 @@ in
|
||||
stringsWithNewlines: indentAll (unlines (lines stringsWithNewlines));
|
||||
|
||||
# String -> String
|
||||
sanitizeString = replaceStrings [ "\n" ''"'' ] [ "\\n" ''\"'' ];
|
||||
sanitizeString =
|
||||
replaceStrings
|
||||
(
|
||||
[
|
||||
"\n"
|
||||
''"''
|
||||
]
|
||||
++ (lib.optional escapeBackslashes "\\")
|
||||
)
|
||||
(
|
||||
[
|
||||
"\\n"
|
||||
''\"''
|
||||
]
|
||||
++ (lib.optional escapeBackslashes "\\\\")
|
||||
);
|
||||
|
||||
# OneOf [Int Float String Bool Null] -> String
|
||||
literalValueToString =
|
||||
|
||||
+13
-25
@@ -7,30 +7,18 @@
|
||||
# [1] https://github.com/NixOS/nixpkgs/blob/737449404589e4a80b3fa99ecbc6d4d803c1f6dc/maintainers/maintainer-list.nix#LC1
|
||||
{
|
||||
# keep-sorted start case=no numeric=no block=yes
|
||||
"3ulalia" = {
|
||||
_3ulalia = {
|
||||
name = "Eulalia del Sol";
|
||||
email = "3ulalia@proton.me";
|
||||
github = "3ulalia";
|
||||
githubId = 179992797;
|
||||
};
|
||||
"74k1" = {
|
||||
name = "Tim";
|
||||
email = "git.t@betsumei.com";
|
||||
github = "74k1";
|
||||
githubId = 49000471;
|
||||
};
|
||||
"9p4" = {
|
||||
_9p4 = {
|
||||
name = "9p4";
|
||||
email = "vcs@ersei.net";
|
||||
github = "9p4";
|
||||
githubId = 17993169;
|
||||
};
|
||||
"will-lol" = {
|
||||
name = "William Bradshaw";
|
||||
email = "will.bradshaw50@gmail.com";
|
||||
github = "will-lol";
|
||||
githubId = 65345408;
|
||||
};
|
||||
aabccd021 = {
|
||||
name = "Muhamad Abdurahman";
|
||||
email = "aabccd021@gmail.com";
|
||||
@@ -529,6 +517,12 @@
|
||||
}
|
||||
];
|
||||
};
|
||||
reesilva = {
|
||||
name = "Renato da Silva";
|
||||
email = "reesilva@proton.me";
|
||||
github = "ReeSilva";
|
||||
githubId = 3605032;
|
||||
};
|
||||
rosuavio = {
|
||||
name = "Rosario Pulella";
|
||||
email = "RosarioPulella@gmail.com";
|
||||
@@ -547,12 +541,6 @@
|
||||
github = "rszamszur";
|
||||
githubId = 10353018;
|
||||
};
|
||||
semi710 = {
|
||||
name = "Nikhil Singh";
|
||||
email = "nik.singh710@gmail.com";
|
||||
github = "semi710";
|
||||
githubId = 60490474;
|
||||
};
|
||||
silmarp = {
|
||||
name = "Silmar Pereira da Silva Junior";
|
||||
email = "silmarjr2@gmail.com";
|
||||
@@ -576,11 +564,11 @@
|
||||
github = "superflash41";
|
||||
githubId = 102434258;
|
||||
};
|
||||
yarn = {
|
||||
name = "yarncat";
|
||||
email = "30006414+yaaaarn@users.noreply.github.com";
|
||||
github = "yaaaarn";
|
||||
githubId = 30006414;
|
||||
will-lol = {
|
||||
name = "William Bradshaw";
|
||||
email = "will.bradshaw50@gmail.com";
|
||||
github = "will-lol";
|
||||
githubId = 65345408;
|
||||
};
|
||||
zorrobert = {
|
||||
name = "zorrobert";
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
time = "2024-03-28T17:02:19+00:00";
|
||||
condition = pkgs.stdenv.hostPlatform.isLinux;
|
||||
message = ''
|
||||
|
||||
A new module is available: 'services.amberol'.
|
||||
'';
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
time = "2025-01-02T11:21:19+00:00";
|
||||
condition = true;
|
||||
message = ''
|
||||
A new module is available: 'services.mpdscribble'.
|
||||
|
||||
A MPD client which submits information about tracks being played to a
|
||||
scrobbler (e.g. last.fm)
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-06-13T11:54:31+00:00";
|
||||
condition = config.programs.swayimg.enable;
|
||||
message = ''
|
||||
`programs.swayimg.settings` has been replaced with
|
||||
`programs.swayimg.initLua` because upstream moved to a lua config.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
time = "2026-06-17T14:09:44+00:00";
|
||||
condition = pkgs.stdenv.hostPlatform.isLinux;
|
||||
message = ''
|
||||
A new module is available: `services.nvibrant`.
|
||||
|
||||
[nvibrant] is used for configuring NVIDIA's "Digital Vibrance" on Wayland.
|
||||
|
||||
[nvibrant]: https://github.com/tremeschin/nvibrant
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
time = "2026-06-18T02:02:00+00:00";
|
||||
condition = pkgs.stdenv.hostPlatform.isDarwin && config.launchd.enable;
|
||||
message = ''
|
||||
|
||||
Home Manager launchd agents now support the
|
||||
`launchd.agents.<name>.domain` option. Agents use the GUI launchd domain by
|
||||
default. Set `launchd.agents.<name>.domain = "user"` for agents that should
|
||||
run without an active graphical login session.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-06-18T13:00:50+00:00";
|
||||
condition =
|
||||
config.services.podman.enable
|
||||
&& (
|
||||
config.services.podman.settings.registries.insecure != [ ]
|
||||
|| config.services.podman.settings.registries.block != [ ]
|
||||
);
|
||||
message = ''
|
||||
`services.podman.settings.registries` now generates `registries.conf` v2
|
||||
format.
|
||||
|
||||
The new per-registry option
|
||||
{option}`services.podman.settings.registries.registry` was added. Use
|
||||
entries like `{ location = "registry.example"; insecure = true; }` or
|
||||
`{ location = "registry.example"; blocked = true; }`.
|
||||
|
||||
Legacy {option}`services.podman.settings.registries.insecure` and
|
||||
{option}`services.podman.settings.registries.block` are still supported
|
||||
but deprecated, and now emit a warning.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-06-19T13:24:15+00:00";
|
||||
condition = config.programs.hyprland-qt-support.enable;
|
||||
message = ''
|
||||
A new module is available: `programs.hyprland-qt-support`.
|
||||
|
||||
This module enables Hyprland Qt support — a Qt6 QML style provider for
|
||||
hypr* apps. It can be configured through the `settings` attribute.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
time = "2026-06-22T19:16:41+00:00";
|
||||
condition = true;
|
||||
message = ''
|
||||
A new {option}`programs.devenv` module has been added to Home Manager.
|
||||
|
||||
devenv is a tool for fast, declarative, reproducible and composable
|
||||
developer environments using Nix. The module provides options to enable
|
||||
devenv and configure shell integration for Bash, Fish, Nushell, and Zsh.
|
||||
|
||||
To use it, set {option}`programs.devenv.enable` to `true`. Shell
|
||||
integration is enabled by default for installed shells.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-06-22T13:02:38+00:00";
|
||||
condition = config.programs.zsh.enable;
|
||||
message = ''
|
||||
A new module has been added: `programs.zsh.fastSyntaxHighlighting`.
|
||||
This is an alternative implementation of syntax highlighting for Zsh.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-06-25T01:43:35+00:00";
|
||||
condition = config.programs.codex.enable;
|
||||
message = ''
|
||||
The `programs.codex.contextOverride` option was added to manage Codex
|
||||
override context under `CODEX_HOME`.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
time = "2026-06-27T12:17:31+00:00";
|
||||
condition = true;
|
||||
message = ''
|
||||
A new module is available: 'programs.herdr'.
|
||||
|
||||
Herdr is a terminal workspace and agent manager. The new module lets
|
||||
you install it and declaratively configure its TOML config file.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-07-01T23:57:59+00:00";
|
||||
condition =
|
||||
config.programs.atuin.enable
|
||||
&& config.programs.atuin.enableNushellIntegration
|
||||
&& config.programs.nushell.enable;
|
||||
message = ''
|
||||
Home Manager now loads the Atuin Nushell integration after other shell
|
||||
integrations such as fzf. This restores Atuin's Ctrl-R binding when both
|
||||
programs are enabled.
|
||||
|
||||
If you previously used `programs.nushell.extraConfig = lib.mkAfter ...`
|
||||
to place custom Nushell configuration after Atuin, use a later order such
|
||||
as `lib.mkOrder 2001 ...` instead.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
time = "2026-07-06T00:46:00+00:00";
|
||||
condition = true;
|
||||
message = ''
|
||||
A new module is available: `programs.attic-client`.
|
||||
|
||||
It manages the client configuration for the Attic Nix binary cache.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
time = "2026-07-08T20:43:23+00:00";
|
||||
condition = true;
|
||||
message = ''
|
||||
A new module is available: 'programs.posting'.
|
||||
|
||||
Posting is an open-source terminal-based API client for developing
|
||||
and testing APIs. The module supports configuration via
|
||||
`programs.posting.settings` and custom themes via
|
||||
`programs.posting.themes`.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
time = "2026-07-15T21:56:32+00:00";
|
||||
condition = pkgs.stdenv.hostPlatform.isDarwin;
|
||||
message = ''
|
||||
The config path for 'senpai' on Darwin now defaults to:
|
||||
~/Library/Application Support/senpai/senpai.scfg
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
time = "2026-07-19T13:55:33+00:00";
|
||||
condition = pkgs.stdenv.hostPlatform.isDarwin;
|
||||
message = ''
|
||||
'programs.rclone' mounts now support a 'mountType' option that can be
|
||||
set to "nfsmount" to mount remotes via rclone's built-in NFS server
|
||||
and the system NFS client instead of FUSE. On macOS this removes the
|
||||
need for macFUSE and its kernel extension (and the Reduced Security
|
||||
boot policy the extension requires). When "nfsmount" is selected,
|
||||
'nfs-cache-type = "disk"' is added to the mount's options by default
|
||||
so NFS file handles survive rclone restarts.
|
||||
'';
|
||||
}
|
||||
@@ -42,7 +42,7 @@ in
|
||||
example = "set -x SSH_AUTH_SOCK $HOME/.ssh/agent.sock";
|
||||
};
|
||||
nushell = mkShellInitOption "nushell" // {
|
||||
example = "$env.SSH_AUTH_SOCK = $HOME/.ssh/agent.sock";
|
||||
example = "$env.SSH_AUTH_SOCK = ($nu.home-dir | path join .ssh agent.sock)";
|
||||
};
|
||||
zsh = mkShellInitOption "zsh" // {
|
||||
example = "export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock";
|
||||
@@ -67,18 +67,25 @@ in
|
||||
|
||||
config =
|
||||
let
|
||||
indentNonEmptyLines = lib.flip lib.pipe [
|
||||
(lib.splitString "\n")
|
||||
(map (line: if line == "" then "" else " ${line}"))
|
||||
lib.concatLines
|
||||
(lib.removeSuffix "\n")
|
||||
];
|
||||
|
||||
# Preserve $SSH_AUTH_SOCK if it stems from a forwarded agent which is the
|
||||
# case if both $SSH_AUTH_SOCK and $SSH_CONNECTION are set.
|
||||
mkShIntegration = code: ''
|
||||
if [ -z "$SSH_AUTH_SOCK" -o -z "$SSH_CONNECTION" ]; then
|
||||
${code}
|
||||
${indentNonEmptyLines code}
|
||||
fi
|
||||
'';
|
||||
bashIntegration = mkShIntegration cfg.initialization.bash;
|
||||
zshIntegration = mkShIntegration cfg.initialization.zsh;
|
||||
fishIntegration = ''
|
||||
if test -z "$SSH_AUTH_SOCK"; or test -z "$SSH_CONNECTION"
|
||||
${cfg.initialization.fish}
|
||||
${indentNonEmptyLines cfg.initialization.fish}
|
||||
end
|
||||
'';
|
||||
nushellIntegration =
|
||||
@@ -87,7 +94,7 @@ in
|
||||
in
|
||||
''
|
||||
if ${unsetOrEmpty "SSH_AUTH_SOCK"} or ${unsetOrEmpty "SSH_CONNECTION"} {
|
||||
${cfg.initialization.nushell}
|
||||
${indentNonEmptyLines cfg.initialization.nushell}
|
||||
}
|
||||
'';
|
||||
in
|
||||
@@ -96,7 +103,17 @@ in
|
||||
programs.bash.profileExtra = lib.mkOrder 900 bashIntegration;
|
||||
programs.fish.shellInit = lib.mkOrder 900 fishIntegration;
|
||||
programs.nushell.extraConfig = lib.mkOrder 900 nushellIntegration;
|
||||
programs.zsh.envExtra = lib.mkOrder 900 zshIntegration;
|
||||
programs.zsh = {
|
||||
# Mimic how `home.sessionVariablesPackage` is sourced in the Zsh module
|
||||
# to ensure that session variables which SSH_AUTH_SOCK might rely on are
|
||||
# set.
|
||||
envExtra = lib.mkOrder 900 ''
|
||||
if [[ ! -o login ]]; then
|
||||
${indentNonEmptyLines zshIntegration}
|
||||
fi
|
||||
'';
|
||||
profileExtra = lib.mkOrder 900 zshIntegration;
|
||||
};
|
||||
|
||||
# Replace this service by an environment generator as soon as they are
|
||||
# available per-user. See https://github.com/systemd/systemd/issues/32423
|
||||
|
||||
+2
-4
@@ -16,6 +16,7 @@
|
||||
}:
|
||||
|
||||
let
|
||||
hasNixSuffix = lib.hasSuffix ".nix";
|
||||
|
||||
modules = builtins.concatLists [
|
||||
[
|
||||
@@ -86,10 +87,7 @@ let
|
||||
(
|
||||
dir:
|
||||
lib.pipe (builtins.readDir dir) [
|
||||
(lib.filterAttrs (path: _kind: !lib.hasPrefix "_" path))
|
||||
(lib.filterAttrs (
|
||||
_path: kind: kind == "directory" || (kind == "regular" && lib.hasSuffix ".nix" _path)
|
||||
))
|
||||
(lib.filterAttrs (path: kind: kind == "directory" || (kind == "regular" && hasNixSuffix path)))
|
||||
(lib.mapAttrsToList (path: _kind: lib.path.append dir path))
|
||||
]
|
||||
)
|
||||
|
||||
+5
-5
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: Home Manager Modules\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nix-community/home-manager/issues\n"
|
||||
"POT-Creation-Date: 2026-01-21 09:53+0100\n"
|
||||
"PO-Revision-Date: 2026-06-12 10:01+0000\n"
|
||||
"Last-Translator: Tommaso Soncin <soncintommaso@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-07-19 08:01+0000\n"
|
||||
"Last-Translator: Artur Sannikov <weblate.rbpxz@8shield.net>\n"
|
||||
"Language-Team: Italian <https://hosted.weblate.org/projects/home-manager/"
|
||||
"modules/it/>\n"
|
||||
"Language: it\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.7.dev0\n"
|
||||
"X-Generator: Weblate 2026.8.dev0\n"
|
||||
|
||||
#: modules/files.nix:214
|
||||
msgid "Creating home file links in %s"
|
||||
@@ -93,11 +93,11 @@ msgstr "Iniziando attivazione Home Manager"
|
||||
|
||||
#: modules/lib-bash/activation-init.sh:142
|
||||
msgid "Sanity checking Nix"
|
||||
msgstr "Controllando Nix"
|
||||
msgstr "Verifica di correttezza di Nix"
|
||||
|
||||
#: modules/lib-bash/activation-init.sh:155
|
||||
msgid "This is a dry run"
|
||||
msgstr "Questo è un avvio secco"
|
||||
msgstr "Questa è una prova generale"
|
||||
|
||||
#: modules/lib-bash/activation-init.sh:159
|
||||
msgid "This is a live run"
|
||||
|
||||
+4
-4
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: Home Manager Modules\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nix-community/home-manager/issues\n"
|
||||
"POT-Creation-Date: 2026-01-21 09:53+0100\n"
|
||||
"PO-Revision-Date: 2026-05-05 07:09+0000\n"
|
||||
"Last-Translator: Xen'on <saveliyburk@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-07-19 08:01+0000\n"
|
||||
"Last-Translator: Artur Sannikov <weblate.rbpxz@8shield.net>\n"
|
||||
"Language-Team: Russian <https://hosted.weblate.org/projects/home-manager/"
|
||||
"modules/ru/>\n"
|
||||
"Language: ru\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"X-Generator: Weblate 5.17.1\n"
|
||||
"X-Generator: Weblate 2026.8.dev0\n"
|
||||
|
||||
#: modules/files.nix:214
|
||||
msgid "Creating home file links in %s"
|
||||
@@ -84,7 +84,7 @@ msgstr "Не найдена подходящая директория профи
|
||||
#: modules/lib-bash/activation-init.sh:109
|
||||
#: modules/lib-bash/activation-init.sh:121
|
||||
msgid "%s is \"%s\", expected \"%s\""
|
||||
msgstr ""
|
||||
msgstr "%s равно «%s», ожидалось «%s»"
|
||||
|
||||
#: modules/lib-bash/activation-init.sh:138
|
||||
msgid "Starting Home Manager activation"
|
||||
|
||||
@@ -96,27 +96,37 @@ in
|
||||
};
|
||||
|
||||
xdg.configFile."alacritty/alacritty.toml" = lib.mkIf (cfg.settings != { }) {
|
||||
source = (tomlFormat.generate "alacritty.toml" cfg.settings).overrideAttrs (
|
||||
_finalAttrs: prevAttrs: {
|
||||
buildCommand = lib.concatStringsSep "\n" [
|
||||
prevAttrs.buildCommand
|
||||
# Nix cannot spell TOML escape sequences like "\u001d" directly:
|
||||
# "\\u001d" is a literal backslash-u string, while fromJSON creates
|
||||
# a control character and fails for "\u0000". Normalize both TOML
|
||||
# generator outputs to the Alacritty escape form:
|
||||
# chars = "\\u001d" -> chars = "\u001d"
|
||||
# chars = '\u001d' -> chars = "\u001d"
|
||||
''
|
||||
sed \
|
||||
-E \
|
||||
-e "s/= \"\\\\\\\\u([0-9a-fA-F]{4})\"\$/= \"\\\\u\1\"/" \
|
||||
-e "s/= '\\\\u([0-9a-fA-F]{4})'\$/= \"\\\\u\1\"/" \
|
||||
"$out" > "$TMPDIR/alacritty.toml"
|
||||
cp "$TMPDIR/alacritty.toml" "$out"
|
||||
''
|
||||
];
|
||||
}
|
||||
);
|
||||
source =
|
||||
let
|
||||
# `pkgs.formats.toml` escapes backslashes and cannot emit raw
|
||||
# control characters (NUL cannot even appear in a Nix string), so a
|
||||
# `chars` escape like "\u001d" generates as literal "\\u001d",
|
||||
# which Alacritty rejects. Stash each escape behind a placeholder that
|
||||
# survives generation, then restore "\u" in the build command.
|
||||
unicodeEscapePrefix = "__home_manager_alacritty_unicode_escape_";
|
||||
|
||||
normalizeAlacrittyEscapes =
|
||||
value:
|
||||
if builtins.isAttrs value then
|
||||
lib.mapAttrs (_: normalizeAlacrittyEscapes) value
|
||||
else if builtins.isList value then
|
||||
map normalizeAlacrittyEscapes value
|
||||
else if !builtins.isString value then
|
||||
value
|
||||
else
|
||||
# Normalize the "\^[" caret form to "\u001b", then replace every
|
||||
# "\uXXXX" with the placeholder.
|
||||
lib.concatMapStrings (
|
||||
chunk: if builtins.isList chunk then unicodeEscapePrefix + builtins.head chunk else chunk
|
||||
) (builtins.split "\\\\u([0-9a-fA-F]{4})" (builtins.replaceStrings [ "\\^[" ] [ "\\u001b" ] value));
|
||||
in
|
||||
(tomlFormat.generate "alacritty.toml" (normalizeAlacrittyEscapes cfg.settings)).overrideAttrs
|
||||
(prevAttrs: {
|
||||
buildCommand = lib.concatStringsSep "\n" [
|
||||
prevAttrs.buildCommand
|
||||
''substituteInPlace $out --replace-quiet '${unicodeEscapePrefix}' '\u' ''
|
||||
];
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
mkOption
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.programs.attic-client;
|
||||
|
||||
settingsFormat = pkgs.formats.toml { };
|
||||
in
|
||||
{
|
||||
options.programs.attic-client = {
|
||||
enable = lib.mkEnableOption "the attic binary cache client";
|
||||
|
||||
package = lib.mkPackageOption pkgs "attic-client" {
|
||||
nullable = true;
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
inherit (settingsFormat) type;
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
default-server = "myserver";
|
||||
servers.myserver = {
|
||||
endpoint = "https://myserver.org";
|
||||
token-file = "/run/secrets/attic-token";
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Configuration written to
|
||||
{file}`$XDG_CONFIG_HOME/attic/config.toml`.
|
||||
|
||||
See <https://github.com/zhaofengli/attic> for the available options.
|
||||
'';
|
||||
};
|
||||
|
||||
watchStore = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "mycacheserver:mycache" ];
|
||||
description = ''
|
||||
Caches to push new store paths to via `attic watch-store`.
|
||||
Format: `server:cache` (or just `cache` to use the default server).
|
||||
|
||||
This relies on systemd and is only supported on Linux.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.watchStore == [ ] || pkgs.stdenv.hostPlatform.isLinux;
|
||||
message = "programs.attic-client.watchStore requires systemd and is only supported on Linux.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.watchStore == [ ] || cfg.package != null;
|
||||
message = "programs.attic-client.watchStore requires programs.attic-client.package to be set.";
|
||||
}
|
||||
];
|
||||
|
||||
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
|
||||
xdg.configFile."attic/config.toml" = lib.mkIf (cfg.settings != { }) {
|
||||
source = settingsFormat.generate "attic-config.toml" cfg.settings;
|
||||
};
|
||||
|
||||
systemd.user.services = lib.mkIf (cfg.watchStore != [ ]) (
|
||||
lib.listToAttrs (
|
||||
map (
|
||||
cache:
|
||||
lib.nameValuePair "attic-watch-store--${lib.replaceStrings [ ":" ] [ "-" ] cache}" {
|
||||
Unit.Description = "Push new store paths to the attic cache ${cache}";
|
||||
|
||||
Install.WantedBy = [ "default.target" ];
|
||||
|
||||
Service = {
|
||||
ExecStart = "${lib.getExe cfg.package} watch-store ${cache}";
|
||||
Restart = "always";
|
||||
RestartSec = 30;
|
||||
};
|
||||
}
|
||||
) cfg.watchStore
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
meta.maintainers = [ lib.maintainers.swarsel ];
|
||||
}
|
||||
@@ -20,7 +20,11 @@ let
|
||||
else
|
||||
[ "daemon" ];
|
||||
|
||||
inherit (lib) mkIf mkOption types;
|
||||
inherit (lib)
|
||||
mkIf
|
||||
mkOption
|
||||
types
|
||||
;
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
@@ -224,7 +228,8 @@ in
|
||||
'';
|
||||
|
||||
programs.nushell = mkIf cfg.enableNushellIntegration {
|
||||
extraConfig = ''
|
||||
# Load after fzf so Atuin keeps Ctrl-R in Nushell.
|
||||
extraConfig = lib.mkOrder 2000 ''
|
||||
source ${
|
||||
pkgs.runCommand "atuin-nushell-config.nu"
|
||||
{
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkChangedOptionModule;
|
||||
|
||||
cfg = config.programs.claude-code;
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
claudeCodeLib = import ./lib.nix { inherit lib pkgs; };
|
||||
|
||||
packageVersion = if cfg.package == null then null else lib.getVersion cfg.package;
|
||||
hasPackageVersion = packageVersion != null && packageVersion != "";
|
||||
|
||||
# A null package has no detectable version, so assume the latest Claude Code
|
||||
# and enable version-gated behavior by default. Preserve the legacy wrapper
|
||||
# for non-null packages without version metadata.
|
||||
atLeast =
|
||||
version: cfg.package == null || (hasPackageVersion && lib.versionAtLeast packageVersion version);
|
||||
supportsPluginDir = !hasPackageVersion || atLeast "2.1.76";
|
||||
supportsPersonalPlugins = atLeast "2.1.157";
|
||||
|
||||
upstreamConfigDir = "${config.home.homeDirectory}/.claude";
|
||||
|
||||
isMcpServerEnabled =
|
||||
server:
|
||||
let
|
||||
enabled = server.enabled or null;
|
||||
disabled = (server.disabled or false) == true;
|
||||
in
|
||||
enabled != false && !disabled;
|
||||
|
||||
transformMcpServer =
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
exclude = [ "enabled" ];
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; }) # envFiles currently still need wrapping https://github.com/anthropics/claude-code/issues/28942
|
||||
];
|
||||
};
|
||||
|
||||
# Shared MCP servers participate only when both integrations are enabled.
|
||||
sharedMcpServers = lib.optionalAttrs (
|
||||
cfg.enableMcpIntegration && config.programs.mcp.enable
|
||||
) config.programs.mcp.servers;
|
||||
|
||||
transformedMcpServers = lib.mapAttrs transformMcpServer sharedMcpServers;
|
||||
|
||||
disabledMcpServerNames = lib.attrNames (
|
||||
lib.filterAttrs (_: server: !(isMcpServerEnabled server)) (sharedMcpServers // cfg.mcpServers)
|
||||
);
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.khaneliman ];
|
||||
|
||||
imports = [
|
||||
(mkChangedOptionModule
|
||||
[ "programs" "claude-code" "memory" "text" ]
|
||||
[ "programs" "claude-code" "context" ]
|
||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "text" ] config)
|
||||
)
|
||||
(mkChangedOptionModule
|
||||
[ "programs" "claude-code" "memory" "source" ]
|
||||
[ "programs" "claude-code" "context" ]
|
||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "source" ] config)
|
||||
)
|
||||
(mkChangedOptionModule
|
||||
[ "programs" "claude-code" "skillsDir" ]
|
||||
[ "programs" "claude-code" "skills" ]
|
||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "skillsDir" ] config)
|
||||
)
|
||||
./options.nix
|
||||
];
|
||||
|
||||
config =
|
||||
let
|
||||
helpers = claudeCodeLib.mkHelpers { inherit (cfg) configDir; };
|
||||
inherit (helpers)
|
||||
mkHookEntries
|
||||
mkInstalledMarketplaceEntry
|
||||
mkMarkdownEntries
|
||||
mkMarketplaceEntry
|
||||
mkPluginEntry
|
||||
mkRecursiveDirAttrs
|
||||
mkSkillEntry
|
||||
;
|
||||
|
||||
mergedMcpServers =
|
||||
transformedMcpServers
|
||||
// lib.mapAttrs (_: server: removeAttrs (lib.hm.mcp.addType server) [ "enabled" ]) cfg.mcpServers;
|
||||
|
||||
generatedPluginFiles =
|
||||
lib.optional (mergedMcpServers != { }) {
|
||||
name = ".mcp.json";
|
||||
path = jsonFormat.generate "claude-code-mcp.json" { mcpServers = mergedMcpServers; };
|
||||
}
|
||||
++ lib.optional (cfg.lspServers != { }) {
|
||||
name = ".lsp.json";
|
||||
path = jsonFormat.generate "claude-code-lsp.json" cfg.lspServers;
|
||||
};
|
||||
|
||||
generatedPlugin = pkgs.runCommand "claude-code-hm-plugin" { } (
|
||||
''
|
||||
install -Dm644 ${
|
||||
jsonFormat.generate "claude-code-plugin.json" {
|
||||
name = "claude-code-home-manager";
|
||||
}
|
||||
} $out/.claude-plugin/plugin.json
|
||||
''
|
||||
+ lib.concatLines (
|
||||
map (pluginFile: "install -Dm644 ${pluginFile.path} $out/${pluginFile.name}") generatedPluginFiles
|
||||
)
|
||||
);
|
||||
|
||||
pluginEntries =
|
||||
lib.optional (generatedPluginFiles != [ ]) {
|
||||
name = "claude-code-home-manager";
|
||||
source = generatedPlugin;
|
||||
}
|
||||
++ map mkPluginEntry cfg.plugins;
|
||||
|
||||
legacyPluginPaths = lib.optional (generatedPluginFiles != [ ]) generatedPlugin ++ cfg.plugins;
|
||||
|
||||
hasManagedPlugins = legacyPluginPaths != [ ];
|
||||
useLegacyPluginWrapper = hasManagedPlugins && !supportsPersonalPlugins;
|
||||
|
||||
legacyWrapperArgs = lib.concatMap (plugin: [
|
||||
"--plugin-dir"
|
||||
"${plugin}"
|
||||
]) legacyPluginPaths;
|
||||
|
||||
legacyFinalPackage = pkgs.symlinkJoin {
|
||||
name = "claude-code";
|
||||
paths = [ cfg.package ];
|
||||
postBuild = ''
|
||||
mv $out/bin/claude $out/bin/.claude-wrapped
|
||||
cat > $out/bin/claude <<EOF
|
||||
#! ${pkgs.bash}/bin/bash -e
|
||||
exec -a "\$0" "$out/bin/.claude-wrapped" ${lib.escapeShellArgs legacyWrapperArgs} "\$@"
|
||||
EOF
|
||||
chmod +x $out/bin/claude
|
||||
'';
|
||||
inherit (cfg.package) meta;
|
||||
};
|
||||
|
||||
pluginNames = map (plugin: plugin.name) pluginEntries;
|
||||
|
||||
skillsAreAttrs = builtins.isAttrs cfg.skills;
|
||||
skillsArePath = lib.hm.strings.isPathLike cfg.skills;
|
||||
skillsAreDirectory = skillsArePath && lib.pathIsDirectory cfg.skills;
|
||||
skillNames = lib.attrNames (
|
||||
if skillsAreAttrs then
|
||||
cfg.skills
|
||||
else
|
||||
lib.optionalAttrs skillsAreDirectory (builtins.readDir cfg.skills)
|
||||
);
|
||||
|
||||
pluginFileEntries = lib.optionalAttrs supportsPersonalPlugins (
|
||||
lib.listToAttrs (
|
||||
map (
|
||||
plugin:
|
||||
lib.nameValuePair "${cfg.configDir}/skills/${plugin.name}" {
|
||||
inherit (plugin) source;
|
||||
recursive = true;
|
||||
}
|
||||
) pluginEntries
|
||||
)
|
||||
);
|
||||
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
warnings = lib.optional (useLegacyPluginWrapper && supportsPluginDir) ''
|
||||
`programs.claude-code.package` ${
|
||||
if hasPackageVersion then "version ${packageVersion}" else "has no detectable version and"
|
||||
}
|
||||
uses the legacy `--plugin-dir` wrapper. Strict-parser subcommands such
|
||||
as `claude rc` may reject managed MCP, LSP, or plugin arguments. Upgrade
|
||||
Claude Code to version 2.1.157 or later to use persistent personal
|
||||
plugins instead.
|
||||
'';
|
||||
|
||||
assertions =
|
||||
let
|
||||
exclusiveInlineDirNames = [
|
||||
"rules"
|
||||
"agents"
|
||||
"commands"
|
||||
"hooks"
|
||||
];
|
||||
|
||||
mkExclusiveAssertion = inline: {
|
||||
assertion = !(cfg.${inline} != { } && cfg.${inline + "Dir"} != null);
|
||||
message = "Cannot specify both `programs.claude-code.${inline}` and `programs.claude-code.${inline}Dir`";
|
||||
};
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = !hasManagedPlugins || supportsPluginDir;
|
||||
message = "Managed Claude Code MCP, LSP, and plugins require `programs.claude-code.package` version 2.1.76 or later";
|
||||
}
|
||||
{
|
||||
assertion = !skillsArePath || skillsAreDirectory;
|
||||
message = "`programs.claude-code.skills` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
!supportsPersonalPlugins || lib.length pluginNames == lib.length (lib.unique pluginNames);
|
||||
message = "`programs.claude-code.plugins` entries must resolve to unique personal-plugin directory names";
|
||||
}
|
||||
{
|
||||
assertion = !supportsPersonalPlugins || lib.intersectLists skillNames pluginNames == [ ];
|
||||
message = "`programs.claude-code.skills` and managed plugins must have unique directory names";
|
||||
}
|
||||
]
|
||||
++ map mkExclusiveAssertion exclusiveInlineDirNames;
|
||||
|
||||
programs.claude-code.finalPackage = lib.mkIf (cfg.package != null) (
|
||||
if useLegacyPluginWrapper then legacyFinalPackage else cfg.package
|
||||
);
|
||||
|
||||
home = {
|
||||
packages = lib.mkIf (cfg.package != null) [ cfg.finalPackage ];
|
||||
|
||||
sessionVariables = lib.mkIf (cfg.configDir != upstreamConfigDir) {
|
||||
CLAUDE_CONFIG_DIR = cfg.configDir;
|
||||
};
|
||||
|
||||
file = lib.mkMerge [
|
||||
(lib.mkIf (cfg.settings != { } || cfg.marketplaces != { } || disabledMcpServerNames != [ ]) {
|
||||
"${cfg.configDir}/settings.json".source =
|
||||
let
|
||||
settingsFile = jsonFormat.generate "claude-code-settings.json" (
|
||||
cfg.settings
|
||||
// {
|
||||
"$schema" = "https://json.schemastore.org/claude-code-settings.json";
|
||||
}
|
||||
// lib.optionalAttrs (cfg.marketplaces != { }) {
|
||||
extraKnownMarketplaces = lib.mapAttrs mkMarketplaceEntry cfg.marketplaces;
|
||||
}
|
||||
// lib.optionalAttrs (disabledMcpServerNames != [ ]) {
|
||||
disabledMcpjsonServers = lib.unique (
|
||||
(cfg.settings.disabledMcpjsonServers or [ ]) ++ disabledMcpServerNames
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
settingsDirectory = pkgs.runCommand "claude-code-settings-directory" { } ''
|
||||
install -Dm444 ${settingsFile} "$out/settings.json"
|
||||
'';
|
||||
in
|
||||
"${settingsDirectory}/settings.json";
|
||||
})
|
||||
(
|
||||
if lib.isPath cfg.context then
|
||||
{
|
||||
"${cfg.configDir}/CLAUDE.md".source = cfg.context;
|
||||
}
|
||||
else
|
||||
(lib.mkIf (cfg.context != "") {
|
||||
"${cfg.configDir}/CLAUDE.md".text = cfg.context;
|
||||
})
|
||||
)
|
||||
(lib.mkIf (cfg.marketplaces != { }) {
|
||||
"${cfg.configDir}/plugins/known_marketplaces.json".source =
|
||||
jsonFormat.generate "claude-code-known-marketplaces.json" (
|
||||
lib.mapAttrs mkInstalledMarketplaceEntry cfg.marketplaces
|
||||
);
|
||||
})
|
||||
(mkMarkdownEntries "agents" cfg.agents)
|
||||
(mkMarkdownEntries "commands" cfg.commands)
|
||||
(mkMarkdownEntries "rules" cfg.rules)
|
||||
(mkRecursiveDirAttrs "agents" cfg.agentsDir)
|
||||
(mkRecursiveDirAttrs "commands" cfg.commandsDir)
|
||||
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
|
||||
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
|
||||
(lib.mkIf skillsArePath {
|
||||
"${cfg.configDir}/skills" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
})
|
||||
pluginFileEntries
|
||||
(mkHookEntries cfg.hooks)
|
||||
(lib.optionalAttrs skillsAreAttrs (lib.mapAttrs' mkSkillEntry cfg.skills))
|
||||
(mkMarkdownEntries "output-styles" cfg.outputStyles)
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{ lib, pkgs }:
|
||||
let
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
in
|
||||
{
|
||||
mkHelpers =
|
||||
{ configDir }:
|
||||
let
|
||||
mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; };
|
||||
|
||||
mkMarketplaceEntry = _name: content: {
|
||||
source = {
|
||||
source = "directory";
|
||||
path = content;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit mkMarketplaceEntry;
|
||||
|
||||
mkHookEntries =
|
||||
attrs:
|
||||
lib.mapAttrs' (
|
||||
name: content:
|
||||
lib.nameValuePair "${configDir}/hooks/${name}" {
|
||||
text = content;
|
||||
executable = true;
|
||||
}
|
||||
) attrs;
|
||||
|
||||
mkInstalledMarketplaceEntry =
|
||||
name: content:
|
||||
(mkMarketplaceEntry name content)
|
||||
// {
|
||||
installLocation = content;
|
||||
lastUpdated = "1970-01-01T00:00:00Z";
|
||||
};
|
||||
|
||||
mkMarkdownEntries =
|
||||
subdir: attrs:
|
||||
lib.mapAttrs' (
|
||||
name: content: lib.nameValuePair "${configDir}/${subdir}/${name}.md" (mkSourceEntry content)
|
||||
) attrs;
|
||||
|
||||
mkPluginEntry =
|
||||
plugin:
|
||||
let
|
||||
name = builtins.unsafeDiscardStringContext (
|
||||
lib.strings.sanitizeDerivationName (baseNameOf (toString plugin))
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit name;
|
||||
source = pkgs.symlinkJoin {
|
||||
name = "claude-code-${name}";
|
||||
paths = [ plugin ];
|
||||
postBuild = ''
|
||||
if [[ ! -e $out/.claude-plugin/plugin.json ]]; then
|
||||
install -Dm644 ${
|
||||
jsonFormat.generate "claude-code-${name}.json" {
|
||||
inherit name;
|
||||
}
|
||||
} $out/.claude-plugin/plugin.json
|
||||
fi
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
mkRecursiveDirAttrs =
|
||||
subdir: dir:
|
||||
lib.optionalAttrs (dir != null) {
|
||||
"${configDir}/${subdir}" = {
|
||||
source = dir;
|
||||
recursive = true;
|
||||
};
|
||||
};
|
||||
|
||||
mkSkillEntry =
|
||||
name: content:
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
lib.nameValuePair "${configDir}/skills/${name}" {
|
||||
source = content;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "${configDir}/skills/${name}/SKILL.md" (
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -5,58 +5,17 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
literalExpression
|
||||
mkChangedOptionModule
|
||||
mkOption
|
||||
nameValuePair
|
||||
optionalAttrs
|
||||
;
|
||||
|
||||
cfg = config.programs.claude-code;
|
||||
inherit (lib) literalExpression mkOption;
|
||||
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
|
||||
upstreamConfigDir = "${config.home.homeDirectory}/.claude";
|
||||
|
||||
isMcpServerEnabled =
|
||||
server:
|
||||
let
|
||||
enabled = server.enabled or null;
|
||||
disabled = (server.disabled or false) == true;
|
||||
in
|
||||
enabled != false && !disabled;
|
||||
|
||||
transformMcpServer =
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
exclude = [ "enabled" ];
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; }) # envFiles currently still need wrapping https://github.com/anthropics/claude-code/issues/28942
|
||||
];
|
||||
};
|
||||
|
||||
transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
|
||||
lib.mapAttrs transformMcpServer (
|
||||
lib.filterAttrs (_: isMcpServerEnabled) config.programs.mcp.servers
|
||||
)
|
||||
);
|
||||
|
||||
mkContentOption =
|
||||
{
|
||||
description,
|
||||
example ? null,
|
||||
}:
|
||||
mkOption (
|
||||
{
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
|
||||
default = { };
|
||||
inherit description;
|
||||
}
|
||||
// optionalAttrs (example != null) { inherit example; }
|
||||
);
|
||||
{ description, example }:
|
||||
mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
|
||||
default = { };
|
||||
inherit description example;
|
||||
};
|
||||
|
||||
mkDirOption =
|
||||
{ description, example }:
|
||||
@@ -65,29 +24,8 @@ let
|
||||
default = null;
|
||||
inherit description example;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.khaneliman ];
|
||||
|
||||
imports = [
|
||||
(mkChangedOptionModule
|
||||
[ "programs" "claude-code" "memory" "text" ]
|
||||
[ "programs" "claude-code" "context" ]
|
||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "text" ] config)
|
||||
)
|
||||
(mkChangedOptionModule
|
||||
[ "programs" "claude-code" "memory" "source" ]
|
||||
[ "programs" "claude-code" "context" ]
|
||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "memory" "source" ] config)
|
||||
)
|
||||
(mkChangedOptionModule
|
||||
[ "programs" "claude-code" "skillsDir" ]
|
||||
[ "programs" "claude-code" "skills" ]
|
||||
(config: lib.getAttrFromPath [ "programs" "claude-code" "skillsDir" ] config)
|
||||
)
|
||||
];
|
||||
|
||||
options.programs.claude-code = {
|
||||
enable = lib.mkEnableOption "Claude Code, Anthropic's official CLI";
|
||||
|
||||
@@ -111,12 +49,18 @@ in
|
||||
Note: Settings defined in {option}`programs.mcp.servers` are merged
|
||||
with {option}`programs.claude-code.mcpServers`, with Claude Code servers
|
||||
taking precedence.
|
||||
|
||||
Servers marked disabled (via `enabled = false` or `disabled = true`)
|
||||
are still written to {file}`.mcp.json` but rejected through
|
||||
`disabledMcpjsonServers` in {file}`settings.json`, since Claude Code
|
||||
has no per-server `enabled` field. This keeps a disabled server
|
||||
present (and re-enableable) instead of dropping it.
|
||||
'';
|
||||
};
|
||||
|
||||
configDir = mkOption {
|
||||
type = lib.types.str;
|
||||
default = upstreamConfigDir;
|
||||
default = "${config.home.homeDirectory}/.claude";
|
||||
defaultText = literalExpression ''"''${config.home.homeDirectory}/.claude"'';
|
||||
example = literalExpression ''"''${config.xdg.configHome}/claude"'';
|
||||
description = ''
|
||||
@@ -211,12 +155,17 @@ in
|
||||
Each entry is either:
|
||||
- A path to the plugin directory
|
||||
- The plugin package, whether a nix package or the output of a fetcher
|
||||
Plugins are enabled via a `--plugin-dir` argument in the wrapper script.
|
||||
With Claude Code 2.1.157 or later, plugins are linked into
|
||||
{option}`programs.claude-code.configDir` and loaded as personal plugins.
|
||||
Versions 2.1.76 through 2.1.156 fall back to a legacy `--plugin-dir`
|
||||
wrapper, as do packages without detectable version metadata.
|
||||
Strict-parser subcommands such as {command}`claude rc` may reject
|
||||
arguments from that compatibility path.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
[
|
||||
./my-local-plugin
|
||||
fetchFromGithub {
|
||||
fetchFromGitHub {
|
||||
owner = "some-github-org";
|
||||
repo = "claude-plugin";
|
||||
rev = "779a68ebc2a75e4a184d2c87e5a43a758e6458a1";
|
||||
@@ -238,7 +187,7 @@ in
|
||||
example = literalExpression ''
|
||||
{
|
||||
local-marketplace = ./my-local-marketplace;
|
||||
gh-marketplace = fetchFromGithub {
|
||||
gh-marketplace = fetchFromGitHub {
|
||||
owner = "some-github-org";
|
||||
repo = "claude-marketplace";
|
||||
rev = "8a873a220b8427b25b03ce1a821593a24e098c34";
|
||||
@@ -429,13 +378,7 @@ in
|
||||
};
|
||||
|
||||
skills = mkOption {
|
||||
type = lib.types.either (lib.types.attrsOf (
|
||||
lib.types.oneOf [
|
||||
lib.types.lines
|
||||
lib.types.path
|
||||
lib.types.str
|
||||
]
|
||||
)) lib.types.path;
|
||||
type = with lib.types; either (attrsOf (either lines path)) path;
|
||||
default = { };
|
||||
description = ''
|
||||
Custom skills for Claude Code.
|
||||
@@ -555,196 +498,4 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
mkSourceEntry = content: if lib.isPath content then { source = content; } else { text = content; };
|
||||
|
||||
mkMarkdownEntries =
|
||||
subdir: attrs:
|
||||
lib.mapAttrs' (
|
||||
name: content: nameValuePair "${cfg.configDir}/${subdir}/${name}.md" (mkSourceEntry content)
|
||||
) attrs;
|
||||
|
||||
mkHookEntries =
|
||||
attrs:
|
||||
lib.mapAttrs' (
|
||||
name: content:
|
||||
nameValuePair "${cfg.configDir}/hooks/${name}" {
|
||||
text = content;
|
||||
executable = true;
|
||||
}
|
||||
) attrs;
|
||||
|
||||
mkRecursiveDirAttrs =
|
||||
subdir: dir:
|
||||
optionalAttrs (dir != null) {
|
||||
"${cfg.configDir}/${subdir}" = {
|
||||
source = dir;
|
||||
recursive = true;
|
||||
};
|
||||
};
|
||||
|
||||
mkSkillEntry =
|
||||
name: content:
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
nameValuePair "${cfg.configDir}/skills/${name}" {
|
||||
source = content;
|
||||
recursive = true;
|
||||
}
|
||||
else
|
||||
nameValuePair "${cfg.configDir}/skills/${name}/SKILL.md" (
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
);
|
||||
|
||||
mkMarketplaceEntry = _name: content: {
|
||||
source = {
|
||||
source = "directory";
|
||||
path = content;
|
||||
};
|
||||
};
|
||||
|
||||
mkInstalledMarketplaceEntry =
|
||||
name: content:
|
||||
(mkMarketplaceEntry name content)
|
||||
// {
|
||||
installLocation = content;
|
||||
lastUpdated = "1970-01-01T00:00:00Z";
|
||||
};
|
||||
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
assertions =
|
||||
let
|
||||
exclusiveInlineDirNames = [
|
||||
"rules"
|
||||
"agents"
|
||||
"commands"
|
||||
"hooks"
|
||||
];
|
||||
|
||||
mkExclusiveAssertion = inline: {
|
||||
assertion = !(cfg.${inline} != { } && cfg.${inline + "Dir"} != null);
|
||||
message = "Cannot specify both `programs.claude-code.${inline}` and `programs.claude-code.${inline}Dir`";
|
||||
};
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion =
|
||||
(cfg.mcpServers == { } && cfg.lspServers == { } && !cfg.enableMcpIntegration && cfg.plugins == [ ])
|
||||
|| cfg.package != null;
|
||||
message = "`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, `enableMcpIntegration`, or `plugins` is configured";
|
||||
}
|
||||
{
|
||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.claude-code.skills` must be a directory when set to a path";
|
||||
}
|
||||
]
|
||||
++ map mkExclusiveAssertion exclusiveInlineDirNames;
|
||||
|
||||
programs.claude-code.finalPackage =
|
||||
let
|
||||
mergedMcpServers =
|
||||
transformedMcpServers
|
||||
// lib.mapAttrs (_: server: removeAttrs (lib.hm.mcp.addType server) [ "enabled" ]) cfg.mcpServers;
|
||||
pluginFiles =
|
||||
lib.optional (mergedMcpServers != { }) {
|
||||
name = ".mcp.json";
|
||||
path = jsonFormat.generate "claude-code-mcp.json" { mcpServers = mergedMcpServers; };
|
||||
}
|
||||
++ lib.optional (cfg.lspServers != { }) {
|
||||
name = ".lsp.json";
|
||||
path = jsonFormat.generate "claude-code-lsp.json" cfg.lspServers;
|
||||
};
|
||||
pluginDir = pkgs.runCommand "claude-code-hm-plugin" { } (
|
||||
''
|
||||
install -Dm644 ${
|
||||
jsonFormat.generate "claude-code-plugin.json" {
|
||||
name = "claude-code-home-manager";
|
||||
}
|
||||
} $out/.claude-plugin/plugin.json
|
||||
''
|
||||
+ lib.concatLines (
|
||||
map (pluginFile: "install -Dm644 ${pluginFile.path} $out/${pluginFile.name}") pluginFiles
|
||||
)
|
||||
);
|
||||
allPluginPaths = (if pluginFiles != [ ] then [ pluginDir ] else [ ]) ++ cfg.plugins;
|
||||
wrapperArgs = lib.flatten (
|
||||
map (p: [
|
||||
"--plugin-dir"
|
||||
"${p}"
|
||||
]) allPluginPaths
|
||||
);
|
||||
in
|
||||
if allPluginPaths != [ ] then
|
||||
pkgs.symlinkJoin {
|
||||
name = "claude-code";
|
||||
paths = [ cfg.package ];
|
||||
postBuild = ''
|
||||
mv $out/bin/claude $out/bin/.claude-wrapped
|
||||
cat > $out/bin/claude <<EOF
|
||||
#! ${pkgs.bash}/bin/bash -e
|
||||
exec -a "\$0" "$out/bin/.claude-wrapped" ${lib.escapeShellArgs wrapperArgs} "\$@"
|
||||
EOF
|
||||
chmod +x $out/bin/claude
|
||||
'';
|
||||
inherit (cfg.package) meta;
|
||||
}
|
||||
else
|
||||
cfg.package;
|
||||
|
||||
home = {
|
||||
packages = lib.mkIf (cfg.package != null) [ cfg.finalPackage ];
|
||||
|
||||
sessionVariables = lib.mkIf (cfg.configDir != upstreamConfigDir) {
|
||||
CLAUDE_CONFIG_DIR = cfg.configDir;
|
||||
};
|
||||
|
||||
file = lib.mkMerge [
|
||||
(lib.mkIf (cfg.settings != { } || cfg.marketplaces != { }) {
|
||||
"${cfg.configDir}/settings.json".source = jsonFormat.generate "claude-code-settings.json" (
|
||||
cfg.settings
|
||||
// {
|
||||
"$schema" = "https://json.schemastore.org/claude-code-settings.json";
|
||||
}
|
||||
// optionalAttrs (cfg.marketplaces != { }) {
|
||||
extraKnownMarketplaces = lib.mapAttrs mkMarketplaceEntry cfg.marketplaces;
|
||||
}
|
||||
);
|
||||
})
|
||||
(
|
||||
if lib.isPath cfg.context then
|
||||
{
|
||||
"${cfg.configDir}/CLAUDE.md".source = cfg.context;
|
||||
}
|
||||
else
|
||||
(lib.mkIf (cfg.context != "") {
|
||||
"${cfg.configDir}/CLAUDE.md".text = cfg.context;
|
||||
})
|
||||
)
|
||||
(lib.mkIf (cfg.marketplaces != { }) {
|
||||
"${cfg.configDir}/plugins/known_marketplaces.json".source =
|
||||
jsonFormat.generate "claude-code-known-marketplaces.json" (
|
||||
lib.mapAttrs mkInstalledMarketplaceEntry cfg.marketplaces
|
||||
);
|
||||
})
|
||||
(mkMarkdownEntries "agents" cfg.agents)
|
||||
(mkMarkdownEntries "commands" cfg.commands)
|
||||
(mkMarkdownEntries "rules" cfg.rules)
|
||||
(mkRecursiveDirAttrs "agents" cfg.agentsDir)
|
||||
(mkRecursiveDirAttrs "commands" cfg.commandsDir)
|
||||
(mkRecursiveDirAttrs "hooks" cfg.hooksDir)
|
||||
(mkRecursiveDirAttrs "rules" cfg.rulesDir)
|
||||
(lib.mkIf (lib.hm.strings.isPathLike cfg.skills) {
|
||||
"${cfg.configDir}/skills" = {
|
||||
source = cfg.skills;
|
||||
recursive = true;
|
||||
};
|
||||
})
|
||||
(mkHookEntries cfg.hooks)
|
||||
(lib.optionalAttrs (builtins.isAttrs cfg.skills) (lib.mapAttrs' mkSkillEntry cfg.skills))
|
||||
(mkMarkdownEntries "output-styles" cfg.outputStyles)
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,522 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkIf;
|
||||
|
||||
cfg = config.programs.codex;
|
||||
|
||||
tomlFormat = pkgs.formats.toml { };
|
||||
yamlFormat = pkgs.formats.yaml { };
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
|
||||
# A null package has no detectable version, so assume the latest Codex and
|
||||
# enable version-gated behavior by default.
|
||||
atLeast = version: cfg.package == null || lib.versionAtLeast (lib.getVersion cfg.package) version;
|
||||
isTomlConfig = atLeast "0.2.0";
|
||||
migrateLegacyProfiles = atLeast "0.134.0";
|
||||
settingsFormat = if isTomlConfig then tomlFormat else yamlFormat;
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
delafthi
|
||||
];
|
||||
|
||||
imports = [
|
||||
(lib.mkRenamedOptionModule
|
||||
[ "programs" "codex" "custom-instructions" ]
|
||||
[ "programs" "codex" "context" ]
|
||||
)
|
||||
];
|
||||
|
||||
options.programs.codex = {
|
||||
enable = lib.mkEnableOption "Lightweight coding agent that runs in your terminal";
|
||||
|
||||
package = lib.mkPackageOption pkgs "codex" { nullable = true; };
|
||||
|
||||
enableMcpIntegration = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to integrate the MCP server config from
|
||||
{option}`programs.mcp.servers` into
|
||||
{option}`programs.codex.settings.mcp_servers`.
|
||||
|
||||
Note: Settings defined in {option}`programs.mcp.servers` are merged
|
||||
with {option}`programs.codex.settings.mcp_servers`, with settings-based
|
||||
values taking precedence.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
# NOTE: `yaml` type supports null, using `nullOr` for backwards compatibility period
|
||||
type = lib.types.nullOr tomlFormat.type;
|
||||
description = ''
|
||||
Configuration written to {file}`CODEX_HOME/config.toml` (0.2.0+)
|
||||
or {file}`~/.codex/config.yaml` (<0.2.0). Per default {env}`CODEX_HOME`
|
||||
defaults to ~/.codex.
|
||||
See <https://github.com/openai/codex/blob/main/codex-rs/config.md> for supported values.
|
||||
'';
|
||||
default = { };
|
||||
defaultText = lib.literalExpression "{ }";
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
model = "gemma3:latest";
|
||||
model_provider = "ollama";
|
||||
model_providers = {
|
||||
ollama = {
|
||||
name = "Ollama";
|
||||
baseURL = "http://localhost:11434/v1";
|
||||
envKey = "OLLAMA_API_KEY";
|
||||
};
|
||||
};
|
||||
mcp_servers = {
|
||||
context7 = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@upstash/context7-mcp"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
profiles = lib.mkOption {
|
||||
type = lib.types.attrsOf tomlFormat.type;
|
||||
default = { };
|
||||
description = ''
|
||||
Named Codex configuration profiles written to
|
||||
{file}`CODEX_HOME/<name>.config.toml`.
|
||||
|
||||
These profiles are selected with {command}`codex --profile <name>`.
|
||||
Codex 0.134.0 and later no longer reads profile settings from
|
||||
{option}`programs.codex.settings.profiles`, and the top-level
|
||||
{option}`programs.codex.settings.profile` selector is no longer
|
||||
supported.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
deep-review = {
|
||||
model = "gpt-5.5";
|
||||
model_reasoning_effort = "xhigh";
|
||||
approval_policy = "on-request";
|
||||
sandbox_mode = "workspace-write";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
context = lib.mkOption {
|
||||
type = lib.types.either lib.types.lines lib.types.path;
|
||||
description = ''
|
||||
Global context for Codex.
|
||||
|
||||
The value is either:
|
||||
- Inline content as a string
|
||||
- A path to a file containing the content
|
||||
|
||||
The configured content is written to
|
||||
{file}`CODEX_HOME/AGENTS.md`.
|
||||
'';
|
||||
default = "";
|
||||
example = lib.literalExpression ''
|
||||
'''
|
||||
- Always respond with emojis
|
||||
- Only use git commands when explicitly requested
|
||||
'''
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = lib.mkOption {
|
||||
type = with lib.types; listOf (either package path);
|
||||
default = [ ];
|
||||
description = ''
|
||||
List of plugins to use when running Codex.
|
||||
Each entry is either:
|
||||
- A path to the plugin directory
|
||||
- The plugin package, whether a nix package or the output of a fetcher
|
||||
Plugins are installed into Codex's plugin cache and enabled through
|
||||
{file}`CODEX_HOME/config.toml`.
|
||||
|
||||
Warning: If using a derivation as the source for a plugin, make sure that
|
||||
the derivation name matches the name of the plugin in the manifest file.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
./my-local-plugin
|
||||
(fetchFromGitHub {
|
||||
owner = "some-github-org";
|
||||
repo = "codex-plugin";
|
||||
rev = "779a68ebc2a75e4a184d2c87e5a43a758e6458a1";
|
||||
sha256 = "228fdd7e5908ea1d2f65218ecd9c71e1eefa0834d200d55fbb8bf8b5563acec0";
|
||||
})
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
marketplaces = lib.mkOption {
|
||||
type = with lib.types; attrsOf (either package path);
|
||||
default = { };
|
||||
description = ''
|
||||
Custom marketplaces for Codex plugins.
|
||||
The attribute name becomes the marketplace name, and the value is either:
|
||||
- A path to the marketplace directory
|
||||
- The marketplace package, whether a nix package or the output of a fetcher
|
||||
|
||||
Marketplaces are configured through {file}`CODEX_HOME/config.toml`.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
local-marketplace = ./my-local-marketplace;
|
||||
gh-marketplace = fetchFromGitHub {
|
||||
owner = "some-github-org";
|
||||
repo = "codex-marketplace";
|
||||
rev = "8a873a220b8427b25b03ce1a821593a24e098c34";
|
||||
sha256 = "5c2dce95122b5bb73fa547edabbb6c3061c2d193d11e51faecd4d22659e67279";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
skills = lib.mkOption {
|
||||
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
|
||||
default = { };
|
||||
description = ''
|
||||
Custom skills for Codex.
|
||||
|
||||
This option can be either:
|
||||
- An attribute set defining skills
|
||||
- A path to a directory containing skill folders
|
||||
|
||||
If an attribute set is used, the attribute name becomes the
|
||||
skill directory name, and the value is either:
|
||||
- Inline content as a string (creates a generated skill directory at {file}`<skills-dir>/<name>/`)
|
||||
- A path to a file (creates a generated skill directory at {file}`<skills-dir>/<name>/`)
|
||||
- A path to a directory (symlinks {file}`<skills-dir>/<name>/` to that directory)
|
||||
|
||||
If a path is used, it is expected to contain one folder per
|
||||
skill name, each containing a {file}`SKILL.md`. Each top-level
|
||||
skill entry is symlinked into {file}`<skills-dir>/`, leaving
|
||||
{file}`<skills-dir>/` itself as a normal directory so unmanaged
|
||||
skills can coexist.
|
||||
|
||||
Home Manager manages skills under {file}`CODEX_HOME/skills`
|
||||
(typically {file}`~/.codex/skills`, or
|
||||
{file}`~/.config/codex/skills` when
|
||||
{option}`home.preferXdgDirectories` is enabled).
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
pdf-processing = '''
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
```
|
||||
''';
|
||||
data-analysis = ./skills/data-analysis;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
rules = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
|
||||
default = { };
|
||||
description = ''
|
||||
Codex rules files to manage under {file}`CODEX_HOME/rules/`.
|
||||
|
||||
The attribute name becomes the filename, with a {file}`.rules`
|
||||
extension added automatically. The value is either:
|
||||
- Inline content as a string
|
||||
- A path to an existing rules file
|
||||
|
||||
This is useful for declaratively managing persistent
|
||||
`prefix_rule()` definitions, including the default
|
||||
{file}`default.rules` allow-list Codex writes when you accept
|
||||
recurring approvals interactively.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
default = "prefix_rule(pattern = [\"nix\", \"build\"], decision = \"allow\")\n";
|
||||
github = ./codex/github.rules;
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
useXdgDirectories = config.home.preferXdgDirectories && isTomlConfig;
|
||||
xdgConfigHome = lib.removePrefix config.home.homeDirectory config.xdg.configHome;
|
||||
configDir = if useXdgDirectories then "${xdgConfigHome}/codex" else ".codex";
|
||||
configFileName = if isTomlConfig then "config.toml" else "config.yaml";
|
||||
skillsDir = "${configDir}/skills";
|
||||
homeRelativeConfigDir = lib.removePrefix "/" configDir;
|
||||
pluginsMarketplaceName = "home-manager";
|
||||
pluginsDir = "${configDir}/plugins";
|
||||
pluginsCacheDir = "${pluginsDir}/cache";
|
||||
homeRelativePluginsCacheDir = "${homeRelativeConfigDir}/plugins/cache";
|
||||
rawSettings = if cfg.settings == null then { } else cfg.settings;
|
||||
|
||||
# TODO: Remove this workaround once Codex supports symlinked SKILL.md
|
||||
# files again. Upstream only supports symlinking the containing skill
|
||||
# directory today: https://github.com/openai/codex/issues/10470
|
||||
mkSkillDir =
|
||||
content:
|
||||
pkgs.writeTextDir "SKILL.md" (
|
||||
if lib.hm.strings.isPathLike content then builtins.readFile content else content
|
||||
);
|
||||
skillSources =
|
||||
if builtins.isAttrs cfg.skills then
|
||||
cfg.skills
|
||||
else if lib.hm.strings.isPathLike cfg.skills && lib.pathIsDirectory cfg.skills then
|
||||
lib.mapAttrs (name: _type: cfg.skills + "/${name}") (builtins.readDir cfg.skills)
|
||||
else
|
||||
{ };
|
||||
mkSkillEntry =
|
||||
name: content:
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
lib.nameValuePair "${skillsDir}/${name}" {
|
||||
source = content;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "${skillsDir}/${name}" {
|
||||
source = mkSkillDir content;
|
||||
};
|
||||
mkRuleEntry =
|
||||
name: content:
|
||||
lib.nameValuePair "${configDir}/rules/${name}.rules" (
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
);
|
||||
mkPluginName =
|
||||
plugin:
|
||||
let
|
||||
manifestPath = plugin + "/.codex-plugin/plugin.json";
|
||||
manifestName =
|
||||
if !lib.isDerivation plugin && builtins.pathExists manifestPath then
|
||||
(builtins.fromJSON (builtins.readFile manifestPath)).name
|
||||
else
|
||||
null;
|
||||
fallbackName =
|
||||
if lib.isDerivation plugin then
|
||||
plugin.pname or (lib.getName plugin)
|
||||
else
|
||||
baseNameOf (toString plugin);
|
||||
in
|
||||
builtins.unsafeDiscardStringContext (if manifestName != null then manifestName else fallbackName);
|
||||
mkPluginVersion =
|
||||
plugin:
|
||||
let
|
||||
manifestPath = plugin + "/.codex-plugin/plugin.json";
|
||||
manifestVersion =
|
||||
if !lib.isDerivation plugin && builtins.pathExists manifestPath then
|
||||
(builtins.fromJSON (builtins.readFile manifestPath)).version or null
|
||||
else
|
||||
null;
|
||||
fallbackVersion = plugin.version or "0.0.0";
|
||||
in
|
||||
builtins.unsafeDiscardStringContext (
|
||||
if manifestVersion != null then manifestVersion else fallbackVersion
|
||||
);
|
||||
mkPluginCachePath =
|
||||
plugin:
|
||||
"${pluginsCacheDir}/${pluginsMarketplaceName}/${mkPluginName plugin}/${mkPluginVersion plugin}";
|
||||
mkPluginFileEntry =
|
||||
plugin:
|
||||
lib.nameValuePair (mkPluginCachePath plugin) {
|
||||
source = plugin;
|
||||
force = true;
|
||||
};
|
||||
mkPluginConfigEntry =
|
||||
plugin:
|
||||
lib.nameValuePair "${mkPluginName plugin}@${pluginsMarketplaceName}" {
|
||||
enabled = true;
|
||||
};
|
||||
mkMarketplaceConfigEntry = _name: content: {
|
||||
source_type = "local";
|
||||
source = "${content}";
|
||||
};
|
||||
mkPersonalMarketplacePluginEntry = plugin: {
|
||||
name = mkPluginName plugin;
|
||||
source = {
|
||||
source = "local";
|
||||
path = "./${homeRelativePluginsCacheDir}/${pluginsMarketplaceName}/${mkPluginName plugin}/${mkPluginVersion plugin}";
|
||||
};
|
||||
policy = {
|
||||
installation = "AVAILABLE";
|
||||
authentication = "ON_INSTALL";
|
||||
};
|
||||
category = "Productivity";
|
||||
};
|
||||
mkProfileEntry =
|
||||
name: settings:
|
||||
lib.nameValuePair "${configDir}/${name}.config.toml" {
|
||||
source = tomlFormat.generate "codex-${name}-config" settings;
|
||||
};
|
||||
|
||||
transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
|
||||
lib.mapAttrs (
|
||||
name: server:
|
||||
# NOTE: Convert shared programs.mcp fields to Codex config keys:
|
||||
# - file-backed env entries are wrapped in a shell script that sets environment variables before exec
|
||||
# - "headers" is renamed to "http_headers"
|
||||
# See: https://developers.openai.com/codex/mcp#other-configuration-options
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
exclude = [
|
||||
"headers"
|
||||
"type"
|
||||
];
|
||||
extraTransforms = [
|
||||
(s: s // lib.optionalAttrs (s.headers or { } != { }) { http_headers = s.headers; })
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
);
|
||||
|
||||
# TODO: remove this migration block in a future stateVersion once the
|
||||
# Codex 0.134 profile transition window has passed.
|
||||
hasLegacyProfileSettings =
|
||||
migrateLegacyProfiles && ((rawSettings ? profile) || (rawSettings ? profiles));
|
||||
legacyProfiles = lib.optionalAttrs (
|
||||
hasLegacyProfileSettings && builtins.isAttrs (rawSettings.profiles or null)
|
||||
) rawSettings.profiles;
|
||||
mergedProfiles = legacyProfiles // cfg.profiles;
|
||||
baseSettings =
|
||||
if hasLegacyProfileSettings then
|
||||
lib.removeAttrs rawSettings [
|
||||
"profile"
|
||||
"profiles"
|
||||
]
|
||||
else
|
||||
rawSettings;
|
||||
|
||||
generatedPluginSettings =
|
||||
lib.optionalAttrs (cfg.plugins != [ ] || cfg.marketplaces != { }) {
|
||||
features.plugins = true;
|
||||
}
|
||||
// lib.optionalAttrs (cfg.plugins != [ ]) {
|
||||
plugins = lib.listToAttrs (map mkPluginConfigEntry cfg.plugins);
|
||||
}
|
||||
// lib.optionalAttrs (cfg.marketplaces != { }) {
|
||||
marketplaces = lib.mapAttrs mkMarketplaceConfigEntry cfg.marketplaces;
|
||||
};
|
||||
mergedSettingsWithoutMcp = lib.recursiveUpdate baseSettings generatedPluginSettings;
|
||||
settingMcpServers = lib.attrByPath [ "mcp_servers" ] { } mergedSettingsWithoutMcp;
|
||||
mergedMcpServers = transformedMcpServers // settingMcpServers;
|
||||
|
||||
mergedSettings =
|
||||
mergedSettingsWithoutMcp
|
||||
// lib.optionalAttrs (mergedMcpServers != { }) { mcp_servers = mergedMcpServers; };
|
||||
in
|
||||
mkIf cfg.enable {
|
||||
warnings = lib.optional hasLegacyProfileSettings ''
|
||||
`programs.codex.settings.profile` and `programs.codex.settings.profiles`
|
||||
are no longer supported by Codex 0.134.0 and later. Home Manager
|
||||
now writes entries from `programs.codex.settings.profiles` to
|
||||
`CODEX_HOME/<name>.config.toml`. Move them to
|
||||
`programs.codex.profiles` and remove `programs.codex.settings.profile`.
|
||||
'';
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = (cfg.plugins == [ ] && cfg.marketplaces == { }) || isTomlConfig;
|
||||
message = "`programs.codex.plugins` and `programs.codex.marketplaces` require Codex 0.2.0 or later";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (
|
||||
plugin:
|
||||
!(lib.hm.strings.isPathLike plugin && !lib.isDerivation plugin) || lib.pathIsDirectory plugin
|
||||
) cfg.plugins;
|
||||
message = "`programs.codex.plugins` entries must be directories";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (
|
||||
marketplace:
|
||||
!(lib.hm.strings.isPathLike marketplace && !lib.isDerivation marketplace)
|
||||
|| lib.pathIsDirectory marketplace
|
||||
) (lib.attrValues cfg.marketplaces);
|
||||
message = "`programs.codex.marketplaces` entries must be directories";
|
||||
}
|
||||
{
|
||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.codex.skills` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (content: !(lib.hm.strings.isPathLike content && lib.pathIsDirectory content)) (
|
||||
lib.attrValues cfg.rules
|
||||
);
|
||||
message = "`programs.codex.rules` attribute values must be files when set to paths";
|
||||
}
|
||||
];
|
||||
|
||||
home = {
|
||||
packages = mkIf (cfg.package != null) [ cfg.package ];
|
||||
|
||||
# This is needed because codex will convert the symlinked plugin directory into
|
||||
# an actual directory (which will not be overwritten by home-manager)
|
||||
activation.cleanCodexPluginCache = lib.mkIf (cfg.plugins != [ ]) (
|
||||
lib.hm.dag.entryBefore [ "linkGeneration" ] (
|
||||
lib.concatMapStringsSep "\n" (
|
||||
plugin:
|
||||
let
|
||||
path = "$HOME/${mkPluginCachePath plugin}";
|
||||
in
|
||||
''
|
||||
if [ -d "${path}" ] && [ ! -L "${path}" ]; then
|
||||
rm -rf "${path}"
|
||||
fi
|
||||
''
|
||||
) cfg.plugins
|
||||
)
|
||||
);
|
||||
|
||||
file = {
|
||||
"${configDir}/${configFileName}" = lib.mkIf (mergedSettings != { }) {
|
||||
source = settingsFormat.generate "codex-config" mergedSettings;
|
||||
};
|
||||
".agents/plugins/marketplace.json" = lib.mkIf (cfg.plugins != [ ]) {
|
||||
source = jsonFormat.generate "codex-home-manager-marketplace" {
|
||||
name = pluginsMarketplaceName;
|
||||
interface.displayName = "Home Manager";
|
||||
plugins = map mkPersonalMarketplacePluginEntry cfg.plugins;
|
||||
};
|
||||
};
|
||||
"${configDir}/AGENTS.md" =
|
||||
if lib.isPath cfg.context then
|
||||
{ source = cfg.context; }
|
||||
else
|
||||
lib.mkIf (cfg.context != "") {
|
||||
text = cfg.context;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' mkProfileEntry mergedProfiles
|
||||
// lib.mapAttrs' mkSkillEntry skillSources
|
||||
// lib.listToAttrs (map mkPluginFileEntry cfg.plugins)
|
||||
// lib.mapAttrs' mkRuleEntry cfg.rules;
|
||||
|
||||
sessionVariables = mkIf useXdgDirectories {
|
||||
CODEX_HOME = "${config.xdg.configHome}/codex";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkIf;
|
||||
|
||||
cfg = config.programs.codex;
|
||||
|
||||
tomlFormat = pkgs.formats.toml { };
|
||||
yamlFormat = pkgs.formats.yaml { };
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
codexLib = import ./lib.nix { inherit lib pkgs; };
|
||||
|
||||
# A null package has no detectable version, so assume the latest Codex and
|
||||
# enable version-gated behavior by default.
|
||||
atLeast = version: cfg.package == null || lib.versionAtLeast (lib.getVersion cfg.package) version;
|
||||
isTomlConfig = atLeast "0.2.0";
|
||||
migrateLegacyProfiles = atLeast "0.134.0";
|
||||
settingsFormat = if isTomlConfig then tomlFormat else yamlFormat;
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
delafthi
|
||||
khaneliman
|
||||
];
|
||||
|
||||
imports = [
|
||||
(lib.mkRenamedOptionModule
|
||||
[ "programs" "codex" "custom-instructions" ]
|
||||
[ "programs" "codex" "context" ]
|
||||
)
|
||||
./options.nix
|
||||
];
|
||||
|
||||
config =
|
||||
let
|
||||
useXdgDirectories = config.home.preferXdgDirectories && isTomlConfig;
|
||||
xdgConfigHome = lib.removePrefix config.home.homeDirectory config.xdg.configHome;
|
||||
configDir = if useXdgDirectories then "${xdgConfigHome}/codex" else ".codex";
|
||||
configFileName = if isTomlConfig then "config.toml" else "config.yaml";
|
||||
skillsDir = "${configDir}/skills";
|
||||
pluginsMarketplaceName = "home-manager";
|
||||
pluginsDir = "${configDir}/plugins";
|
||||
pluginsCacheDir = "${pluginsDir}/cache";
|
||||
homeRelativePluginsCacheDir = "${configDir}/plugins/cache";
|
||||
rawSettings = if cfg.settings == null then { } else cfg.settings;
|
||||
skillSources = codexLib.mkSkillSources cfg.skills;
|
||||
helpers = codexLib.mkHelpers {
|
||||
inherit
|
||||
configDir
|
||||
homeRelativePluginsCacheDir
|
||||
pluginsCacheDir
|
||||
pluginsMarketplaceName
|
||||
skillsDir
|
||||
tomlFormat
|
||||
;
|
||||
};
|
||||
inherit (helpers)
|
||||
mkMarketplaceConfigEntry
|
||||
mkPersonalMarketplacePluginEntry
|
||||
mkPluginCachePath
|
||||
mkPluginConfigEntry
|
||||
mkPluginFileEntry
|
||||
mkProfileEntry
|
||||
mkRuleEntry
|
||||
mkSkillEntry
|
||||
mkTextOrPathEntry
|
||||
;
|
||||
|
||||
transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
|
||||
lib.mapAttrs (
|
||||
name: server:
|
||||
# NOTE: Convert shared programs.mcp fields to Codex config keys:
|
||||
# - file-backed env entries are wrapped in a shell script that sets environment variables before exec
|
||||
# - "headers" is renamed to "http_headers"
|
||||
# See: https://developers.openai.com/codex/mcp#other-configuration-options
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
exclude = [
|
||||
"headers"
|
||||
"type"
|
||||
];
|
||||
extraTransforms = [
|
||||
(s: s // lib.optionalAttrs (s.headers or { } != { }) { http_headers = s.headers; })
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
];
|
||||
}
|
||||
) config.programs.mcp.servers
|
||||
);
|
||||
|
||||
# TODO: remove this migration block in a future stateVersion once the
|
||||
# Codex 0.134 profile transition window has passed.
|
||||
hasLegacyProfileSettings =
|
||||
migrateLegacyProfiles && ((rawSettings ? profile) || (rawSettings ? profiles));
|
||||
legacyProfiles = lib.optionalAttrs (
|
||||
hasLegacyProfileSettings && builtins.isAttrs (rawSettings.profiles or null)
|
||||
) rawSettings.profiles;
|
||||
mergedProfiles = legacyProfiles // cfg.profiles;
|
||||
baseSettings =
|
||||
if hasLegacyProfileSettings then
|
||||
lib.removeAttrs rawSettings [
|
||||
"profile"
|
||||
"profiles"
|
||||
]
|
||||
else
|
||||
rawSettings;
|
||||
|
||||
generatedPluginSettings =
|
||||
lib.optionalAttrs (cfg.plugins != [ ] || cfg.marketplaces != { }) {
|
||||
features.plugins = true;
|
||||
}
|
||||
// lib.optionalAttrs (cfg.plugins != [ ]) {
|
||||
plugins = lib.listToAttrs (map mkPluginConfigEntry cfg.plugins);
|
||||
}
|
||||
// lib.optionalAttrs (cfg.marketplaces != { }) {
|
||||
marketplaces = lib.mapAttrs mkMarketplaceConfigEntry cfg.marketplaces;
|
||||
};
|
||||
mergedSettingsWithoutMcp = lib.recursiveUpdate baseSettings generatedPluginSettings;
|
||||
settingMcpServers = lib.attrByPath [ "mcp_servers" ] { } mergedSettingsWithoutMcp;
|
||||
mergedMcpServers = transformedMcpServers // settingMcpServers;
|
||||
|
||||
mergedSettings =
|
||||
mergedSettingsWithoutMcp
|
||||
// lib.optionalAttrs (mergedMcpServers != { }) { mcp_servers = mergedMcpServers; };
|
||||
hooksArePath = lib.hm.strings.isPathLike cfg.hooks;
|
||||
hooksAreDir = hooksArePath && lib.pathIsDirectory cfg.hooks;
|
||||
hooksJsonSource = if hooksAreDir then cfg.hooks + "/hooks.json" else cfg.hooks;
|
||||
in
|
||||
mkIf cfg.enable {
|
||||
warnings = lib.optional hasLegacyProfileSettings ''
|
||||
`programs.codex.settings.profile` and `programs.codex.settings.profiles`
|
||||
are no longer supported by Codex 0.134.0 and later. Home Manager
|
||||
now writes entries from `programs.codex.settings.profiles` to
|
||||
`CODEX_HOME/<name>.config.toml`. Move them to
|
||||
`programs.codex.profiles` and remove `programs.codex.settings.profile`.
|
||||
'';
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = (cfg.plugins == [ ] && cfg.marketplaces == { }) || isTomlConfig;
|
||||
message = "`programs.codex.plugins` and `programs.codex.marketplaces` require Codex 0.2.0 or later";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (
|
||||
plugin:
|
||||
!(lib.hm.strings.isPathLike plugin && !lib.isDerivation plugin) || lib.pathIsDirectory plugin
|
||||
) cfg.plugins;
|
||||
message = "`programs.codex.plugins` entries must be directories";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (
|
||||
marketplace:
|
||||
!(lib.hm.strings.isPathLike marketplace && !lib.isDerivation marketplace)
|
||||
|| lib.pathIsDirectory marketplace
|
||||
) (lib.attrValues cfg.marketplaces);
|
||||
message = "`programs.codex.marketplaces` entries must be directories";
|
||||
}
|
||||
{
|
||||
assertion = !lib.hm.strings.isPathLike cfg.skills || lib.pathIsDirectory cfg.skills;
|
||||
message = "`programs.codex.skills` must be a directory when set to a path";
|
||||
}
|
||||
{
|
||||
assertion = lib.all (content: !(lib.hm.strings.isPathLike content && lib.pathIsDirectory content)) (
|
||||
lib.attrValues cfg.rules
|
||||
);
|
||||
message = "`programs.codex.rules` attribute values must be files when set to paths";
|
||||
}
|
||||
{
|
||||
assertion = !(hooksAreDir && !builtins.pathExists hooksJsonSource);
|
||||
message = "`programs.codex.hooks` directory must contain a hooks.json file";
|
||||
}
|
||||
];
|
||||
|
||||
home = {
|
||||
packages = mkIf (cfg.package != null) [ cfg.package ];
|
||||
|
||||
# This is needed because codex will convert the symlinked plugin directory into
|
||||
# an actual directory (which will not be overwritten by home-manager)
|
||||
activation.cleanCodexPluginCache = lib.mkIf (cfg.plugins != [ ]) (
|
||||
lib.hm.dag.entryBefore [ "linkGeneration" ] (
|
||||
lib.concatMapStringsSep "\n" (
|
||||
plugin:
|
||||
let
|
||||
cachePath = lib.escapeShellArg (mkPluginCachePath plugin);
|
||||
in
|
||||
''
|
||||
path="$HOME"/${cachePath}
|
||||
if [ -d "$path" ] && [ ! -L "$path" ]; then
|
||||
rm -rf "$path"
|
||||
fi
|
||||
''
|
||||
) cfg.plugins
|
||||
)
|
||||
);
|
||||
|
||||
file = {
|
||||
"${configDir}/${configFileName}" = lib.mkIf (mergedSettings != { }) {
|
||||
source = settingsFormat.generate "codex-config" mergedSettings;
|
||||
};
|
||||
".agents/plugins/marketplace.json" = lib.mkIf (cfg.plugins != [ ]) {
|
||||
source = jsonFormat.generate "codex-home-manager-marketplace" {
|
||||
name = pluginsMarketplaceName;
|
||||
interface.displayName = "Home Manager";
|
||||
plugins = map mkPersonalMarketplacePluginEntry cfg.plugins;
|
||||
};
|
||||
};
|
||||
"${configDir}/hooks.json" = lib.mkIf (cfg.hooks != { }) {
|
||||
source =
|
||||
if hooksArePath then
|
||||
hooksJsonSource
|
||||
else
|
||||
jsonFormat.generate "codex-hooks" { inherit (cfg) hooks; };
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs hooksAreDir (
|
||||
lib.genAttrs [ "${configDir}/hooks" ] (_: {
|
||||
source = cfg.hooks;
|
||||
recursive = true;
|
||||
})
|
||||
)
|
||||
// lib.listToAttrs [ (mkTextOrPathEntry "${configDir}/AGENTS.md" cfg.context) ]
|
||||
// lib.optionalAttrs (cfg.contextOverride != null) (
|
||||
lib.listToAttrs [ (mkTextOrPathEntry "${configDir}/AGENTS.override.md" cfg.contextOverride) ]
|
||||
)
|
||||
// lib.mapAttrs' mkProfileEntry mergedProfiles
|
||||
// lib.mapAttrs' mkSkillEntry skillSources
|
||||
// lib.listToAttrs (map mkPluginFileEntry cfg.plugins)
|
||||
// lib.mapAttrs' mkRuleEntry cfg.rules;
|
||||
|
||||
sessionVariables = mkIf useXdgDirectories {
|
||||
CODEX_HOME = "${config.xdg.configHome}/codex";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
{ lib, pkgs }:
|
||||
let
|
||||
# TODO: Remove this workaround once Codex supports symlinked SKILL.md
|
||||
# files again. Upstream only supports symlinking the containing skill
|
||||
# directory today: https://github.com/openai/codex/issues/10470
|
||||
mkSkillDir =
|
||||
content:
|
||||
pkgs.writeTextDir "SKILL.md" (
|
||||
if lib.hm.strings.isPathLike content then builtins.readFile content else content
|
||||
);
|
||||
|
||||
mkPluginName =
|
||||
plugin:
|
||||
let
|
||||
manifestPath = plugin + "/.codex-plugin/plugin.json";
|
||||
manifestName =
|
||||
if !lib.isDerivation plugin && builtins.pathExists manifestPath then
|
||||
(builtins.fromJSON (builtins.readFile manifestPath)).name
|
||||
else
|
||||
null;
|
||||
fallbackName =
|
||||
if lib.isDerivation plugin then
|
||||
plugin.pname or (lib.getName plugin)
|
||||
else
|
||||
baseNameOf (toString plugin);
|
||||
in
|
||||
builtins.unsafeDiscardStringContext (if manifestName != null then manifestName else fallbackName);
|
||||
|
||||
mkPluginVersion =
|
||||
plugin:
|
||||
let
|
||||
manifestPath = plugin + "/.codex-plugin/plugin.json";
|
||||
manifestVersion =
|
||||
if !lib.isDerivation plugin && builtins.pathExists manifestPath then
|
||||
(builtins.fromJSON (builtins.readFile manifestPath)).version or null
|
||||
else
|
||||
null;
|
||||
fallbackVersion = plugin.version or "0.0.0";
|
||||
in
|
||||
builtins.unsafeDiscardStringContext (
|
||||
if manifestVersion != null then manifestVersion else fallbackVersion
|
||||
);
|
||||
|
||||
sanitizePathComponent =
|
||||
value: builtins.unsafeDiscardStringContext (lib.strings.sanitizeDerivationName value);
|
||||
|
||||
mkPluginPathName = plugin: sanitizePathComponent (mkPluginName plugin);
|
||||
mkPluginPathVersion = plugin: sanitizePathComponent (mkPluginVersion plugin);
|
||||
in
|
||||
{
|
||||
mkSkillSources =
|
||||
skills:
|
||||
if builtins.isAttrs skills then
|
||||
skills
|
||||
else if lib.hm.strings.isPathLike skills && lib.pathIsDirectory skills then
|
||||
lib.mapAttrs (name: _type: skills + "/${name}") (builtins.readDir skills)
|
||||
else
|
||||
{ };
|
||||
|
||||
mkHelpers =
|
||||
{
|
||||
configDir,
|
||||
homeRelativePluginsCacheDir,
|
||||
pluginsCacheDir,
|
||||
pluginsMarketplaceName,
|
||||
skillsDir,
|
||||
tomlFormat,
|
||||
}:
|
||||
let
|
||||
mkPluginCachePath =
|
||||
plugin:
|
||||
"${pluginsCacheDir}/${pluginsMarketplaceName}/${mkPluginPathName plugin}/${mkPluginPathVersion plugin}";
|
||||
in
|
||||
{
|
||||
inherit mkPluginCachePath;
|
||||
|
||||
mkMarketplaceConfigEntry = _name: content: {
|
||||
source_type = "local";
|
||||
source = "${content}";
|
||||
};
|
||||
|
||||
mkPersonalMarketplacePluginEntry = plugin: {
|
||||
name = mkPluginName plugin;
|
||||
source = {
|
||||
source = "local";
|
||||
path = "./${homeRelativePluginsCacheDir}/${pluginsMarketplaceName}/${mkPluginPathName plugin}/${mkPluginPathVersion plugin}";
|
||||
};
|
||||
policy = {
|
||||
installation = "AVAILABLE";
|
||||
authentication = "ON_INSTALL";
|
||||
};
|
||||
category = "Productivity";
|
||||
};
|
||||
|
||||
mkPluginConfigEntry =
|
||||
plugin:
|
||||
lib.nameValuePair "${mkPluginName plugin}@${pluginsMarketplaceName}" {
|
||||
enabled = true;
|
||||
};
|
||||
|
||||
mkPluginFileEntry =
|
||||
plugin:
|
||||
lib.nameValuePair (mkPluginCachePath plugin) {
|
||||
source = plugin;
|
||||
force = true;
|
||||
};
|
||||
|
||||
mkProfileEntry =
|
||||
name: settings:
|
||||
lib.nameValuePair "${configDir}/${name}.config.toml" {
|
||||
source = tomlFormat.generate "codex-${name}-config" settings;
|
||||
};
|
||||
|
||||
mkRuleEntry =
|
||||
name: content:
|
||||
lib.nameValuePair "${configDir}/rules/${name}.rules" (
|
||||
if lib.hm.strings.isPathLike content then { source = content; } else { text = content; }
|
||||
);
|
||||
|
||||
mkSkillEntry =
|
||||
name: content:
|
||||
if lib.hm.strings.isPathLike content && lib.pathIsDirectory content then
|
||||
lib.nameValuePair "${skillsDir}/${name}" {
|
||||
source = content;
|
||||
}
|
||||
else
|
||||
lib.nameValuePair "${skillsDir}/${name}" {
|
||||
source = mkSkillDir content;
|
||||
};
|
||||
|
||||
mkTextOrPathEntry =
|
||||
path: content:
|
||||
if lib.isPath content then
|
||||
lib.nameValuePair path { source = content; }
|
||||
else
|
||||
lib.nameValuePair path (lib.mkIf (content != "") { text = content; });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
{ lib, pkgs, ... }:
|
||||
let
|
||||
tomlFormat = pkgs.formats.toml { };
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
in
|
||||
{
|
||||
options.programs.codex = {
|
||||
enable = lib.mkEnableOption "Lightweight coding agent that runs in your terminal";
|
||||
|
||||
package = lib.mkPackageOption pkgs "codex" { nullable = true; };
|
||||
|
||||
enableMcpIntegration = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to integrate the MCP server config from
|
||||
{option}`programs.mcp.servers` into
|
||||
{option}`programs.codex.settings.mcp_servers`.
|
||||
|
||||
Note: Settings defined in {option}`programs.mcp.servers` are merged
|
||||
with {option}`programs.codex.settings.mcp_servers`, with settings-based
|
||||
values taking precedence.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
# NOTE: `yaml` type supports null, using `nullOr` for backwards compatibility period
|
||||
type = lib.types.nullOr tomlFormat.type;
|
||||
description = ''
|
||||
Configuration written to {file}`CODEX_HOME/config.toml` (0.2.0+)
|
||||
or {file}`~/.codex/config.yaml` (<0.2.0). Per default {env}`CODEX_HOME`
|
||||
defaults to ~/.codex.
|
||||
See <https://developers.openai.com/codex/config-reference> for supported values.
|
||||
'';
|
||||
default = { };
|
||||
defaultText = lib.literalExpression "{ }";
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
model = "gemma3:latest";
|
||||
model_provider = "ollama";
|
||||
model_providers = {
|
||||
ollama = {
|
||||
name = "Ollama";
|
||||
base_url = "http://localhost:11434/v1";
|
||||
env_key = "OLLAMA_API_KEY";
|
||||
};
|
||||
};
|
||||
mcp_servers = {
|
||||
context7 = {
|
||||
command = "npx";
|
||||
args = [
|
||||
"-y"
|
||||
"@upstash/context7-mcp"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
profiles = lib.mkOption {
|
||||
type = lib.types.attrsOf tomlFormat.type;
|
||||
default = { };
|
||||
description = ''
|
||||
Named Codex configuration profiles written to
|
||||
{file}`CODEX_HOME/<name>.config.toml`.
|
||||
|
||||
These profiles are selected with {command}`codex --profile <name>`.
|
||||
Codex 0.134.0 and later no longer reads profile settings from
|
||||
{option}`programs.codex.settings.profiles`, and the top-level
|
||||
{option}`programs.codex.settings.profile` selector is no longer
|
||||
supported.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
deep-review = {
|
||||
model = "gpt-5.5";
|
||||
model_reasoning_effort = "xhigh";
|
||||
approval_policy = "on-request";
|
||||
sandbox_mode = "workspace-write";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
context = lib.mkOption {
|
||||
type = lib.types.either lib.types.lines lib.types.path;
|
||||
description = ''
|
||||
Global context for Codex.
|
||||
|
||||
The value is either:
|
||||
- Inline content as a string
|
||||
- A path to a file containing the content
|
||||
|
||||
The configured content is written to
|
||||
{file}`CODEX_HOME/AGENTS.md`.
|
||||
'';
|
||||
default = "";
|
||||
example = lib.literalExpression ''
|
||||
'''
|
||||
- Always respond with emojis
|
||||
- Only use git commands when explicitly requested
|
||||
'''
|
||||
'';
|
||||
};
|
||||
|
||||
contextOverride = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.either lib.types.lines lib.types.path);
|
||||
description = ''
|
||||
Global override context for Codex.
|
||||
|
||||
This has the same value format as {option}`programs.codex.context`,
|
||||
but writes {file}`CODEX_HOME/AGENTS.override.md`.
|
||||
|
||||
Codex prefers {file}`AGENTS.override.md` over
|
||||
{file}`AGENTS.md` in the same directory.
|
||||
'';
|
||||
default = null;
|
||||
example = lib.literalExpression ''
|
||||
'''
|
||||
- Temporarily ignore default global guidance
|
||||
- Prefer brief answers while debugging
|
||||
'''
|
||||
'';
|
||||
};
|
||||
|
||||
hooks = lib.mkOption {
|
||||
type = lib.types.either jsonFormat.type lib.types.path;
|
||||
default = { };
|
||||
description = ''
|
||||
Lifecycle hook events written to {file}`CODEX_HOME/hooks.json`.
|
||||
|
||||
This option can either be a hook event attribute set, a path to a
|
||||
complete hooks JSON file, or a path to a hook bundle directory
|
||||
containing {file}`hooks.json` and supporting hook scripts.
|
||||
|
||||
Attribute set values use the same event structure as
|
||||
{option}`programs.codex.settings.hooks` and are written under the
|
||||
top-level `hooks` key expected by Codex's JSON hooks file.
|
||||
|
||||
Directory values install {file}`hooks.json` to
|
||||
{file}`CODEX_HOME/hooks.json` and install the full directory to
|
||||
the active Codex home hooks directory. In the default layout, commands
|
||||
can reference bundled scripts with paths such as
|
||||
{file}`$HOME/.codex/hooks/my-hook`. When
|
||||
{option}`home.preferXdgDirectories` is enabled, use
|
||||
{file}`$CODEX_HOME/hooks/my-hook`.
|
||||
|
||||
Hooks can also be configured inline through
|
||||
{option}`programs.codex.settings.hooks`; prefer using only one hook
|
||||
representation per layer.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
PreToolUse = [
|
||||
{
|
||||
matcher = "^Bash$";
|
||||
hooks = [
|
||||
{
|
||||
type = "command";
|
||||
command = "/usr/local/bin/codex-pre-tool-use";
|
||||
timeout = 30;
|
||||
statusMessage = "Checking Bash command";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = lib.mkOption {
|
||||
type = with lib.types; listOf (either package path);
|
||||
default = [ ];
|
||||
description = ''
|
||||
List of plugins to use when running Codex.
|
||||
Each entry is either:
|
||||
- A path to the plugin directory
|
||||
- The plugin package, whether a nix package or the output of a fetcher
|
||||
Plugins are installed into Codex's plugin cache and enabled through
|
||||
{file}`CODEX_HOME/config.toml`.
|
||||
|
||||
Warning: If using a derivation as the source for a plugin, make sure that
|
||||
the derivation name matches the name of the plugin in the manifest file.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
./my-local-plugin
|
||||
(fetchFromGitHub {
|
||||
owner = "some-github-org";
|
||||
repo = "codex-plugin";
|
||||
rev = "779a68ebc2a75e4a184d2c87e5a43a758e6458a1";
|
||||
sha256 = "228fdd7e5908ea1d2f65218ecd9c71e1eefa0834d200d55fbb8bf8b5563acec0";
|
||||
})
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
marketplaces = lib.mkOption {
|
||||
type = with lib.types; attrsOf (either package path);
|
||||
default = { };
|
||||
description = ''
|
||||
Custom marketplaces for Codex plugins.
|
||||
The attribute name becomes the marketplace name, and the value is either:
|
||||
- A path to the marketplace directory
|
||||
- The marketplace package, whether a nix package or the output of a fetcher
|
||||
|
||||
Marketplaces are configured through {file}`CODEX_HOME/config.toml`.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
local-marketplace = ./my-local-marketplace;
|
||||
gh-marketplace = fetchFromGitHub {
|
||||
owner = "some-github-org";
|
||||
repo = "codex-marketplace";
|
||||
rev = "8a873a220b8427b25b03ce1a821593a24e098c34";
|
||||
sha256 = "5c2dce95122b5bb73fa547edabbb6c3061c2d193d11e51faecd4d22659e67279";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
skills = lib.mkOption {
|
||||
type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path;
|
||||
default = { };
|
||||
description = ''
|
||||
Custom skills for Codex.
|
||||
|
||||
This option can be either:
|
||||
- An attribute set defining skills
|
||||
- A path to a directory containing skill folders
|
||||
|
||||
If an attribute set is used, the attribute name becomes the
|
||||
skill directory name, and the value is either:
|
||||
- Inline content as a string (creates a generated skill directory at {file}`<skills-dir>/<name>/`)
|
||||
- A path to a file (creates a generated skill directory at {file}`<skills-dir>/<name>/`)
|
||||
- A path to a directory (symlinks {file}`<skills-dir>/<name>/` to that directory)
|
||||
|
||||
If a path is used, it is expected to contain one folder per
|
||||
skill name, each containing a {file}`SKILL.md`. Each top-level
|
||||
skill entry is symlinked into {file}`<skills-dir>/`, leaving
|
||||
{file}`<skills-dir>/` itself as a normal directory so unmanaged
|
||||
skills can coexist.
|
||||
|
||||
Home Manager manages skills under {file}`CODEX_HOME/skills`
|
||||
(typically {file}`~/.codex/skills`, or
|
||||
{file}`~/.config/codex/skills` when
|
||||
{option}`home.preferXdgDirectories` is enabled).
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
pdf-processing = '''
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
```
|
||||
''';
|
||||
data-analysis = ./skills/data-analysis;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
rules = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path);
|
||||
default = { };
|
||||
description = ''
|
||||
Codex rules files to manage under {file}`CODEX_HOME/rules/`.
|
||||
|
||||
The attribute name becomes the filename, with a {file}`.rules`
|
||||
extension added automatically. The value is either:
|
||||
- Inline content as a string
|
||||
- A path to an existing rules file
|
||||
|
||||
This is useful for declaratively managing persistent
|
||||
`prefix_rule()` definitions, including the default
|
||||
{file}`default.rules` allow-list Codex writes when you accept
|
||||
recurring approvals interactively.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
default = "prefix_rule(pattern = [\"nix\", \"build\"], decision = \"allow\")\n";
|
||||
github = ./codex/github.rules;
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
mkEnableOption
|
||||
mkPackageOption
|
||||
mkIf
|
||||
mkAfter
|
||||
getExe
|
||||
;
|
||||
|
||||
cfg = config.programs.devenv;
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
leiserfg
|
||||
];
|
||||
|
||||
options.programs.devenv = {
|
||||
enable = mkEnableOption "devenv, Fast, Declarative, Reproducible and Composable Developer Environments using Nix";
|
||||
|
||||
package = mkPackageOption pkgs "devenv" { };
|
||||
|
||||
enableBashIntegration = lib.hm.shell.mkBashIntegrationOption { inherit config; };
|
||||
|
||||
enableFishIntegration = lib.hm.shell.mkFishIntegrationOption { inherit config; };
|
||||
|
||||
enableNushellIntegration = lib.hm.shell.mkNushellIntegrationOption { inherit config; };
|
||||
|
||||
enableZshIntegration = lib.hm.shell.mkZshIntegrationOption { inherit config; };
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
home.packages = [ cfg.package ];
|
||||
programs = {
|
||||
bash.initExtra = mkIf cfg.enableBashIntegration (mkAfter ''
|
||||
eval "$(${getExe cfg.package} hook bash)"
|
||||
'');
|
||||
|
||||
fish.interactiveShellInit = mkIf cfg.enableFishIntegration (mkAfter ''
|
||||
${getExe cfg.package} hook fish | source
|
||||
'');
|
||||
|
||||
zsh.initContent = mkIf cfg.enableZshIntegration ''
|
||||
eval "$(${getExe cfg.package} hook zsh)"
|
||||
'';
|
||||
|
||||
nushell = mkIf cfg.enableNushellIntegration {
|
||||
extraConfig = "source ${
|
||||
pkgs.runCommand "devenv-nushell-config.nu" { } ''
|
||||
${getExe cfg.package} hook nu > $out
|
||||
''
|
||||
} ";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -188,10 +188,14 @@ in
|
||||
programs.jujutsu.settings.ui.diff-formatter = [
|
||||
(lib.getExe cfg.package)
|
||||
]
|
||||
++ (lib.cli.toCommandLineGNU { } cfg.options)
|
||||
++ (lib.cli.toCommandLineGNU { } (
|
||||
cfg.options
|
||||
// {
|
||||
color = "always";
|
||||
sort-paths = true;
|
||||
}
|
||||
))
|
||||
++ [
|
||||
"--color=always"
|
||||
"--sort-paths"
|
||||
"$left"
|
||||
"$right"
|
||||
];
|
||||
|
||||
@@ -147,9 +147,19 @@ in
|
||||
$env.config.hooks.pre_prompt?
|
||||
| default []
|
||||
| append {||
|
||||
${getExe cfg.package} export json
|
||||
| from json --strict
|
||||
| default {}
|
||||
let direnv = (
|
||||
${getExe cfg.package} export json
|
||||
| from json --strict
|
||||
| default {}
|
||||
)
|
||||
|
||||
for key in ($direnv | columns) {
|
||||
if ($direnv | get $key) == null {
|
||||
hide-env --ignore-errors $key
|
||||
}
|
||||
}
|
||||
|
||||
$direnv
|
||||
| items {|key, value|
|
||||
let value = do (
|
||||
{
|
||||
@@ -164,6 +174,7 @@ in
|
||||
) $value
|
||||
return [ $key $value ]
|
||||
}
|
||||
| where {|pair| $pair.1 != null }
|
||||
| into record
|
||||
| load-env
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ let
|
||||
length
|
||||
literalExpression
|
||||
mapAttrsToList
|
||||
mkDefault
|
||||
mkIf
|
||||
mkMerge
|
||||
mkOption
|
||||
@@ -54,12 +55,17 @@ let
|
||||
profiles =
|
||||
lib.flip lib.mapAttrs' cfg.profiles (
|
||||
_: profile:
|
||||
lib.nameValuePair "Profile${toString profile.id}" {
|
||||
Name = profile.name;
|
||||
Path = if isDarwin then "Profiles/${profile.path}" else profile.path;
|
||||
IsRelative = 1;
|
||||
Default = if profile.isDefault then 1 else 0;
|
||||
}
|
||||
lib.nameValuePair "Profile${toString profile.id}" (
|
||||
{
|
||||
Name = profile.name;
|
||||
Path = if isDarwin then "Profiles/${profile.path}" else profile.path;
|
||||
IsRelative = 1;
|
||||
Default = if profile.isDefault then 1 else 0;
|
||||
}
|
||||
// (lib.optionalAttrs (profile.storeId != null) {
|
||||
StoreID = profile.storeId;
|
||||
})
|
||||
)
|
||||
)
|
||||
// {
|
||||
General = {
|
||||
@@ -450,6 +456,23 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
storeId = mkOption {
|
||||
type = types.nullOr (types.strMatching "[0-9a-f]{8}");
|
||||
default = null;
|
||||
description = ''
|
||||
Store ID. Either null, or the first segment of a UUID string.
|
||||
|
||||
If this value is set, then profile.ini is created
|
||||
with predictable StoreIDs.
|
||||
|
||||
This is useful to keep the StoreID static,
|
||||
which helps to bridge the firefox profiles and the new
|
||||
firefox switchable profile implementations.
|
||||
|
||||
This should be set to a string of 8 lowercase hex characters.
|
||||
'';
|
||||
};
|
||||
|
||||
preConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
@@ -884,6 +907,16 @@ in
|
||||
};
|
||||
|
||||
config = {
|
||||
settings = mkMerge [
|
||||
(mkIf (config.userChrome != "") {
|
||||
"toolkit.legacyUserProfileCustomizations.stylesheets" = mkDefault true;
|
||||
})
|
||||
|
||||
(mkIf (config.storeId != null) {
|
||||
"toolkit.profiles.storeID" = mkDefault config.storeId;
|
||||
})
|
||||
];
|
||||
|
||||
assertions = [
|
||||
(mkNoDuplicateAssertion config.containers "container")
|
||||
{
|
||||
@@ -896,6 +929,15 @@ in
|
||||
to acknowledge this.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
(config.storeId == null)
|
||||
|| ((config.settings."toolkit.profiles.storeID" or config.storeId) == config.storeId);
|
||||
message = ''
|
||||
${moduleName}.profiles.${name}.storeId must match
|
||||
${moduleName}.profiles.${name}.settings."toolkit.profiles.storeID"
|
||||
'';
|
||||
}
|
||||
]
|
||||
++ (builtins.concatMap (
|
||||
{
|
||||
@@ -1085,6 +1127,30 @@ in
|
||||
}
|
||||
|
||||
(mkNoDuplicateAssertion cfg.profiles "profile")
|
||||
|
||||
(
|
||||
let
|
||||
profilesWithStoreId = lib.filterAttrs (_: profile: profile.storeId != null) cfg.profiles;
|
||||
|
||||
duplicateStoreIds = lib.filterAttrs (_storeId: profileNames: length profileNames != 1) (
|
||||
lib.zipAttrs (
|
||||
mapAttrsToList (profileName: profile: {
|
||||
"${profile.storeId}" = profileName;
|
||||
}) profilesWithStoreId
|
||||
)
|
||||
);
|
||||
|
||||
mkMsg =
|
||||
storeId: profileNames: " - StoreID ${storeId} is used by " + (concatStringsSep ", " profileNames);
|
||||
in
|
||||
{
|
||||
assertion = cfg.profiles == { } || duplicateStoreIds == { };
|
||||
message = ''
|
||||
Must not have duplicate ${appName} profile StoreIDs:
|
||||
${concatStringsSep "\n" (mapAttrsToList mkMsg duplicateStoreIds)}
|
||||
'';
|
||||
}
|
||||
)
|
||||
]
|
||||
++ (lib.concatMap (profile: profile.assertions) (attrValues cfg.profiles));
|
||||
|
||||
|
||||
@@ -658,17 +658,21 @@ in
|
||||
++ lib.filter (p: p != null) (
|
||||
map (outName: package.${outName} or null) config.home.extraOutputsToInstall
|
||||
);
|
||||
nativeBuildInputs = [ pkgs.python3 ];
|
||||
buildInputs = [ cfg.package ];
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
cfg.package
|
||||
];
|
||||
preferLocalBuild = true;
|
||||
}
|
||||
''
|
||||
# The generator script is embedded in the fish binary, so extract it.
|
||||
generator=$PWD/create_manpage_completions.py
|
||||
fish --no-config -c 'status get-file tools/create_manpage_completions.py' > "$generator"
|
||||
|
||||
mkdir -p $out
|
||||
for src in $srcs; do
|
||||
if [ -d $src/share/man ]; then
|
||||
find -L $src/share/man -type f \
|
||||
-exec python ${cfg.package}/share/fish/tools/create_manpage_completions.py --directory $out {} + \
|
||||
> /dev/null
|
||||
find -L $src/share/man -type f -exec python "$generator" --directory $out {} + > /dev/null
|
||||
fi
|
||||
done
|
||||
'';
|
||||
|
||||
+436
-183
@@ -1,6 +1,7 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
options,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
@@ -8,6 +9,7 @@ let
|
||||
inherit (lib)
|
||||
getExe
|
||||
literalExpression
|
||||
mkAfter
|
||||
mkIf
|
||||
mkOption
|
||||
mkOrder
|
||||
@@ -16,210 +18,461 @@ let
|
||||
|
||||
cfg = config.programs.fzf;
|
||||
|
||||
renderedColors =
|
||||
colors: lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}:${value}") colors);
|
||||
shells = [
|
||||
"bash"
|
||||
"fish"
|
||||
"nushell"
|
||||
"zsh"
|
||||
];
|
||||
|
||||
hasShellIntegrationEmbedded = lib.versionAtLeast cfg.package.version "0.48.0";
|
||||
|
||||
bashIntegration =
|
||||
if hasShellIntegrationEmbedded then
|
||||
''
|
||||
if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
|
||||
eval "$(${getExe cfg.package} --bash)"
|
||||
fi
|
||||
''
|
||||
else
|
||||
''
|
||||
if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
|
||||
. ${cfg.package}/share/fzf/completion.bash
|
||||
. ${cfg.package}/share/fzf/key-bindings.bash
|
||||
fi
|
||||
'';
|
||||
|
||||
zshIntegration =
|
||||
if hasShellIntegrationEmbedded then
|
||||
''
|
||||
if [[ $options[zle] = on ]]; then
|
||||
source <(${getExe cfg.package} --zsh)
|
||||
fi
|
||||
''
|
||||
else
|
||||
''
|
||||
if [[ $options[zle] = on ]]; then
|
||||
. ${cfg.package}/share/fzf/completion.zsh
|
||||
. ${cfg.package}/share/fzf/key-bindings.zsh
|
||||
fi
|
||||
'';
|
||||
|
||||
fishIntegration =
|
||||
if hasShellIntegrationEmbedded then
|
||||
''
|
||||
${getExe cfg.package} --fish | source
|
||||
''
|
||||
else
|
||||
''
|
||||
source ${cfg.package}/share/fzf/key-bindings.fish && fzf_key_bindings
|
||||
'';
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ khaneliman ];
|
||||
|
||||
imports = [
|
||||
(lib.mkRemovedOptionModule [
|
||||
"programs"
|
||||
"fzf"
|
||||
"historyWidgetCommand"
|
||||
] "This option is no longer supported by fzf.")
|
||||
];
|
||||
options.programs.fzf = {
|
||||
enable = lib.mkEnableOption "fzf - a command-line fuzzy finder";
|
||||
imports =
|
||||
let
|
||||
mkRenamedFzfOption =
|
||||
oldName: newPath:
|
||||
lib.mkRenamedOptionModule [ "programs" "fzf" oldName ] (
|
||||
[
|
||||
"programs"
|
||||
"fzf"
|
||||
]
|
||||
++ newPath
|
||||
);
|
||||
in
|
||||
[
|
||||
(mkRenamedFzfOption "fileWidgetCommand" [
|
||||
"fileWidget"
|
||||
"command"
|
||||
])
|
||||
(mkRenamedFzfOption "fileWidgetOptions" [
|
||||
"fileWidget"
|
||||
"options"
|
||||
])
|
||||
(mkRenamedFzfOption "changeDirWidgetCommand" [
|
||||
"changeDirWidget"
|
||||
"command"
|
||||
])
|
||||
(mkRenamedFzfOption "changeDirWidgetOptions" [
|
||||
"changeDirWidget"
|
||||
"options"
|
||||
])
|
||||
(mkRenamedFzfOption "historyWidgetOptions" [
|
||||
"historyWidget"
|
||||
"options"
|
||||
])
|
||||
(mkRenamedFzfOption "historyWidgetCommand" [
|
||||
"historyWidget"
|
||||
"command"
|
||||
])
|
||||
];
|
||||
|
||||
package = lib.mkPackageOption pkgs "fzf" { };
|
||||
|
||||
defaultCommand = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "fd --type f";
|
||||
description = ''
|
||||
The command that gets executed as the default source for fzf
|
||||
when running.
|
||||
'';
|
||||
};
|
||||
|
||||
defaultOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"--height 40%"
|
||||
"--border"
|
||||
];
|
||||
description = ''
|
||||
Extra command line options given to fzf by default.
|
||||
'';
|
||||
};
|
||||
|
||||
fileWidgetCommand = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "fd --type f";
|
||||
description = ''
|
||||
The command that gets executed as the source for fzf for the
|
||||
CTRL-T keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
fileWidgetOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "--preview 'head {}'" ];
|
||||
description = ''
|
||||
Command line options for the CTRL-T keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
changeDirWidgetCommand = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "fd --type d";
|
||||
description = ''
|
||||
The command that gets executed as the source for fzf for the
|
||||
ALT-C keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
changeDirWidgetOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "--preview 'tree -C {} | head -200'" ];
|
||||
description = ''
|
||||
Command line options for the ALT-C keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
historyWidgetOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"--sort"
|
||||
"--exact"
|
||||
];
|
||||
description = ''
|
||||
Command line options for the CTRL-R keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
colors = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
options.programs.fzf =
|
||||
let
|
||||
mkWidgetShellOverrides =
|
||||
{
|
||||
bg = "#1e1e1e";
|
||||
"bg+" = "#1e1e1e";
|
||||
fg = "#d4d4d4";
|
||||
"fg+" = "#d4d4d4";
|
||||
commandExample ? null,
|
||||
commandDescription ? null,
|
||||
optionsExample,
|
||||
...
|
||||
}:
|
||||
lib.genAttrs shells (
|
||||
_shell:
|
||||
lib.optionalAttrs (commandDescription != null) {
|
||||
command = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = commandExample;
|
||||
description = ''
|
||||
Shell-specific override for this widget command.
|
||||
|
||||
If this is `null`, the global command is used.
|
||||
'';
|
||||
};
|
||||
}
|
||||
// {
|
||||
options = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
example = optionsExample;
|
||||
description = ''
|
||||
Shell-specific override for this widget's command line
|
||||
options.
|
||||
|
||||
If this is `null`, the global options are used.
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
mkWidgetOption =
|
||||
args@{
|
||||
commandExample ? null,
|
||||
commandDescription ? null,
|
||||
optionsExample,
|
||||
optionsDescription,
|
||||
}:
|
||||
lib.optionalAttrs (commandDescription != null) {
|
||||
command = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = commandExample;
|
||||
description = commandDescription;
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Color scheme options added to `FZF_DEFAULT_OPTS`. See
|
||||
<https://github.com/junegunn/fzf/wiki/Color-schemes>
|
||||
for documentation.
|
||||
'';
|
||||
};
|
||||
// {
|
||||
options = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = optionsExample;
|
||||
description = optionsDescription;
|
||||
};
|
||||
}
|
||||
// mkWidgetShellOverrides args;
|
||||
in
|
||||
{
|
||||
enable = lib.mkEnableOption "fzf - a command-line fuzzy finder";
|
||||
|
||||
tmux = {
|
||||
enableShellIntegration = lib.mkEnableOption ''
|
||||
setting `FZF_TMUX=1` which causes shell integration to use fzf-tmux
|
||||
'';
|
||||
package = lib.mkPackageOption pkgs "fzf" { };
|
||||
|
||||
shellIntegrationOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "-d 40%" ];
|
||||
defaultCommand = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "fd --type f";
|
||||
description = ''
|
||||
If {option}`programs.fzf.tmux.enableShellIntegration` is set to `true`,
|
||||
shell integration will use these options for fzf-tmux.
|
||||
See {command}`fzf-tmux --help` for available options.
|
||||
The command that gets executed as the default source for fzf
|
||||
when running.
|
||||
'';
|
||||
};
|
||||
|
||||
defaultOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"--height 40%"
|
||||
"--border"
|
||||
];
|
||||
description = ''
|
||||
Extra command line options given to fzf by default.
|
||||
'';
|
||||
};
|
||||
|
||||
fileWidget = mkWidgetOption {
|
||||
commandExample = "fd --type f";
|
||||
commandDescription = ''
|
||||
The command that gets executed as the source for fzf for the
|
||||
CTRL-T keybinding.
|
||||
'';
|
||||
optionsExample = [ "--preview 'head {}'" ];
|
||||
optionsDescription = ''
|
||||
Command line options for the CTRL-T keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
changeDirWidget = mkWidgetOption {
|
||||
commandExample = "fd --type d";
|
||||
commandDescription = ''
|
||||
The command that gets executed as the source for fzf for the
|
||||
ALT-C keybinding.
|
||||
'';
|
||||
optionsExample = [ "--preview 'tree -C {} | head -200'" ];
|
||||
optionsDescription = ''
|
||||
Command line options for the ALT-C keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
historyWidget = mkWidgetOption {
|
||||
commandExample = "";
|
||||
commandDescription = ''
|
||||
The command that gets executed as the source for fzf for the
|
||||
CTRL-R keybinding.
|
||||
|
||||
An empty string disables the CTRL-R binding, which is the supported
|
||||
way to yield CTRL-R to a history manager such as Atuin or McFly.
|
||||
Non-empty custom commands require fzf 0.66.0 or later but are not
|
||||
supported by fzf yet and currently print a shell startup warning.
|
||||
'';
|
||||
optionsExample = [
|
||||
"--sort"
|
||||
"--exact"
|
||||
];
|
||||
optionsDescription = ''
|
||||
Command line options for the CTRL-R keybinding.
|
||||
'';
|
||||
};
|
||||
|
||||
colors = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
bg = "#1e1e1e";
|
||||
"bg+" = "#1e1e1e";
|
||||
fg = "#d4d4d4";
|
||||
"fg+" = "#d4d4d4";
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Color scheme options added to `FZF_DEFAULT_OPTS`. See
|
||||
<https://github.com/junegunn/fzf/wiki/Color-schemes>
|
||||
for documentation.
|
||||
'';
|
||||
};
|
||||
|
||||
tmux = {
|
||||
enableShellIntegration = lib.mkEnableOption ''
|
||||
setting `FZF_TMUX=1` which causes shell integration to use fzf-tmux
|
||||
'';
|
||||
|
||||
shellIntegrationOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "-d 40%" ];
|
||||
description = ''
|
||||
If {option}`programs.fzf.tmux.enableShellIntegration` is set to `true`,
|
||||
shell integration will use these options for fzf-tmux.
|
||||
See {command}`fzf-tmux --help` for available options.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
enableBashIntegration = lib.hm.shell.mkBashIntegrationOption { inherit config; };
|
||||
|
||||
enableFishIntegration = lib.hm.shell.mkFishIntegrationOption { inherit config; };
|
||||
|
||||
enableZshIntegration = lib.hm.shell.mkZshIntegrationOption { inherit config; };
|
||||
|
||||
enableNushellIntegration = lib.hm.shell.mkNushellIntegrationOption { inherit config; };
|
||||
};
|
||||
|
||||
enableBashIntegration = lib.hm.shell.mkBashIntegrationOption { inherit config; };
|
||||
config =
|
||||
let
|
||||
shellFzfEnvVars =
|
||||
shell:
|
||||
lib.filterAttrs (_n: v: v != null) {
|
||||
FZF_ALT_C_COMMAND = cfg.changeDirWidget.${shell}.command;
|
||||
FZF_ALT_C_OPTS = cfg.changeDirWidget.${shell}.options;
|
||||
FZF_CTRL_R_COMMAND = cfg.historyWidget.${shell}.command;
|
||||
FZF_CTRL_R_OPTS = cfg.historyWidget.${shell}.options;
|
||||
FZF_CTRL_T_COMMAND = cfg.fileWidget.${shell}.command;
|
||||
FZF_CTRL_T_OPTS = cfg.fileWidget.${shell}.options;
|
||||
};
|
||||
|
||||
enableFishIntegration = lib.hm.shell.mkFishIntegrationOption { inherit config; };
|
||||
|
||||
enableZshIntegration = lib.hm.shell.mkZshIntegrationOption { inherit config; };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
home.packages = [ cfg.package ];
|
||||
|
||||
home.sessionVariables = lib.mapAttrs (_n: toString) (
|
||||
lib.filterAttrs (_n: v: v != [ ] && v != null) {
|
||||
FZF_ALT_C_COMMAND = cfg.changeDirWidgetCommand;
|
||||
FZF_ALT_C_OPTS = cfg.changeDirWidgetOptions;
|
||||
FZF_CTRL_R_OPTS = cfg.historyWidgetOptions;
|
||||
FZF_CTRL_T_COMMAND = cfg.fileWidgetCommand;
|
||||
FZF_CTRL_T_OPTS = cfg.fileWidgetOptions;
|
||||
fzfEnvVars = lib.filterAttrs (_n: v: v != [ ] && v != null) {
|
||||
FZF_ALT_C_COMMAND = cfg.changeDirWidget.command;
|
||||
FZF_ALT_C_OPTS = cfg.changeDirWidget.options;
|
||||
FZF_CTRL_R_COMMAND = cfg.historyWidget.command;
|
||||
FZF_CTRL_R_OPTS = cfg.historyWidget.options;
|
||||
FZF_CTRL_T_COMMAND = cfg.fileWidget.command;
|
||||
FZF_CTRL_T_OPTS = cfg.fileWidget.options;
|
||||
FZF_DEFAULT_COMMAND = cfg.defaultCommand;
|
||||
FZF_DEFAULT_OPTS =
|
||||
cfg.defaultOptions ++ lib.optionals (cfg.colors != { }) [ "--color ${renderedColors cfg.colors}" ];
|
||||
cfg.defaultOptions
|
||||
++ lib.optionals (cfg.colors != { }) [
|
||||
"--color ${
|
||||
lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}:${value}") cfg.colors)
|
||||
}"
|
||||
];
|
||||
FZF_TMUX = if cfg.tmux.enableShellIntegration then "1" else null;
|
||||
FZF_TMUX_OPTS = cfg.tmux.shellIntegrationOptions;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
# Note, since fzf unconditionally binds C-r we use `mkOrder` to make the
|
||||
# initialization show up a bit earlier. This is to make initialization of
|
||||
# other history managers, like mcfly or atuin, take precedence.
|
||||
programs.bash.initExtra = mkIf cfg.enableBashIntegration (mkOrder 200 bashIntegration);
|
||||
in
|
||||
mkIf cfg.enable {
|
||||
warnings =
|
||||
let
|
||||
shellIntegrationOptions = {
|
||||
bash = "enableBashIntegration";
|
||||
fish = "enableFishIntegration";
|
||||
nushell = "enableNushellIntegration";
|
||||
zsh = "enableZshIntegration";
|
||||
};
|
||||
|
||||
# Note, since fzf unconditionally binds C-r we use `mkOrder` to make the
|
||||
# initialization show up a bit earlier. This is to make initialization of
|
||||
# other history managers, like mcfly or atuin, take precedence.
|
||||
# Still needs to be initialized after oh-my-zsh (order 800), otherwise
|
||||
# omz will take precedence.
|
||||
programs.zsh.initContent = mkIf cfg.enableZshIntegration (mkOrder 910 zshIntegration);
|
||||
integrationCtrlRActive =
|
||||
shell: programConfig:
|
||||
config.programs.${shell}.enable
|
||||
&& programConfig.enable
|
||||
&& programConfig.${shellIntegrationOptions.${shell}};
|
||||
|
||||
programs.fish.interactiveShellInit = mkIf cfg.enableFishIntegration (mkOrder 200 fishIntegration);
|
||||
};
|
||||
fzfCtrlRActive =
|
||||
shell:
|
||||
let
|
||||
shellCommand = cfg.historyWidget.${shell}.command;
|
||||
effectiveCommand = if shellCommand != null then shellCommand else cfg.historyWidget.command;
|
||||
in
|
||||
integrationCtrlRActive shell cfg && effectiveCommand != "";
|
||||
|
||||
atuinCtrlRActive =
|
||||
shell:
|
||||
integrationCtrlRActive shell config.programs.atuin
|
||||
&& !(builtins.elem "--disable-ctrl-r" config.programs.atuin.flags);
|
||||
|
||||
mcflyCtrlRActive = shell: shell != "nushell" && integrationCtrlRActive shell config.programs.mcfly;
|
||||
|
||||
ctrlRConflictShells = builtins.filter (
|
||||
shell: fzfCtrlRActive shell && (atuinCtrlRActive shell || mcflyCtrlRActive shell)
|
||||
) shells;
|
||||
|
||||
showOptionFiles = option: lib.showFiles option.files;
|
||||
|
||||
fzfCtrlRDefinition =
|
||||
shell:
|
||||
if cfg.historyWidget.${shell}.command != null then
|
||||
"`programs.fzf.historyWidget.${shell}.command` defined in ${
|
||||
showOptionFiles options.programs.fzf.historyWidget.${shell}.command
|
||||
}"
|
||||
else if cfg.historyWidget.command != null then
|
||||
"`programs.fzf.historyWidget.command` defined in ${showOptionFiles options.programs.fzf.historyWidget.command}"
|
||||
else
|
||||
"`programs.fzf.enable` defined in ${showOptionFiles options.programs.fzf.enable}";
|
||||
|
||||
atuinCtrlRDefinition =
|
||||
if config.programs.atuin.flags != [ ] then
|
||||
"`programs.atuin.flags` defined in ${showOptionFiles options.programs.atuin.flags}"
|
||||
else
|
||||
"`programs.atuin.enable` defined in ${showOptionFiles options.programs.atuin.enable}";
|
||||
|
||||
mcflyCtrlRDefinition = "`programs.mcfly.enable` defined in ${showOptionFiles options.programs.mcfly.enable}";
|
||||
|
||||
ctrlRConflictDefinitions = lib.concatStringsSep "\n" (
|
||||
lib.concatMap (
|
||||
shell:
|
||||
[
|
||||
" ${shell}:"
|
||||
" ${fzfCtrlRDefinition shell}"
|
||||
]
|
||||
++ lib.optionals (atuinCtrlRActive shell) [ " ${atuinCtrlRDefinition}" ]
|
||||
++ lib.optionals (mcflyCtrlRActive shell) [ " ${mcflyCtrlRDefinition}" ]
|
||||
) ctrlRConflictShells
|
||||
);
|
||||
in
|
||||
|
||||
lib.optional (cfg.historyWidget.command != null && lib.versionOlder cfg.package.version "0.66.0") ''
|
||||
`programs.fzf.historyWidget.command` defined in ${lib.showFiles options.programs.fzf.historyWidget.command.files} requires fzf 0.66.0 or greater.
|
||||
|
||||
The configured FZF_CTRL_R_COMMAND value will be ignored by older fzf
|
||||
versions.
|
||||
''
|
||||
++ lib.optional (ctrlRConflictShells != [ ]) ''
|
||||
programs.fzf and a history manager both configure Ctrl-R for ${lib.concatStringsSep ", " ctrlRConflictShells}.
|
||||
|
||||
Definitions:
|
||||
${ctrlRConflictDefinitions}
|
||||
|
||||
The history manager integration is sourced after fzf and owns Ctrl-R.
|
||||
Choose which integration should own Ctrl-R.
|
||||
To keep the history manager on Ctrl-R, disable fzf's binding:
|
||||
programs.fzf.historyWidget.command = "";
|
||||
or disable it for one shell:
|
||||
programs.fzf.historyWidget.${builtins.head ctrlRConflictShells}.command = "";
|
||||
To keep fzf on Ctrl-R with Atuin, disable Atuin's Ctrl-R binding:
|
||||
programs.atuin.flags = [ "--disable-ctrl-r" ];
|
||||
McFly has no Ctrl-R-only option in Home Manager. Disable McFly's
|
||||
shell integration for that shell if fzf should own Ctrl-R.
|
||||
To use another key, disable one default Ctrl-R binding and add a
|
||||
shell-specific key binding in your shell configuration.
|
||||
'';
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion =
|
||||
(cfg.enableBashIntegration || cfg.enableFishIntegration || cfg.enableZshIntegration)
|
||||
-> lib.versionAtLeast cfg.package.version "0.48.0";
|
||||
message = "fzf package version must be 0.48.0 or greater for bash, fish, or zsh integration";
|
||||
}
|
||||
{
|
||||
assertion = cfg.enableNushellIntegration -> lib.versionAtLeast cfg.package.version "0.73.0";
|
||||
message = "fzf package version must be 0.73.0 or greater for nushell integration";
|
||||
}
|
||||
];
|
||||
|
||||
home = {
|
||||
packages = [ cfg.package ];
|
||||
sessionVariables = lib.mapAttrs (_n: toString) fzfEnvVars;
|
||||
};
|
||||
|
||||
programs = {
|
||||
# Load early so history managers can reclaim Ctrl-R.
|
||||
bash.initExtra =
|
||||
let
|
||||
vars = shellFzfEnvVars "bash";
|
||||
in
|
||||
mkIf cfg.enableBashIntegration (
|
||||
mkOrder 200 (
|
||||
''
|
||||
if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
|
||||
''
|
||||
+ lib.optionalString (vars != { }) "${config.lib.shell.exportAll vars}\n"
|
||||
+ ''
|
||||
eval "$(${getExe cfg.package} --bash)"
|
||||
fi
|
||||
''
|
||||
)
|
||||
);
|
||||
|
||||
# Still needs to be initialized after oh-my-zsh (order 800), otherwise
|
||||
# omz will take precedence.
|
||||
zsh.initContent =
|
||||
let
|
||||
vars = shellFzfEnvVars "zsh";
|
||||
in
|
||||
mkIf cfg.enableZshIntegration (
|
||||
mkOrder 910 (
|
||||
''
|
||||
if [[ $options[zle] = on ]]; then
|
||||
''
|
||||
+ lib.optionalString (vars != { }) "${config.lib.shell.exportAll vars}\n"
|
||||
+ ''
|
||||
source <(${getExe cfg.package} --zsh)
|
||||
fi
|
||||
''
|
||||
)
|
||||
);
|
||||
|
||||
fish.interactiveShellInit =
|
||||
let
|
||||
vars = shellFzfEnvVars "fish";
|
||||
exports = lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (name: value: "set -gx ${name} ${lib.escapeShellArg (toString value)}") vars
|
||||
);
|
||||
in
|
||||
mkIf cfg.enableFishIntegration (
|
||||
mkOrder 200 (
|
||||
lib.optionalString (vars != { }) "${exports}\n"
|
||||
+ ''
|
||||
${getExe cfg.package} --fish | source
|
||||
''
|
||||
)
|
||||
);
|
||||
|
||||
# Initialize after other completion integrations, such as carapace.
|
||||
# fzf preserves the previous external completer and falls back to it
|
||||
# when its own completer does not apply.
|
||||
nushell = lib.mkIf cfg.enableNushellIntegration {
|
||||
environmentVariables = lib.mapAttrs (_n: toString) fzfEnvVars;
|
||||
|
||||
extraConfig =
|
||||
let
|
||||
vars = shellFzfEnvVars "nushell";
|
||||
assignments = lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
name: value: "$env.${name} = ${lib.hm.nushell.toNushell { } (toString value)}"
|
||||
) vars
|
||||
);
|
||||
in
|
||||
mkAfter (
|
||||
lib.optionalString (vars != { }) "${assignments}\n"
|
||||
+ ''
|
||||
source ${
|
||||
pkgs.runCommand "nushell-fzf-integration.nu" { } ''
|
||||
${getExe cfg.package} --nushell > $out
|
||||
''
|
||||
}
|
||||
''
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,14 +19,15 @@ let
|
||||
|
||||
upstreamConfigDir = "${config.home.homeDirectory}/.copilot";
|
||||
|
||||
# Applied after transformMcpServer so that args = [] is not filtered out.
|
||||
forCopilotFormat =
|
||||
server:
|
||||
let
|
||||
isLocal = server.type == "stdio";
|
||||
isLocal = server.type == "stdio" || server.type == "local";
|
||||
in
|
||||
server
|
||||
// {
|
||||
type = if server.type == "stdio" then "local" else server.type or "local";
|
||||
type = if isLocal then "local" else server.type;
|
||||
}
|
||||
// lib.optionalAttrs isLocal {
|
||||
args = server.args or [ ];
|
||||
@@ -43,14 +44,15 @@ let
|
||||
if cfg.enableMcpIntegration && config.programs.mcp.enable && enabledServers != { } then
|
||||
lib.mapAttrs (
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
forCopilotFormat
|
||||
];
|
||||
}
|
||||
forCopilotFormat (
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; })
|
||||
];
|
||||
}
|
||||
)
|
||||
) enabledServers
|
||||
else
|
||||
{ };
|
||||
@@ -59,16 +61,17 @@ let
|
||||
transformedMcpServers
|
||||
// lib.mapAttrs (
|
||||
name: server:
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand {
|
||||
inherit pkgs name;
|
||||
})
|
||||
forCopilotFormat
|
||||
];
|
||||
}
|
||||
forCopilotFormat (
|
||||
lib.hm.mcp.transformMcpServer {
|
||||
inherit server;
|
||||
extraTransforms = [
|
||||
lib.hm.mcp.addType
|
||||
(lib.hm.mcp.wrapEnvFilesCommand {
|
||||
inherit pkgs name;
|
||||
})
|
||||
];
|
||||
}
|
||||
)
|
||||
) cfg.mcpServers;
|
||||
in
|
||||
{
|
||||
|
||||
@@ -109,7 +109,7 @@ let
|
||||
|
||||
importTrustBashFunctions =
|
||||
let
|
||||
gpg = "${cfg.package}/bin/gpg";
|
||||
gpg = if cfg.package != null then "${cfg.package}/bin/gpg" else "gpg";
|
||||
in
|
||||
''
|
||||
function gpgKeyId() {
|
||||
@@ -132,7 +132,7 @@ let
|
||||
|
||||
keyringFiles =
|
||||
let
|
||||
gpg = "${cfg.package}/bin/gpg";
|
||||
gpg = if cfg.package != null then "${cfg.package}/bin/gpg" else "gpg";
|
||||
|
||||
importKey =
|
||||
{ source, trust, ... }:
|
||||
@@ -163,6 +163,7 @@ in
|
||||
enable = lib.mkEnableOption "GnuPG";
|
||||
|
||||
package = lib.mkPackageOption pkgs "gnupg" {
|
||||
nullable = true;
|
||||
example = "pkgs.gnupg23";
|
||||
extraDescription = "Also used by the gpg-agent service.";
|
||||
};
|
||||
@@ -303,7 +304,7 @@ in
|
||||
no-symkey-cache = mkDefault true;
|
||||
};
|
||||
|
||||
home.packages = [ cfg.package ];
|
||||
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
home.sessionVariables = {
|
||||
GNUPGHOME = cfg.homedir;
|
||||
};
|
||||
@@ -336,7 +337,7 @@ in
|
||||
|
||||
importGpgKeys =
|
||||
let
|
||||
gpg = "${cfg.package}/bin/gpg";
|
||||
gpg = if cfg.package != null then "${cfg.package}/bin/gpg" else "gpg";
|
||||
|
||||
importKey =
|
||||
{ source, trust, ... }:
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib) mkIf mkOption;
|
||||
|
||||
cfg = config.programs.herdr;
|
||||
|
||||
tomlFormat = pkgs.formats.toml { };
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.amadejkastelic ];
|
||||
|
||||
options.programs.herdr = {
|
||||
enable = lib.mkEnableOption "Herdr";
|
||||
|
||||
package = lib.mkPackageOption pkgs "herdr" { nullable = true; };
|
||||
|
||||
settings = mkOption {
|
||||
inherit (tomlFormat) type;
|
||||
default = { };
|
||||
example = {
|
||||
onboarding = false;
|
||||
terminal = {
|
||||
default_shell = "nu";
|
||||
shell_mode = "auto";
|
||||
new_cwd = "follow";
|
||||
};
|
||||
theme = {
|
||||
name = "catppuccin";
|
||||
auto_switch = true;
|
||||
light_name = "catppuccin-latte";
|
||||
dark_name = "catppuccin";
|
||||
};
|
||||
ui = {
|
||||
sidebar_width = 32;
|
||||
agent_panel_sort = "priority";
|
||||
toast.delivery = "herdr";
|
||||
sound.enabled = true;
|
||||
};
|
||||
keys.prefix = "ctrl+b";
|
||||
keys.command = [
|
||||
{
|
||||
key = "prefix+l";
|
||||
type = "plugin_action";
|
||||
command = "example.layout.apply";
|
||||
description = "apply layout";
|
||||
}
|
||||
];
|
||||
};
|
||||
description = ''
|
||||
Configuration written to {file}`$XDG_CONFIG_HOME/herdr/config.toml`.
|
||||
See <https://herdr.dev/docs/configuration/> for the full list of options.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
|
||||
xdg.configFile."herdr/config.toml" = mkIf (cfg.settings != { }) {
|
||||
source = tomlFormat.generate "herdr-config.toml" cfg.settings;
|
||||
onChange =
|
||||
let
|
||||
binPath = if cfg.package == null then "herdr" else "${lib.getExe cfg.package}";
|
||||
in
|
||||
"${binPath} server reload-config || true";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
|
||||
cfg = config.programs.hyprland-qt-support;
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ ReStranger ];
|
||||
|
||||
options.programs.hyprland-qt-support = {
|
||||
enable = lib.mkEnableOption "" // {
|
||||
description = ''
|
||||
Whether to enable Hyprland Qt support, a Qt6 QML style provider for hypr* apps.
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkPackageOption pkgs "hyprland-qt-support" { nullable = true; };
|
||||
|
||||
settings = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
let
|
||||
valueType =
|
||||
nullOr (oneOf [
|
||||
bool
|
||||
int
|
||||
float
|
||||
str
|
||||
path
|
||||
(attrsOf valueType)
|
||||
(listOf valueType)
|
||||
])
|
||||
// {
|
||||
description = "Hyprland configuration value";
|
||||
};
|
||||
in
|
||||
valueType;
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
roundness = 1;
|
||||
border_width = 1;
|
||||
reduce_motion = false;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Hyprland Qt Support configuration written in Nix. See
|
||||
<https://wiki.hypr.land/Hypr-Ecosystem/hyprland-qt-support/> for more
|
||||
examples.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
(lib.hm.assertions.assertPlatform "programs.hyprland-qt-support" pkgs lib.platforms.linux)
|
||||
];
|
||||
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
|
||||
xdg.configFile."hypr/application-style.conf" = lib.mkIf (cfg.settings != { }) {
|
||||
text = lib.hm.generators.toHyprconf { attrs = cfg.settings; };
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -433,7 +433,7 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
xdg.configFile."kitty/diff.conf" = mkIf (cfg.diffConfig != { }) {
|
||||
xdg.configFile."kitty/diff.conf" = mkIf (cfg.diffConfig.extraConfig != "") {
|
||||
text = ''
|
||||
# Generated by Home Manager
|
||||
# See https://sw.kovidgoyal.net/kitty/kittens/diff/
|
||||
|
||||
@@ -41,7 +41,7 @@ in
|
||||
{file}`$XDG_CONFIG_HOME/navi/config.yaml` on Linux or
|
||||
{file}`$HOME/Library/Application Support/navi/config.yaml`
|
||||
on Darwin. See
|
||||
<https://github.com/denisidoro/navi/blob/master/docs/config_file.md>
|
||||
<https://github.com/denisidoro/navi/blob/master/docs/configuration/README.md>
|
||||
for more information.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ let
|
||||
preferLocalBuild = true;
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
postBuild = ''
|
||||
wrapProgram $out/bin/opencode \
|
||||
wrapProgram $out/bin/${cfg.package.meta.mainProgram} \
|
||||
--suffix PATH : ${lib.makeBinPath cfg.extraPackages}
|
||||
'';
|
||||
}
|
||||
@@ -124,6 +124,11 @@ in
|
||||
raw sibling values treated as unordered entries. This is useful for
|
||||
OpenCode permission rules, where the last matching rule wins.
|
||||
|
||||
The `plugin` key accepts a list of plugin references: local paths to
|
||||
plugin directories or files, derivations (e.g. `fetchFromGitHub`),
|
||||
string paths into derivations, or names of external plugins fetched and
|
||||
built by OpenCode.
|
||||
|
||||
Note, `"$schema": "https://opencode.ai/config.json"` is automatically added to the configuration.
|
||||
'';
|
||||
};
|
||||
@@ -468,7 +473,7 @@ in
|
||||
) (lib.attrNames cfg.settings);
|
||||
|
||||
packageVersion = if cfg.package != null then lib.getVersion cfg.package else null;
|
||||
hasTuiConfig = lib.versionAtLeast packageVersion "1.2.15";
|
||||
hasTuiConfig = packageVersion == null || lib.versionAtLeast packageVersion "1.2.15";
|
||||
in
|
||||
lib.optionals (hasTuiConfig && deprecatedConfigKeys != [ ]) [
|
||||
''
|
||||
|
||||
@@ -36,7 +36,7 @@ let
|
||||
cfg.package;
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.hm.maintainers; [ semi710 ];
|
||||
meta.maintainers = with lib.maintainers; [ semi710 ];
|
||||
|
||||
options.programs.pi-coding-agent = {
|
||||
enable = mkEnableOption "pi-coding-agent";
|
||||
|
||||
@@ -53,10 +53,10 @@
|
||||
name = "storage";
|
||||
params = [ "${if calendar then "calendar" else "contacts"}-${name}-local" ];
|
||||
children =
|
||||
(attrsToDirectives {
|
||||
(attrsToDirectives { type = if calendar then "vdir/icalendar" else "vdir/vcard"; })
|
||||
++ (attrsToDirectives {
|
||||
inherit (acc.local) path;
|
||||
fileext = acc.local.fileExt;
|
||||
type = if calendar then "vdir/icalendar" else "vdir/vcard";
|
||||
})
|
||||
++ acc.pimsync.extraLocalStorageDirectives;
|
||||
};
|
||||
@@ -66,8 +66,6 @@
|
||||
params = [ "${if calendar then "calendar" else "contacts"}-${name}-remote" ];
|
||||
children =
|
||||
(attrsToDirectives {
|
||||
inherit (acc.remote) url;
|
||||
username = acc.remote.userName;
|
||||
type =
|
||||
if !calendar then
|
||||
"carddav"
|
||||
@@ -76,6 +74,10 @@
|
||||
else
|
||||
"webcal";
|
||||
})
|
||||
++ (attrsToDirectives {
|
||||
inherit (acc.remote) url;
|
||||
username = acc.remote.userName;
|
||||
})
|
||||
++ lib.optional (acc.remote.passwordCommand != null) {
|
||||
name = "password";
|
||||
children = lib.singleton {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.programs.posting;
|
||||
yamlFormat = pkgs.formats.yaml { };
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.hm.maintainers.reesilva ];
|
||||
|
||||
options.programs.posting = {
|
||||
enable = lib.mkEnableOption "Posting";
|
||||
|
||||
package = lib.mkPackageOption pkgs "posting" { nullable = true; };
|
||||
|
||||
settings = lib.mkOption {
|
||||
inherit (yamlFormat) type;
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
theme = "custom";
|
||||
layout = "horizontal";
|
||||
response = {
|
||||
prettify_json = false;
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Configuration written to
|
||||
{file}`$XDG_CONFIG_HOME/posting/config.yaml`.
|
||||
|
||||
See <https://posting.sh/guide/configuration/>
|
||||
for supported values.
|
||||
'';
|
||||
};
|
||||
|
||||
themes = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
attrsOf (oneOf [
|
||||
yamlFormat.type
|
||||
path
|
||||
lines
|
||||
]);
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
custom = {
|
||||
name = "custom";
|
||||
primary = "#4e78c4";
|
||||
secondary = "#f39c12";
|
||||
accent = "#e74c3c";
|
||||
background = "#0e1726";
|
||||
surface = "#17202a";
|
||||
error = "#e74c3c";
|
||||
success = "#2ecc71";
|
||||
warning = "#f1c40f";
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Each theme is written to
|
||||
{file}`$XDG_DATA_HOME/posting/themes/NAME.yaml`.
|
||||
|
||||
See <https://posting.sh/guide/themes/>
|
||||
for more information.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
|
||||
|
||||
xdg.configFile."posting/config.yaml" = lib.mkIf (cfg.settings != { }) {
|
||||
source = yamlFormat.generate "posting-config.yaml" cfg.settings;
|
||||
};
|
||||
|
||||
xdg.dataFile = lib.mkIf (cfg.themes != { }) (
|
||||
lib.mapAttrs' (
|
||||
name: value:
|
||||
lib.nameValuePair "posting/themes/${name}.yaml" {
|
||||
source =
|
||||
if builtins.isPath value || lib.isStorePath value then
|
||||
value
|
||||
else if lib.isString value then
|
||||
pkgs.writeText "posting-theme-${name}.yaml" value
|
||||
else
|
||||
yamlFormat.generate "posting-theme-${name}.yaml" value;
|
||||
}
|
||||
) cfg.themes
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -82,7 +82,12 @@ let
|
||||
{
|
||||
Unit = {
|
||||
Description = "Rclone ${
|
||||
if isMount then "FUSE daemon" else "protocol serving"
|
||||
if !isMount then
|
||||
"protocol serving"
|
||||
else if cmdName == "nfsmount" then
|
||||
"NFS mount daemon"
|
||||
else
|
||||
"FUSE daemon"
|
||||
} for ${remoteName}:${sidecarPath}";
|
||||
Requires = [ "rclone-config.service" ];
|
||||
After = [ "rclone-config.service" ];
|
||||
@@ -196,9 +201,16 @@ let
|
||||
_sidecar:
|
||||
let
|
||||
sidecarPath = _sidecar.name;
|
||||
sidecar = _sidecar.value;
|
||||
isMount = sidecarType == "mounts";
|
||||
cmdName = if isMount then "mount" else "serve";
|
||||
cmdName = if isMount then _sidecar.value.mountType else "serve";
|
||||
# nfsmount keeps its NFS file-handle cache in memory by default,
|
||||
# so a service restart would leave the kernel client with stale
|
||||
# handles; persist it to disk unless the user says otherwise.
|
||||
sidecar =
|
||||
if cmdName == "nfsmount" then
|
||||
_sidecar.value // { options = lib.mergeAttrs { nfs-cache-type = "disk"; } _sidecar.value.options; }
|
||||
else
|
||||
_sidecar.value;
|
||||
in
|
||||
lib.optional sidecar.enable (
|
||||
lib.nameValuePair "rclone-${cmdName}:${replaceIllegalChars sidecarPath}@${remoteName}" (mkValue {
|
||||
@@ -360,6 +372,35 @@ in
|
||||
'';
|
||||
example = "/home/alice/my-remote";
|
||||
};
|
||||
|
||||
mountType = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"mount"
|
||||
"nfsmount"
|
||||
];
|
||||
default = "mount";
|
||||
example = "nfsmount";
|
||||
description = ''
|
||||
Which rclone command performs the mount.
|
||||
|
||||
`mount` uses FUSE, which on macOS requires macFUSE and its
|
||||
kernel extension. `nfsmount` instead runs an in-process NFS
|
||||
server and mounts it via the system NFS client — no FUSE
|
||||
driver, kernel extension, or Recovery-mode security downgrade
|
||||
needed, making it the recommended type on macOS. See
|
||||
<https://rclone.org/commands/rclone_nfsmount/> for details
|
||||
and caveats.
|
||||
|
||||
For `nfsmount`, `nfs-cache-type = "disk"` is added to
|
||||
{option}`options` by default (overridable) so that NFS file
|
||||
handles survive rclone restarts; the in-memory default would
|
||||
leave the kernel client with stale handles whenever the
|
||||
service is restarted.
|
||||
|
||||
On Linux, mounting NFS normally requires root, so `nfsmount`
|
||||
is primarily useful on Darwin.
|
||||
'';
|
||||
};
|
||||
}
|
||||
// mountServeOptions;
|
||||
}
|
||||
@@ -378,7 +419,9 @@ in
|
||||
On macOS, FUSE mounts additionally require macFUSE
|
||||
(<https://osxfuse.github.io/>) to be installed and its system extension
|
||||
approved. Home Manager does not install macFUSE; rclone will surface
|
||||
a runtime error if it is missing.
|
||||
a runtime error if it is missing. Alternatively, set
|
||||
{option}`mountType` to `"nfsmount"` to mount via the system NFS
|
||||
client instead, which requires no additional software.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
|
||||
+12
-13
@@ -8,6 +8,10 @@ let
|
||||
inherit (lib) mkOption types;
|
||||
|
||||
cfg = config.programs.senpai;
|
||||
|
||||
configDir =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin then "Library/Application Support" else config.xdg.configHome;
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.malte-v ];
|
||||
@@ -15,6 +19,7 @@ in
|
||||
options.programs.senpai = {
|
||||
enable = lib.mkEnableOption "senpai";
|
||||
package = lib.mkPackageOption pkgs "senpai" { };
|
||||
|
||||
config = mkOption {
|
||||
type = types.submodule {
|
||||
freeformType = types.attrsOf types.anything;
|
||||
@@ -102,8 +107,10 @@ in
|
||||
message = "senpai: no-tls is deprecated, use tls instead";
|
||||
}
|
||||
];
|
||||
|
||||
home.packages = [ cfg.package ];
|
||||
xdg.configFile."senpai/senpai.scfg".text =
|
||||
|
||||
home.file."${configDir}/senpai/senpai.scfg".text =
|
||||
let
|
||||
attrsToDirectiveList = lib.mapAttrsToList (
|
||||
name: value:
|
||||
@@ -111,22 +118,14 @@ in
|
||||
inherit name;
|
||||
}
|
||||
// (
|
||||
if (builtins.typeOf value != "set") then
|
||||
{
|
||||
params = lib.toList value;
|
||||
}
|
||||
if builtins.typeOf value != "set" then
|
||||
{ params = lib.toList value; }
|
||||
else
|
||||
let
|
||||
children = lib.filterAttrs (n: _: n != "_params") value;
|
||||
in
|
||||
(
|
||||
lib.optionalAttrs (value ? "_params") {
|
||||
params = value._params;
|
||||
}
|
||||
// lib.optionalAttrs (children != { }) {
|
||||
children = attrsToDirectiveList children;
|
||||
}
|
||||
)
|
||||
lib.optionalAttrs (value ? "_params") { params = value._params; }
|
||||
// lib.optionalAttrs (children != { }) { children = attrsToDirectiveList children; }
|
||||
)
|
||||
);
|
||||
in
|
||||
|
||||
@@ -19,9 +19,9 @@ let
|
||||
jsonFormat = pkgs.formats.json { };
|
||||
in
|
||||
{
|
||||
meta.maintainers = [
|
||||
lib.maintainers.khaneliman
|
||||
lib.hm.maintainers."74k1"
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
khaneliman
|
||||
_74k1
|
||||
];
|
||||
|
||||
options.programs.sherlock = {
|
||||
|
||||
@@ -19,7 +19,7 @@ let
|
||||
tomlType = tomlFormat.type;
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ diniamo ];
|
||||
meta.maintainers = [ ];
|
||||
|
||||
options.programs.spotify-player = {
|
||||
enable = mkEnableOption "spotify-player";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user