Compare commits

...
Author SHA1 Message Date
Benedikt RipsandAustin Horstman 079a3b5d1a sshAuthSock: for Zsh, initialize in .zprofile and .zshenv 2026-07-24 12:12:39 -05:00
Benedikt RipsandAustin Horstman 40d502faf2 sshAuthSock: indent initialization code 2026-07-24 12:12:39 -05:00
Marius BärandRobert Helgesson 32de400b6a direnv: fix nushell integration failing to unset variables
The nushell pre_prompt hook applied direnv's export diff with `load-env`,
which cannot remove a variable. Variables direnv marks for removal (null
values) were left behind as empty ghosts. Hide those keys explicitly and
drop them before `load-env`.
2026-07-24 09:16:45 +02:00
Eman ResuandRobert Helgesson 3b0e6bbd65 modules: partially apply .nix suffix check
hasSuffix runs some checks on the suffix, that can be done all at once
rather than every single time.
2026-07-23 08:47:55 +02:00
Eman ResuandRobert Helgesson cbfe673271 modules: stop filtering files starting with _
There are no instances of this in-tree.
2026-07-23 08:47:55 +02:00
YethalandAustin Horstman 041a999e8c modules/misc/ssh-auth-sock: correct nushell init 2026-07-22 08:08:47 -05:00
Sebastián EstrellaandAustin Horstman 6d8d615678 herdr: Use the binary outside Nix to reload the config when package is unset 2026-07-21 22:14:27 -05:00
abba1081f5 rclone: add nfsmount mount type
Add a per-mount 'mountType' option ('mount' | 'nfsmount') selecting
which rclone command performs the mount. 'nfsmount' runs rclone's
in-process NFS server and mounts it via the system NFS client, so no
FUSE driver is needed — on macOS this removes the module's documented
macFUSE requirement (and the kernel-extension approval plus Reduced
Security boot policy that come with it).

When 'nfsmount' is selected, 'nfs-cache-type = "disk"' is merged
into the mount's options (user-overridable, mirroring the existing
'vfs-cache-mode' defaulting) so NFS file handles survive rclone
restarts; with the in-memory default, every service restart would
leave the kernel client with stale handles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:13:44 -05:00
yaaaarnandAustin Horstman a2a9fec572 senpai: use correct config directory on darwin systems 2026-07-21 22:13:04 -05:00
Matt SchreiberandRobert Helgesson deeb6b7eb7 files: fix unintended subshell expansion 2026-07-21 00:53:36 +02:00
Arnaud VenturiandRobert Helgesson cbcbd8b043 kanshi: Fix scale example values from int to float 2026-07-21 00:24:25 +02:00
JessandRobert Helgesson 7e2199f4bc rclone: set programs.fuse.enable to fix integration tests 2026-07-21 00:23:33 +02:00
DimaniojkandRobert Helgesson 56ccab8918 programs.claude-code: avoid watching the Nix store 2026-07-21 00:19:38 +02:00
AstreaprtclandRobert Helgesson a02190edf9 wlsunset: add duration option
Add an option to set the easing duration if the times have been set
manually.
2026-07-20 23:18:26 +02:00
Eman ResuandRobert Helgesson 411408f523 lib/file-type: partially apply hasPrefix and removePrefix 2026-07-20 23:17:54 +02:00
reducedradiusandRobert Helgesson 7834e3fb36 Translate using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (39 of 39 strings)

Co-authored-by: reducedradius <137701630+flytothehighest@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/home-manager/cli/zh_Hans/
Translation: Home Manager/Home Manager CLI
2026-07-20 23:00:06 +02:00
Artur SannikovandRobert Helgesson 7d7a2566f6 Translate using Weblate (Italian)
Currently translated at 93.7% (15 of 16 strings)

Translate using Weblate (Italian)

Currently translated at 100.0% (39 of 39 strings)

Translate using Weblate (Russian)

Currently translated at 100.0% (16 of 16 strings)

Translate using Weblate (Russian)

Currently translated at 100.0% (39 of 39 strings)

Co-authored-by: Artur Sannikov <weblate.rbpxz@8shield.net>
Translate-URL: https://hosted.weblate.org/projects/home-manager/cli/it/
Translate-URL: https://hosted.weblate.org/projects/home-manager/cli/ru/
Translate-URL: https://hosted.weblate.org/projects/home-manager/modules/it/
Translate-URL: https://hosted.weblate.org/projects/home-manager/modules/ru/
Translation: Home Manager/Home Manager CLI
Translation: Home Manager/Home Manager Modules
2026-07-20 23:00:06 +02:00
greenpsiandMatthieu Coudron e3dea8e479 tirith: zsh initExtra -> initContent
Migrate usage of zsh initExtra to initContent because initExtra is
deprecated.
2026-07-20 13:17:44 +02:00
diniamoandMatthieu Coudron 3139deb8ca maintainers: remove diniamo 2026-07-19 21:31:31 +02:00
Jason BowmanandRobert Helgesson 39411a8e12 syncthing: use localhost authority for unix-socket REST calls
When services.syncthing.guiAddress is a unix socket, curlAddressArgs built
`--unix-socket <sock> http://.<path>`. curl >= 8.21 tightened URL parsing and
rejects the bare-dot authority with "URL rejected: Bad hostname" (exit 3), so
every REST call in the merge script fails. Combined with `--retry 1000
--retry-delay 1 --retry-all-errors`, syncthing-init.service retries until
systemd terminates it at the start timeout and Home Manager activation fails.

