nixos/atuin: add system-wide atuin program option

Add a `programs.atuin` module to enable atuin shell history
integration system-wide, without requiring home-manager.

Features:
- Shell integration for bash, zsh, and fish (enabled by default)
- Configuration via `settings` (written to /etc/atuin/config.toml,
  using ATUIN_CONFIG_DIR since atuin does not support XDG_CONFIG_DIRS)
- Custom themes support
- Package selection via `package` option
- Additional flags via `flags` option
- Daemon option with systemd user service and socket activation
  (Linux only), required for some configurations such as root-on-zfs

Example usage:

  programs.atuin = {
    enable = true;
    settings = {
      auto_sync = true;
      search_mode = "prefix";
    };
  };
This commit is contained in:
Peter Bittner
2026-04-23 09:10:04 +02:00
parent 04c36bd130
commit 3770285fd9
6 changed files with 239 additions and 1 deletions

View File

@@ -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).

View File

@@ -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

View File

@@ -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 <https://docs.atuin.sh/configuration/config/> 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 <https://docs.atuin.sh/guide/theming/> 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;
}

View File

@@ -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;

View File

@@ -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")
'';
}

View File

@@ -58,7 +58,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
passthru = {
tests = {
inherit (nixosTests) atuin;
inherit (nixosTests) atuin atuin-programs;
};
updateScript = nix-update-script { };
};