diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md
index 7166945d0b1e..89c971d54e38 100644
--- a/nixos/doc/manual/release-notes/rl-2605.section.md
+++ b/nixos/doc/manual/release-notes/rl-2605.section.md
@@ -57,6 +57,8 @@
- [OpenThread Border Router](https://openthread.io/), a Thread border router for POSIX-based platforms that bridges Thread mesh networks to IP networks. Available as [services.openthread-border-router](#opt-services.openthread-border-router.enable).
+- [Atuin](https://atuin.sh), magical shell history — sync, search and backup your terminal history. Available as [programs.atuin](#opt-programs.atuin.enable).
+
- [Meshtastic](https://meshtastic.org), an open-source, off-grid, decentralised mesh network
designed to run on affordable, low-power devices. Available as [services.meshtasticd]
(#opt-services.meshtasticd.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 4a9d5b7695bb..dcba86a33b07 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -168,6 +168,7 @@
./programs/appimage.nix
./programs/arp-scan.nix
./programs/atop.nix
+ ./programs/atuin.nix
./programs/ausweisapp.nix
./programs/autoenv.nix
./programs/autojump.nix
diff --git a/nixos/modules/programs/atuin.nix b/nixos/modules/programs/atuin.nix
new file mode 100644
index 000000000000..51541959606a
--- /dev/null
+++ b/nixos/modules/programs/atuin.nix
@@ -0,0 +1,195 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+let
+ inherit (lib) escapeShellArgs;
+
+ cfg = config.programs.atuin;
+
+ tomlFormat = pkgs.formats.toml { };
+
+ settingsFile = tomlFormat.generate "atuin-config" cfg.settings;
+in
+{
+ options.programs.atuin = {
+ enable = lib.mkEnableOption "atuin";
+
+ package = lib.mkPackageOption pkgs "atuin" { };
+
+ enableBashIntegration = lib.mkEnableOption "Bash integration" // {
+ default = config.programs.bash.enable;
+ defaultText = lib.literalExpression "config.programs.bash.enable";
+ };
+
+ enableZshIntegration = lib.mkEnableOption "Zsh integration" // {
+ default = config.programs.zsh.enable;
+ defaultText = lib.literalExpression "config.programs.zsh.enable";
+ };
+
+ enableFishIntegration = lib.mkEnableOption "Fish integration" // {
+ default = config.programs.fish.enable;
+ defaultText = lib.literalExpression "config.programs.fish.enable";
+ };
+
+ flags = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ example = [
+ "--disable-up-arrow"
+ "--disable-ctrl-r"
+ ];
+ description = ''
+ Flags to append to the shell hook.
+ '';
+ };
+
+ settings = lib.mkOption {
+ type = tomlFormat.type;
+ default = { };
+ example = lib.literalExpression ''
+ {
+ auto_sync = true;
+ sync_frequency = "5m";
+ sync_address = "https://api.atuin.sh";
+ search_mode = "prefix";
+ }
+ '';
+ description = ''
+ Configuration written to {file}`/etc/atuin/config.toml`.
+
+ See for the full list
+ of options.
+ '';
+ };
+
+ daemon = {
+ enable = lib.mkEnableOption "the Atuin daemon" // {
+ default = pkgs.stdenv.hostPlatform.isLinux;
+ defaultText = lib.literalExpression "pkgs.stdenv.hostPlatform.isLinux";
+ };
+
+ logLevel = lib.mkOption {
+ type = lib.types.enum [
+ "trace"
+ "debug"
+ "info"
+ "warn"
+ "error"
+ ];
+ default = "info";
+ description = ''
+ Log level for the Atuin daemon.
+ '';
+ };
+ };
+
+ themes = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.oneOf [
+ tomlFormat.type
+ lib.types.path
+ lib.types.lines
+ ]
+ );
+ description = ''
+ Each theme is written to
+ {file}`/etc/atuin/themes/theme-name.toml`
+ where the name of each attribute is the theme-name
+
+ See for the full list
+ of options.
+ '';
+ default = { };
+ example = lib.literalExpression ''
+ {
+ "my-theme" = {
+ theme.name = "My Theme";
+ colors = {
+ Base = "#000000";
+ Title = "#FFFFFF";
+ };
+ };
+ }
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ environment.systemPackages = [ cfg.package ];
+
+ # Atuin only reads from ATUIN_CONFIG_DIR or XDG_CONFIG_HOME, not XDG_CONFIG_DIRS,
+ # so we must set ATUIN_CONFIG_DIR to point to the system-wide config location.
+ environment.variables.ATUIN_CONFIG_DIR = "/etc/atuin";
+
+ environment.etc = lib.mkMerge [
+ (lib.mkIf (cfg.settings != { }) {
+ "atuin/config.toml".source = settingsFile;
+ })
+
+ (lib.mkIf (cfg.themes != { }) (
+ builtins.mapAttrs' (
+ name: theme:
+ lib.nameValuePair "atuin/themes/${name}.toml" {
+ source =
+ if builtins.isString theme then
+ pkgs.writeText "atuin-theme-${name}" theme
+ else if builtins.isPath theme || lib.isStorePath theme then
+ theme
+ else
+ tomlFormat.generate "atuin-theme-${name}" theme;
+ }
+ ) cfg.themes
+ ))
+ ];
+
+ programs.bash.interactiveShellInit = lib.mkIf cfg.enableBashIntegration ''
+ if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
+ eval "$(${lib.getExe cfg.package} init bash ${escapeShellArgs cfg.flags})"
+ fi
+ '';
+
+ programs.zsh.interactiveShellInit = lib.mkIf cfg.enableZshIntegration ''
+ if [[ $options[zle] = on ]]; then
+ eval "$(${lib.getExe cfg.package} init zsh ${escapeShellArgs cfg.flags})"
+ fi
+ '';
+
+ programs.fish.interactiveShellInit = lib.mkIf cfg.enableFishIntegration ''
+ ${lib.getExe cfg.package} init fish ${escapeShellArgs cfg.flags} | source
+ '';
+
+ systemd = lib.mkIf (cfg.daemon.enable && pkgs.stdenv.hostPlatform.isLinux) {
+ user.services.atuin-daemon = {
+ unitConfig = {
+ Description = "Atuin daemon";
+ Requires = [ "atuin-daemon.socket" ];
+ };
+ serviceConfig = {
+ ExecStart = "${lib.getExe cfg.package} daemon start";
+ Environment = [ "ATUIN_LOG=${cfg.daemon.logLevel}" ];
+ Restart = "on-failure";
+ RestartSteps = 3;
+ RestartMaxDelaySec = 6;
+ };
+ };
+
+ user.sockets.atuin-daemon = {
+ unitConfig = {
+ Description = "Atuin daemon socket";
+ };
+ socketConfig = {
+ ListenStream = "%t/atuin/atuin.sock";
+ SocketMode = "0640";
+ DirectoryMode = "0740";
+ RemoveOnStop = true;
+ };
+ wantedBy = [ "sockets.target" ];
+ };
+ };
+ };
+
+ meta.maintainers = cfg.package.meta.maintainers;
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 8a1173f52cd5..11f3cd49ea44 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -236,6 +236,7 @@ in
atticd = runTest ./atticd.nix;
attr = pkgs.callPackage ./attr.nix { };
atuin = runTest ./atuin.nix;
+ atuin-programs = runTest ./atuin-programs.nix;
audiobookshelf = runTest ./audiobookshelf.nix;
audit = runTest ./audit.nix;
audit-testsuite = runTest ./audit-testsuite.nix;
diff --git a/nixos/tests/atuin-programs.nix b/nixos/tests/atuin-programs.nix
new file mode 100644
index 000000000000..14477bae9b08
--- /dev/null
+++ b/nixos/tests/atuin-programs.nix
@@ -0,0 +1,39 @@
+{ pkgs, ... }:
+{
+ name = "atuin";
+ meta.maintainers = pkgs.atuin.meta.maintainers;
+
+ nodes.machine = {
+ programs = {
+ bash.enable = true;
+ fish.enable = true;
+ zsh.enable = true;
+
+ atuin = {
+ enable = true;
+ settings = {
+ auto_sync = false;
+ };
+ };
+ };
+ };
+
+ testScript = ''
+ start_all()
+ machine.wait_for_unit("default.target")
+
+ # Check atuin is installed
+ machine.succeed("atuin --version")
+
+ # Check shell integration - verify the init scripts can be sourced without error
+ machine.succeed("bash -c 'eval \"$(atuin init bash)\"'")
+ machine.succeed("zsh -c 'eval \"$(atuin init zsh)\"'")
+ machine.succeed("fish -c 'atuin init fish | source'")
+
+ # Verify config file was created
+ machine.succeed("grep -q 'auto_sync = false' /etc/atuin/config.toml")
+
+ # Verify daemon socket unit is enabled
+ machine.succeed("systemctl --user --machine=root@ is-enabled atuin-daemon.socket")
+ '';
+}
diff --git a/pkgs/by-name/at/atuin/package.nix b/pkgs/by-name/at/atuin/package.nix
index ebb7e47cf50d..dac5a4961653 100644
--- a/pkgs/by-name/at/atuin/package.nix
+++ b/pkgs/by-name/at/atuin/package.nix
@@ -58,7 +58,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
passthru = {
tests = {
- inherit (nixosTests) atuin;
+ inherit (nixosTests) atuin atuin-programs;
};
updateScript = nix-update-script { };
};