Use `localhost` as the placeholder authority; --unix-socket still routes the
request to the socket, and localhost is accepted by all curl versions. Add a
regression test asserting the unix-socket path uses http://localhost.
2026-07-18 22:41:57 +02:00
Sebastián EstrellaandAustin Horstman 460108009c herdr: Automatically reload config 2026-07-18 00:08:44 -05:00
Mike ThaiandAustin Horstman 18f9047340 navi: update url for config file docs 2026-07-18 00:04:03 -05:00
home-manager-ci[bot]andAustin Horstman c71e83ef50 maintainers: update all-maintainers.nix
Automated update of the master maintainers list combining:
- Home Manager specific maintainers from modules/lib/maintainers.nix
- Nixpkgs maintainers referenced in Home Manager modules

**Added:** 2 maintainers
**Removed:** 0 maintainers
**Total:** 305 → 307 maintainers

** Added:** ReeSilva, ratakor

Generated by: lib/python/generate-all-maintainers.py
2026-07-18 00:02:55 -05:00
Austin Horstman 165228b0ef launchd: restore gui domain defaults
User-domain agents are not rediscovered from the user LaunchAgents directory after reboot. Let built-in agents inherit the GUI default while keeping explicit user-domain configuration available.
2026-07-15 10:29:26 -05:00
6dc9e081ba docs: fix cross-page links broken by mdbook migration
The mdBook migration split the manual into one HTML page per section,
but same-page fragment links (e.g. [Standalone setup](#sec-flakes-standalone))
were left pointing at anchors that now live on other pages, so they
silently do nothing when clicked.

Rewrite all cross-page fragment links in docs/manual and
docs/release-notes to page-qualified relative links. Option anchor
links (#opt-*, #nixos-opt-*, #nix-darwin-opt-*) are untouched since
convert-markup.py rewrites those at build time.

Also give the previously empty-label [](#sec-tests) link in the 25.05
release notes a "Tests" label, since the mdBook renderer does not
auto-fill labels from the target heading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 08:56:06 -05:00
Thierry DelafontaineandMatthieu Coudron a45a7c4514 opencode: use meta.mainProgram to wrap the main binary
Resolves #9640
2026-07-13 19:22:27 +02:00
David ThievonandAustin Horstman 7566825d46 swayimg: support new lua config
Closes: #9479
2026-07-11 21:30:09 -05:00
port22exposedandAustin Horstman ae6f976083 psd: remove aliased packages
aliased packages result in breakage on systems with them disabled and home-manager set to use global nixpkgs
2026-07-11 20:49:16 -05:00
Thierry DelafontaineandAustin Horstman c74f4f6cdd opencode: document plugin option usage in settings description 2026-07-11 19:45:00 -05:00
pancho horrilloandAustin Horstman e473bdcd87 home-cursor: update URL to Freedesktop.org specs
The old one returns a `404 - Page not found` error.

Located the content via archive.org¹ and a quick search yielded the
current URL for the spec.

¹: https://web.archive.org/web/20161024065530/https://specifications.freedesktop.org/icon-theme-spec/latest/ar01s03.html
2026-07-11 18:57:34 -05:00
Rachit Kumar VermaandAustin Horstman 564bc37de5 kitty: fix diff.conf being generated regardless of user config
'cfg.diffConfig' is an implicit submodule with sub-options that carry
their own defaults, so it never actually equals `{ }` - the previous
`cfg.diffConfig != { }` check was always true. This caused
'kitty/diff.conf' to be written (and clobber any existing unmanaged file)
even when the user configured nothing under `programs.kitty.diffConfig`.
2026-07-11 18:56:44 -05:00
csanthiagoandAustin Horstman 0992a29487 television: add extraPackages option with default dependencies
Add an extraPackages option to the television module that wraps the `tv`
binary with additional packages in PATH. Defaults to fd, bat, and ripgrep
which are the dependencies required by television's default channels.

Existing tests updated to set extraPackages = [ ] since they test config
generation, not binary wrapping.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-11 15:00:36 -05:00
Austin Horstman 2afec152df tests/darwinScrublist: add podman
podman pulls in vfkit on darwin, which fails to build on
aarch64-darwin with the current nixpkgs pin (cctools ld crashes
with 'Trace/BPT trap: 5'). The podman darwin tests reference the
real package through the machine watchdog script, establishing a
build dependency. Stub it on darwin test runs.
2026-07-10 23:35:26 -05:00
dependabot[bot]andAustin Horstman d29a1c0f34 flake: bump nixpkgs from 9e92285 to 767b0d3
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `9e92285` to `767b0d3`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/9e92285f211dad236540fd617d7e30e0b99bc0e1...767b0d3ec98a143ad9ed7dfc0d5553510ac27133)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 767b0d3ec98a143ad9ed7dfc0d5553510ac27133
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 23:35:26 -05:00
Austin Horstman 3e2af6c9e5 tests/darwinScrublist: add colima, gurk-rs
Both fail to build on aarch64-darwin with the current nixpkgs pin
(cctools ld crashes with 'Trace/BPT trap: 5'): gurk-rs directly, and
vfkit as a colima dependency. Stub them on darwin test runs.
2026-07-10 23:06:34 -05:00
Austin Horstman f938277527 claude-code: tighten module implementation
Remove redundant option machinery, centralize skills shape checks, and use narrower list composition while preserving generated behavior.
2026-07-10 14:19:11 -05:00
Austin Horstman 437ae5be42 claude-code: test plugin collisions
Cover duplicate personal-plugin directory names and collisions between managed plugins and configured skills.
2026-07-10 14:19:11 -05:00
Austin Horstman 5c7199cd3b claude-code: split module files
Move public option declarations and file-entry helpers into dedicated files while keeping module assembly in default.nix. Update the AI labeler for the new module directory.
2026-07-10 14:19:11 -05:00
Austin Horstman 1c029fb0dc claude-code: load plugins without wrapper
Persist managed plugins in Claude Code's personal skills directory so strict-parser subcommands do not receive injected --plugin-dir arguments. Retain version-gated legacy behavior for older and unversioned packages.
2026-07-10 14:19:11 -05:00
Eason20000andAustin Horstman 79e6082586 launchd: treat absent domain as harmless in bootoutAgent
When an agent's launchd.agents.<name>.domain is changed from gui to
user (or the agent is newly installed on a headless macOS system
where the gui/<UID> domain never existed), bootoutAgent attempts to
unload the agent from the old domain first. On such systems, this
fails with: Boot-out failed: 125: Domain does not support specified
action.

The bootoutAgent function only treated "No such process" as a
non-fatal condition (return 2). This change adds the domain-not-exist
error to the same handling path: the agent wasn't running in a domain
that doesn't exist, so we can safely skip the boot-out and proceed
with bootstrap into the new domain.
2026-07-10 08:22:30 -05:00
Austin Horstman 144f4e36d0 files: respect executable for recursive directories
Apply explicit executable settings to recursive directory leaves so directory sources behave like documented file sources.
2026-07-09 12:27:58 -05:00
reesilvaandAustin Horstman f4d01c1d87 posting: add module
posting: add news
2026-07-08 17:33:28 -05:00
reesilvaandAustin Horstman a1fc9a1cc3 maintainers: add reesilva 2026-07-08 17:33:28 -05:00
Austin Horstman 1aac8895fe home-cursor: warn before removing implicit enable
Follow up on home-cursor: remove legacy enable by preserving the implicit enable path that was not covered by the earlier deprecation warning.

Configurations that set home.pointerCursor without top-level enable now continue to work and emit a warning asking users to set home.pointerCursor.enable explicitly.
2026-07-08 10:46:52 -05:00
Austin Horstman cee3f0e7fe yubikey-agent: switch to gui domain for hardware key PIN prompts
yubikey-agent triggers OS-level CCID/PIV PIN entry dialogs when SSH
authentication is requested. These system security prompts require
access to the macOS Aqua session (window server). Running in the user
domain (LimitLoadToSessionType=Background) blocks these prompts,
causing authentication to fail or hang.

Revert to the default gui domain so PIN prompts can render in the
graphical login session.
2026-07-07 22:02:13 -05:00
Austin Horstman 7a62d63707 gpg-agent: switch to gui domain for graphical pinentry support
gpg-agent is commonly configured with graphical pinentry programs
(pinentry-mac, pinentry-gnome3) on macOS that need access to the Aqua
session to display passphrase prompts. Running in the user domain
(LimitLoadToSessionType=Background) prevents these prompts from
rendering, causing passphrase entry to fail silently or hang.

Revert to the default gui domain so the agent runs in the graphical
login session where pinentry can interact with the window server.
2026-07-07 22:02:13 -05:00
Thierry DelafontaineandAustin Horstman 53ebbdc405 proton-pass-agent: switch to gui domain to prevent error accessing keyring
Fixes the following accessing issue:

```
Error accessing credential on keyring: PlatformFailure(Error { code: -25308, message: "User interaction is not allowed." })
```
2026-07-07 16:08:19 -05:00
isabelandAustin Horstman 2ad49968c3 home-cursor: remove legacy enable
its been 482 days since this was merged. users should have had enough
time to migrate
2026-07-07 15:47:40 -05:00
Batuhan Baserdem, Ph.D.andGitHub 3c6f1244ed firefox: allow setting persistent StoreID
Added a new config option programs.firefox.profiles.<name>.storeId.
Setting this option now writes the StoreID both to the profiles.ini
and to the settings."toolkit.profiles.storeID" of the profile.
Firefox has two different profile implementations;
- the old one that home-manager module can declare
- the new "Selectable Profile Service" introduced in 150
The new profiles are managed in a runtime db, and reads from profiles.ini
Migration without database operations is not possible right now,
but when it becomes available, this option will stop home-manager from
clobbering the linked storeIDs.
2026-07-07 15:46:26 -05:00
Thierry DelafontaineandAustin Horstman f09af49406 opencode: assume most recent version when package=null
Resolves #9589
2026-07-07 09:54:30 -05:00
Austin Horstman cd863288d4 codex: stop duplicating hook script deployment
Write path-backed hook bundles only under the active Codex home so XDG mode no longer mirrors scripts into ~/.codex.
2026-07-07 09:53:53 -05:00
RatakorandAustin Horstman 52f93adbac walker: fix systemd dependency cycle with elephant
Signed-off-by: Ratakor <ratakor@disroot.org>
2026-07-07 09:50:48 -05:00
RatakorandAustin Horstman 16acb8c7ff maintainers: add ratakor to walker & elephant
Signed-off-by: Ratakor <ratakor@disroot.org>
2026-07-07 09:50:48 -05:00
RatakorandAustin Horstman 8de7488af2 elephant: fix service unit
Signed-off-by: Ratakor <ratakor@disroot.org>
2026-07-07 09:50:48 -05:00
Saad NadeemandAustin Horstman ed10fd394e clipse: support store paths for theme option 2026-07-07 09:50:12 -05:00
Leon SchwarzäuglandAustin Horstman 63d02d1c19 attic-client: init module 2026-07-06 20:46:24 -05:00
Austin Horstman 0d33882bd4 codex: support path-backed hooks
Allow path-backed hook bundles to provide both hooks.json and supporting scripts through programs.codex.hooks.
2026-07-06 12:20:20 -05:00
Austin Horstman a1645f4077 maintainers: regenerate all-maintainers
Regenerate all-maintainers.nix from module metadata after removing duplicate yarn maintainer entry.
2026-07-04 22:28:41 -05:00
Austin Horstman cf0c5c3e9f maintainers: drop duplicate yarn entry
Remove HM-only yarn maintainer definition and keep module references aligned with nixpkgs maintainer data.
2026-07-04 22:28:41 -05:00
dependabot[bot]andAustin Horstman 40545758d8 flake: bump nixpkgs from 89570f2 to 9e92285
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `89570f2` to `9e92285`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/89570f24e97e614aa34aa9ab1c927b6578a43775...9e92285f211dad236540fd617d7e30e0b99bc0e1)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 9e92285f211dad236540fd617d7e30e0b99bc0e1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-04 22:28:41 -05:00
pancho horrilloandAustin Horstman bfb67e07a3 difftastic: fix jujutsu integration
Don’t append `--color=always` and `--sort-paths` unconditionally, since
`difftastic` doesn’t like repeated arguments, and the user might have
already set them (via `config.programs.difftastic.options`).

To clarify, if `difft` gets called with any parameter repeated, it will
croak:

    difft --color=always --color=always
    error: the argument '--color <WHEN>' cannot be used multiple times

    Usage: difft [OPTIONS] OLD-PATH NEW-PATH

    For more information, try '--help'.

By patching `cfg.options` we ensure that the parameters are defined, and
have the required value, since the user might have selected differently
for their general setup (again, via `config.programs.difftastic.options`).
2026-07-04 22:13:42 -05:00
SigheryandAustin Horstman a7209b6794 syncthing: fix badly escaped ignore patterns test
The previous test would pass no matter what the `ignoreString` was. I
don't know enough Bash/Grep to explain it, but it was easy to test by
just changing the pattern to have `WRONG` instead of `val1`, and that
would still pass the `assertFileContains` test.

I've hopefully fixed it now, by using `lib.escapeShellArg` before
passing it to `assertFileContains`, and I've also added a negative test
case, to ensure that a bad ignore string is _not_ found in the merge
script.
2026-07-04 22:02:21 -05:00
b885baad53 syncthing: add option for ignore patterns
Taken from this nixpkgs commit by @zorrobert:
https://github.com/NixOS/nixpkgs/commit/7a223b26858aaebb44009c8966a1756918e936e3

Syncthing shared folders can be configured to ignore paths. These
changes add support for this functionality.

Co-authored-by: zorrobert <118135271+zorrobert@users.noreply.github.com>
2026-07-03 22:08:35 -05:00
Ben BrownandAustin Horstman b2e4390ff3 kanshi: Add test to check service when settings are unset 2026-07-03 12:27:00 -05:00
Ben BrownandAustin Horstman 1c9ddc707c kanshi: fix check for empty settings
Fixes regression introduced by me in 74b3817.
2026-07-03 12:27:00 -05:00
isabelandAustin Horstman 3a60d513d0 clipse: use rfc 042 style settings 2026-07-03 12:26:19 -05:00
Austin Horstman a4d410db95 fzf: group related module attributes
Group repeated attr prefixes so the fzf module and new tests are easier to scan without changing behavior.
2026-07-02 15:28:15 -05:00
Austin Horstman 0396024ea5 fzf: warn on conflicting ctrl-r bindings
Detect active fzf and history-manager Ctrl-R bindings by their configured values, warn with the winning precedence, and let the normal binding options silence the warning.
2026-07-02 15:28:15 -05:00
Austin Horstman f15a4bf065 fzf: add per-shell widget overrides
Allow each shell integration to override widget commands and options before fzf is sourced, so users can disable or tune bindings only where needed.
2026-07-02 15:28:15 -05:00
Austin Horstman 2d92f5398d fzf: export environment variables to nushell
Pure Nushell sessions do not source hm-session-vars, so feed the same FZF_* values through programs.nushell.environmentVariables before sourcing fzf integration.
2026-07-02 15:28:15 -05:00
Austin Horstman fe3ce0a527 fzf: add historyWidget.command option
Expose FZF_CTRL_R_COMMAND again so users can disable fzf's Ctrl-R binding explicitly, and warn when the configured package is too old to support it.
2026-07-02 15:28:15 -05:00
Austin Horstman cde77509b6 fzf: restructure widget options
Move widget settings under nested option groups while preserving behavior through rename aliases, and centralize FZF environment variable rendering for later reuse.
2026-07-02 15:28:15 -05:00
Austin Horstman 31ca071a7d fzf: remove legacy shell integration fallback
fzf 0.48.0 embeds shell integration scripts, so keep only the supported fzf --bash, --zsh, and --fish paths and assert that integrations use a new enough package.
2026-07-02 15:28:15 -05:00
Austin Horstman 2fca6a7372 atuin: order nushell integration after fzf
Source the Nushell integration at order 2000 so fzf can keep chaining carapace at mkAfter while Atuin still wins Ctrl-R like it does in other shells.

Users who used programs.nushell.extraConfig = lib.mkAfter ... to beat Atuin now need lib.mkOrder 2001 or later.
2026-07-02 15:28:15 -05:00
Benedikt RipsandAustin Horstman f469c79b95 home: document defaults inherited from NixOS/nix-darwin 2026-07-02 10:19:51 -05:00
Benedikt RipsandAustin Horstman 7b64c06f1d home: use lib.literalMD 2026-07-02 10:19:51 -05:00
Benedikt RipsandAustin Horstman 937e85df03 home: check username being non-empty through its type 2026-07-02 10:19:51 -05:00
Benedikt RipsandAustin Horstman c0b7cadd81 home: remove redundant non-emptiness assertion
The option type `path` checks that the path starts with a slash, hence
it can not be empty.
2026-07-02 10:19:51 -05:00
andre4ik3andAustin Horstman ebc87daabc launchd: only use --wait on macOS 26 and later 2026-07-01 22:54:10 -05:00
Ben BrownandAustin Horstman 74b38179ac kanshi: Reload on config change 2026-07-01 15:07:43 -05:00
_andAustin Horstman d32537981c lib: escape backslash in lib.hm.generators.toKDL 2026-06-30 23:49:47 -05:00
Austin Horstman 2f3da3d45f launchd: fix user domain agents
User-domain agents need the Background session type to bootstrap into user/501, and activation should fail rather than silently unloading services when launchctl bootstrap fails.
2026-06-30 23:39:07 -05:00
Amadej KastelicandAustin Horstman 2a37d71bbe herdr: add module 2026-06-30 12:14:44 -05:00
Matej BlagšičandGitHub 5d72a29fc3 hyprpaper: add system target option
Adding option `systemdTarget` to hyprpaper to match other
wayland/hyprland modules. This allows easy fixup of startup dependencies
for the service, depending on system.
2026-06-29 11:13:51 -05:00
Austin Horstman 8f096963ae maintainers: regenerate all-maintainers
Regenerate the combined maintainer list with the lock-aware extractor so migrated nixpkgs maintainers and Home Manager source labels are correct.
2026-06-29 11:12:43 -05:00
Austin Horstman 7042d2b31b lib/nix: use locked nixpkgs for maintainer extraction
Importing <nixpkgs> tied extraction to the caller's NIX_PATH. When that
channel lacks a maintainer referenced through `with lib.maintainers`
(such as sherlock's `_74k1`, which exists in the locked nixpkgs but not
in an older channel), evaluation fails outright and the generated list
varies by machine. Resolve nixpkgs from flake.lock so extraction matches
what Home Manager builds with.

Also classify maintainers by identity (github/name) rather than attribute
name so renamed entries like `_9p4` still match, and correct the
arguments passed to the manual derivation so its maintainer is collected.
2026-06-29 11:12:43 -05:00
Austin Horstman 9eec3ef087 maintainers: remove quoted names in set
Prefer underscore to match nixpkgs.
2026-06-29 11:12:43 -05:00
Austin Horstman 789a35fbde maintainers: remove duplicate maintainers 2026-06-28 22:04:23 -05:00
dependabot[bot]andAustin Horstman 2c661cc960 flake: bump nixpkgs from 3e41b24 to 89570f2
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `3e41b24` to `89570f2`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/3e41b24abd260e8f71dbe2f5737d24122f972158...89570f24e97e614aa34aa9ab1c927b6578a43775)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 89570f24e97e614aa34aa9ab1c927b6578a43775
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-28 22:04:23 -05:00
Felix StuppandGitHub 1cba40f931 vscode: refactor profile paths to use unified profileDir helper
Refactor code to remove duplicated code. The location of the profile directory dependent on the profile name was duplicated over several locations. This PR unifies this into one single helper function profileDir.
2026-06-28 21:50:51 -05:00
Felix StuppandGitHub abeeea1221 vscode: fix userMcp option example syntax
literalExample with JSON code is wrong, changed to direct Nix attr
2026-06-28 21:50:04 -05:00
Jure VarlecandAustin Horstman 4ad9aaae70 generic-linux-gpu: add EGL support for Nvidia 2026-06-28 09:30:28 -05:00
Joshua FergusonandAustin Horstman 8d8a6cc50d gpg: make package optional 2026-06-26 20:08:58 -05:00
Peter BittnerandAustin Horstman 6eba758fee uv: prune Python versions and tools by diff
The prune option uninstalled all managed Python versions (and tools)
and reinstalled them on every activation, which is slow. Instead,
resolve each requested version to the install target uv would produce
and uninstall only the difference; already-correct versions are left
untouched. Resolving to the install target (not every installed match)
also prunes superseded patch releases, which uv retains on upgrade.

Tools are diffed by PEP 503-normalized package name: requested names
are extracted and normalized at build time, and the installed set is
read from uv's tool directory, whose entries are already normalized, so
no uv output is parsed. Pruning to an empty set still removes everything.
2026-06-26 20:08:04 -05:00
Vinicius DeolindoandAustin Horstman 5d320ab301 fzf: source nushell integration after carapace 2026-06-25 16:45:22 -05:00
Austin Horstman 19d7472aca codex: reorganize module files
Move option definitions and helper functions into dedicated files while keeping module assembly in default.nix.
2026-06-25 14:21:12 -05:00
Austin Horstman 2ebe9c0fe7 codex: sanitize plugin cache paths
Use sanitized plugin name and version components for cache targets while preserving manifest names in Codex config.
2026-06-25 14:21:12 -05:00
Austin Horstman 64221f9c87 codex: add lifecycle hooks option
Manage CODEX_HOME/hooks.json directly while covering the generated file in the existing TOML test.
2026-06-25 14:21:12 -05:00
Austin Horstman 1c64122e70 codex: add context override file
Expose AGENTS.override.md declaratively and cover it in the existing TOML generation test.
2026-06-25 14:21:12 -05:00
Austin Horstman 01e23f497c codex: update upstream config reference
Use the current Codex configuration docs URL and snake_case model provider keys in examples and fixtures.
2026-06-25 14:21:12 -05:00
Austin Horstman 9e3e1d95be codex: add maintainer khaneliman 2026-06-25 14:21:12 -05:00
Austin Horstman 3d8f49f360 alacritty: normalize key binding escape sequences
`pkgs.formats.toml` escapes backslashes and cannot emit raw control
characters, so key-binding `chars` written as escape text (e.g.
"\uXXXX" or the "\^[" caret form) ended up as invalid literals in the
generated config. Rewrite each escape to a placeholder before
generation and restore it to "\u" afterward.

Use `builtins.split` so escapes are handled anywhere in the value,
including when repeated or mixed with other text.
2026-06-25 14:20:58 -05:00
085e92b04a github-copilot-cli: preserve args for local-type MCP servers
forCopilotFormat only checked for type == "stdio" to add back args,
missing the case where a server already has type = "local" (e.g. set
directly by a third-party module). This caused args to be absent in the
generated mcp-config.json, failing validation.

Also, running forCopilotFormat as an extraTransform meant it ran inside
transformMcpServer, which then filtered out args = [] in its cleanup
pass. Move forCopilotFormat to run after transformMcpServer so the
empty args list is preserved in the output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 14:16:07 -05:00
Dionisie StratulatandGitHub 3b3ff17621 firefox: legacyUserProfileCustomizations when userChrome is present
When a user configures the userChrome option, automatically enable the
`toolkit.legacyUserProfileCustomizations.stylesheets` preference with
`mkDefault` priority so that the user chrome CSS is actually loaded.
The user can still override this in the settings option if needed.
2026-06-25 14:15:06 -05:00
r-vdpandAustin Horstman 3a70e333f2 fish: extract completion generator from the fish binary
fish 4.8.0 stops installing share/fish/tools/ and embeds the script
in the binary instead. Instead we use `status get-file` to extract it,
which works for fish >= 4.1.

See NixOS/nixpkgs#534907.
2026-06-24 22:36:00 -05:00
Austin Horstman a9232b8b10 launchd: use user domain for background agents
Move non-graphical Darwin services out of the GUI launchd domain so they can be managed from non-Aqua contexts such as SSH sessions.
2026-06-24 20:38:38 -05:00
Austin Horstman dc89b0c0bb launchd: add agent domain option
Allow Darwin LaunchAgents to target either the GUI or user launchd domain while preserving gui as the compatibility default.
2026-06-24 20:38:38 -05:00
Austin Horstman 062581938b claude-code: reject disabled mcp servers instead of dropping
Filtering disabled shared servers out of `.mcp.json` (#9457) collapsed
"present but disabled" and "absent" into the same output, so a server
set
`enabled = false` for OpenCode could no longer remain present in Claude
Code. `.mcp.json` has no per-server `enabled` field, but settings.json
does via `disabledMcpjsonServers`.

Keep disabled servers in `.mcp.json` and list them under
`disabledMcpjsonServers` (merged with any user-set value). This honors
the
disabled intent without dropping the server, and covers both shared and
Claude Code-native servers.
2026-06-23 11:54:25 -05:00
Benedikt RipsandAustin Horstman 1ac720f007 home: document the 'home.profileDirectory' default 2026-06-23 11:11:50 -05:00
ReStrangerandAustin Horstman d6b7bf1695 hyprland-qt-support: add news entry
Signed-off-by: ReStranger <restranger@disroot.org>
2026-06-23 11:10:25 -05:00
ReStrangerandAustin Horstman 16fdc3504e hyprland-qt-support: add test case
Signed-off-by: ReStranger <restranger@disroot.org>
2026-06-23 11:10:25 -05:00
ReStrangerandAustin Horstman 83c0e03687 hyprland-qt-support: init module 2026-06-23 11:10:25 -05:00
Bruno BELANYIandAustin Horstman 4b0b947474 zsh: add 'fastSyntaxHighlighting' 2026-06-23 08:09:29 -05:00
leiserfgandAustin Horstman 471a1d2f84 news: Add devenv entry 2026-06-22 17:14:11 -05:00
leiserfgandAustin Horstman 6cb9da2a4d devenv: init module 2026-06-22 17:14:11 -05:00
leiserfgandAustin Horstman 351959fcdb fzf: Add tests 2026-06-22 15:17:52 -05:00
leiserfgandAustin Horstman cb6bcc43e7 fzf: add nushell integration 2026-06-22 15:17:52 -05:00
mikaeladevandAustin Horstman af11a877f2 nvibrant: add module 2026-06-22 13:55:29 -05:00
Austin Horstman 66eb0fc116 docs: clarify new module news conditions
Clarify that new-module news should remain visible to users who have not enabled the new module yet.
2026-06-22 12:35:03 -05:00
leiserfgandAustin Horstman 979bfee6e1 vicinae: add firefox mesaging host 2026-06-22 08:19:19 -05:00
Austin Horstman d8dac1f668 ci: build docs in buildbot output
GitHub Actions runners lack a reusable /nix/store, so rendering the full
manual, options JSON, and manpages on every docs PR pays that cost
uncached. buildbot has a persistent store, so expose the docs outputs
there instead and leave the GHA docs step at the lightweight maintainers
check.
2026-06-21 23:44:06 -05:00
Austin Horstman f3dc0b8583 docs: update platform module guidance
Program and service modules are auto-imported now, so contributor docs should point platform-specific work at guarded module behavior and tests instead of loadModule.
2026-06-21 22:58:48 -05:00
Austin Horstman db97e414d5 docs: describe embedded module integration
Document the extra module arguments, shared modules hook, and NixOS activation modes so embedded Home Manager setup matches the current integration code.
2026-06-21 22:51:39 -05:00
Austin Horstman e731ff60ec docs: correct test command guidance
Document list and run commands separately, keep test runner usage through the flake wrapper, and avoid stale legacy full-suite commands.
2026-06-21 22:43:01 -05:00
Austin Horstman e45cc6e15a docs: fix manpage path
The generated home-configuration manpage is not compressed, so point contributors at the file that docs.manPages actually creates.
2026-06-21 22:20:43 -05:00
Austin Horstman 8f3bc61192 news: remove duplicate module announcements
Drop sparse duplicate entries for amberol and mpdscribble while keeping the richer platform-gated announcements.
2026-06-21 22:07:39 -05:00
Austin Horstman 5bf17310cf docs: preserve legacy manual links
Generate XHTML-safe redirect shims that route old single-page manual anchors to their mdBook pages.
2026-06-21 22:02:16 -05:00
Thierry DelafontaineandAustin Horstman d1ccd0721e news: add podman registries v2 entry 2026-06-21 09:20:14 -05:00
Thierry DelafontaineandAustin Horstman e3e24bfd0f podman: generate v2 registries.conf
Convert `services.podman.settings.registries` output to v2 format.
Keep legacy `insecure` and `block` inputs, add `registry` entries,
and warn when legacy options are used.

Update podman configuration tests and expected registries.conf output.

Resolves #9501
2026-06-21 09:20:14 -05:00
Austin Horstman 78e7d8b13e ci: checkout base ref for backports
Checkout v7 still treats the pull_request_target default ref as fork PR code, so pin the backport checkout to the trusted base SHA.
2026-06-20 16:06:13 -05:00
Austin Horstman ee6b160bcb ci: avoid fork head checkout in backport
Checkout v7 blocks fork PR head checkouts in pull_request_target workflows, so keep backport on the trusted base checkout and let the backport action fetch PR commits itself.
2026-06-20 16:01:01 -05:00
Anton MosichandAustin Horstman 61157d3c01 pimsync: have type be the first directive in storage blocks
pimsync 0.5.10 introduced a change to config parsing which now mandates
that the type directive is the first one in each storage block.
2026-06-20 15:54:19 -05:00
Austin Horstman 8695ecb389 tests/firefox: avoid building librewolf
Stub the Librewolf package in module tests so shared Firefox logic still runs without evaluating or building the insecure nixpkgs package.
2026-06-20 13:40:22 -05:00
Austin Horstman 8762262054 tests/darwinScrublist: add parallel-full
Scrub the default GNU Parallel package on Darwin so the module test does not build the failing DBD::CSV dependency.
2026-06-20 13:40:22 -05:00
dependabot[bot]andAustin Horstman 72a8bf11c8 flake: bump nixpkgs from ffa10e2 to 3e41b24
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `ffa10e2` to `3e41b24`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/ffa10e26ae11d676b2db836259889f1f571cb14f...3e41b24abd260e8f71dbe2f5737d24122f972158)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 3e41b24abd260e8f71dbe2f5737d24122f972158
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 13:40:22 -05:00
Juan C NunoandAustin Horstman a4ee5403ca home-manager: Pass extraArgs to nix-instantiate
My NIX_PATH is empty because of the new system.nix NixOS entry point.
I've been relying on -I options more and here they're not being passed
to nix-instantiate.
2026-06-20 12:38:59 -05:00
dependabot[bot]andAustin Horstman c3dfce6c63 ci: bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 12:35:00 -05:00
267 changed files with 6643 additions and 1822 deletions
+2 -2
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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: |
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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: |
+5 -1
View File
@@ -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
View File
@@ -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";
+1 -25
View File
@@ -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
'';
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+12 -8
View File
@@ -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}
+13 -13
View File
@@ -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.
+11 -5
View File
@@ -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.
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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
+17 -4
View File
@@ -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.
+25 -6
View File
@@ -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.
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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
+6 -2
View File
@@ -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
+3
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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}
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -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)
+132
View File
@@ -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()
+1 -1
View File
@@ -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}
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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": {
+19 -6
View File
@@ -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
{
+1
View File
@@ -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" \
+4 -4
View File
@@ -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"
+4 -4
View File
@@ -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"
+5 -5
View File
@@ -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"
+35 -20
View File
@@ -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
];
+10 -74
View File
@@ -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
View File
@@ -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
}
''
+1 -1
View File
@@ -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"
+37 -32
View File
@@ -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
View File
@@ -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
'';
})
];
+4 -2
View File
@@ -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}.
+19 -2
View File
@@ -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
View File
@@ -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.
'';
}
+14
View File
@@ -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.
'';
}
+22 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+31 -21
View File
@@ -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' ''
];
});
};
};
}
+97
View File
@@ -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 ];
}
+7 -2
View File
@@ -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"
{
+295
View File
@@ -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)
];
};
};
}
+90
View File
@@ -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)
];
};
};
}
-522
View File
@@ -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";
};
};
};
}
+239
View File
@@ -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";
};
};
};
}
+138
View File
@@ -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; });
};
}
+300
View File
@@ -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;
}
'';
};
};
}
+63
View File
@@ -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
''
} ";
};
};
};
}
+7 -3
View File
@@ -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"
];
+14 -3
View File
@@ -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
}
+72 -6
View File
@@ -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));
+9 -5
View File
@@ -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
View File
@@ -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
''
}
''
);
};
};
};
}
+23 -20
View File
@@ -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
{
+5 -4
View File
@@ -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, ... }:
+74
View File
@@ -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";
};
};
}
+71
View File
@@ -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; };
};
};
}
+1 -1
View File
@@ -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/
+1 -1
View File
@@ -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.
'';
};
+7 -2
View File
@@ -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 != [ ]) [
''
+1 -1
View File
@@ -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";
+6 -4
View File
@@ -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 {
+97
View File
@@ -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
);
};
}
+47 -4
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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 = {
+1 -1
View File
@@ -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