From 21399deff2b0fab5a6243d1607d474e0356db7f7 Mon Sep 17 00:00:00 2001 From: "Kristopher James Kent (kjkent)" Date: Tue, 31 Dec 2024 19:53:48 +0000 Subject: [PATCH] zsh: improve dotDir handling Previously, `config.programs.zsh.dotDir` prepended strings with `$HOME`. This caused issues like nix-community#5100, where `$HOME` is inconsistently resolved in time for the evaluation of the option. The handling of this variable is also inconsistent with how paths are handled elsewhere, including within the same module, where `config.programs.zsh.history.path` does not mutate the supplied string. To preserve backwards compatibility, this change prepends `config.home.homeDirectory` to relative paths, while assigning absolute paths unchanged. Tests for both cases are added. Signed-off-by: Austin Horstman --- modules/programs/zsh/default.nix | 37 +++++---- modules/programs/zsh/lib.nix | 42 ++++++++++ modules/programs/zsh/plugins/default.nix | 80 +++++++++---------- modules/programs/zsh/plugins/oh-my-zsh.nix | 8 +- modules/programs/zsh/plugins/prezto.nix | 14 ++-- tests/modules/programs/zsh/default.nix | 7 +- tests/modules/programs/zsh/dotdir.nix | 52 ++++++++++++ .../programs/zsh/history-path-new-default.nix | 3 +- .../programs/zsh/history-path-old-custom.nix | 3 +- .../programs/zsh/history-path-old-default.nix | 3 +- tests/modules/programs/zsh/plugins.nix | 6 +- 11 files changed, 173 insertions(+), 82 deletions(-) create mode 100644 modules/programs/zsh/lib.nix create mode 100644 tests/modules/programs/zsh/dotdir.nix diff --git a/modules/programs/zsh/default.nix b/modules/programs/zsh/default.nix index 282022b7..5179e43a 100644 --- a/modules/programs/zsh/default.nix +++ b/modules/programs/zsh/default.nix @@ -18,13 +18,13 @@ let cfg = config.programs.zsh; - relToDotDir = file: (optionalString (cfg.dotDir != null) (cfg.dotDir + "/")) + file; - bindkeyCommands = { emacs = "bindkey -e"; viins = "bindkey -v"; vicmd = "bindkey -a"; }; + + inherit (import ./lib.nix { inherit config lib; }) homeDir dotDirAbs dotDirRel; in { imports = [ @@ -100,8 +100,9 @@ in }; dotDir = mkOption { - default = null; - example = ".config/zsh"; + default = homeDir; + defaultText = "`config.home.homeDirectory`"; + example = "`\${config.xdg.configHome}/zsh`"; description = '' Directory where the zsh configuration and more should be located, relative to the users home directory. The default is the home @@ -144,9 +145,9 @@ in default = { }; example = literalExpression '' { - docs = "$HOME/Documents"; - vids = "$HOME/Videos"; - dl = "$HOME/Downloads"; + docs = "$\{config.home.homeDirectory}/Documents"; + vids = "$\{config.home.homeDirectory}/Videos"; + dl = "$\{config.home.homeDirectory}/Downloads"; } ''; description = '' @@ -339,30 +340,28 @@ in dirHashesStr = concatStringsSep "\n" ( lib.mapAttrsToList (k: v: ''hash -d ${k}="${v}"'') cfg.dirHashes ); - - zdotdir = "$HOME/" + lib.escapeShellArg cfg.dotDir; in mkIf cfg.enable ( lib.mkMerge [ (mkIf (cfg.envExtra != "") { - home.file."${relToDotDir ".zshenv"}".text = cfg.envExtra; + home.file."${dotDirRel}/.zshenv".text = cfg.envExtra; }) (mkIf (cfg.profileExtra != "") { - home.file."${relToDotDir ".zprofile"}".text = cfg.profileExtra; + home.file."${dotDirRel}/.zprofile".text = cfg.profileExtra; }) (mkIf (cfg.loginExtra != "") { - home.file."${relToDotDir ".zlogin"}".text = cfg.loginExtra; + home.file."${dotDirRel}/.zlogin".text = cfg.loginExtra; }) (mkIf (cfg.logoutExtra != "") { - home.file."${relToDotDir ".zlogout"}".text = cfg.logoutExtra; + home.file."${dotDirRel}/.zlogout".text = cfg.logoutExtra; }) - (mkIf (cfg.dotDir != null) { - home.file."${relToDotDir ".zshenv"}".text = '' - export ZDOTDIR=${zdotdir} + (mkIf (dotDirAbs != homeDir) { + home.file."${dotDirRel}/.zshenv".text = '' + export ZDOTDIR=${dotDirAbs} ''; # When dotDir is set, only use ~/.zshenv to source ZDOTDIR/.zshenv, @@ -370,12 +369,12 @@ in # already set correctly (by e.g. spawning a zsh inside a zsh), all env # vars still get exported home.file.".zshenv".text = '' - source ${zdotdir}/.zshenv + source ${dotDirAbs}/.zshenv ''; }) { - home.file."${relToDotDir ".zshenv"}".text = '' + home.file."${dotDirRel}/.zshenv".text = '' # Environment variables . "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh" @@ -486,7 +485,7 @@ in )) ]; - home.file."${relToDotDir ".zshrc"}".text = cfg.initContent; + home.file."${dotDirRel}/.zshrc".text = cfg.initContent; } ] ); diff --git a/modules/programs/zsh/lib.nix b/modules/programs/zsh/lib.nix new file mode 100644 index 00000000..96525f85 --- /dev/null +++ b/modules/programs/zsh/lib.nix @@ -0,0 +1,42 @@ +{ config, lib, ... }: +let + cfg = config.programs.zsh; +in +rec { + homeDir = config.home.homeDirectory; + + # escapes for shell and cleans trailing slashes that can mess with test regex + cleanPathStr = pathStr: lib.escapeShellArg (lib.removeSuffix "/" pathStr); + + # strips home directory prefix from absolute path. + mkRelPathStr = + pathStr: + # is already a relative path + if (!lib.hasPrefix "/" pathStr) then + cleanPathStr pathStr + # is an absolute path within home dir + else if (lib.hasPrefix homeDir pathStr) then + cleanPathStr (lib.removePrefix "${homeDir}/" pathStr) + # is an absolute path not in home dir + else + throw '' + Attempted to convert an absolute path not within home directory to a + home-relative path. + Conversion attempted on: + ${pathStr} + ...which does not start with: + ${homeDir} + ''; + + # given a relative (or unknown) path, returns absolute by prepending home dir + # if path doesn't begin with "/" + mkAbsPathStr = + pathStr: cleanPathStr ((lib.optionalString (!lib.hasPrefix "/" pathStr) "${homeDir}/") + pathStr); + + dotDirAbs = mkAbsPathStr cfg.dotDir; + dotDirRel = mkRelPathStr cfg.dotDir; + + # If dotDir is default (i.e., the user's home dir) plugins are stored in + # ~/.zsh/plugins -- otherwise, in `programs.zsh.dotDir`/plugins + pluginsDir = dotDirAbs + (lib.optionalString (homeDir == dotDirAbs) "/.zsh") + "/plugins"; +} diff --git a/modules/programs/zsh/plugins/default.nix b/modules/programs/zsh/plugins/default.nix index f51918c2..0398aa5b 100644 --- a/modules/programs/zsh/plugins/default.nix +++ b/modules/programs/zsh/plugins/default.nix @@ -8,7 +8,7 @@ let cfg = config.programs.zsh; - relToDotDir = file: (lib.optionalString (cfg.dotDir != null) (cfg.dotDir + "/")) + file; + inherit (import ../lib.nix { inherit config lib; }) pluginsDir; in { imports = [ @@ -88,49 +88,45 @@ in }; }; - config = - let - pluginsDir = if cfg.dotDir != null then relToDotDir "plugins" else ".zsh/plugins"; - in - lib.mkIf (cfg.plugins != [ ]) { - home.file = lib.foldl' (a: b: a // b) { } ( - map (plugin: { "${pluginsDir}/${plugin.name}".source = plugin.src; }) cfg.plugins - ); + config = lib.mkIf (cfg.plugins != [ ]) { + home.file = lib.foldl' (a: b: a // b) { } ( + map (plugin: { "${pluginsDir}/${plugin.name}".source = plugin.src; }) cfg.plugins + ); - programs.zsh = { - # Many plugins require compinit to be called - # but allow the user to opt out. - enableCompletion = lib.mkDefault true; + programs.zsh = { + # Many plugins require compinit to be called + # but allow the user to opt out. + enableCompletion = lib.mkDefault true; - initContent = lib.mkMerge [ - (lib.mkOrder 560 ( - lib.concatStrings ( - map (plugin: '' - path+="$HOME/${pluginsDir}/${plugin.name}" - fpath+="$HOME/${pluginsDir}/${plugin.name}" - ${ - (lib.optionalString (plugin.completions != [ ]) '' - fpath+=(${ - lib.concatMapStringsSep " " ( - completion: "\"$HOME/${pluginsDir}/${plugin.name}/${completion}\"" - ) plugin.completions - }) - '') - } - '') cfg.plugins - ) - )) + initContent = lib.mkMerge [ + (lib.mkOrder 560 ( + lib.concatStrings ( + map (plugin: '' + path+="${pluginsDir}/${plugin.name}" + fpath+="${pluginsDir}/${plugin.name}" + ${ + (lib.optionalString (plugin.completions != [ ]) '' + fpath+=(${ + lib.concatMapStringsSep " " ( + completion: "\"${pluginsDir}/${plugin.name}/${completion}\"" + ) plugin.completions + }) + '') + } + '') cfg.plugins + ) + )) - (lib.mkOrder 900 ( - lib.concatStrings ( - map (plugin: '' - if [[ -f "$HOME/${pluginsDir}/${plugin.name}/${plugin.file}" ]]; then - source "$HOME/${pluginsDir}/${plugin.name}/${plugin.file}" - fi - '') cfg.plugins - ) - )) - ]; - }; + (lib.mkOrder 900 ( + lib.concatStrings ( + map (plugin: '' + if [[ -f "${pluginsDir}/${plugin.name}/${plugin.file}" ]]; then + source "${pluginsDir}/${plugin.name}/${plugin.file}" + fi + '') cfg.plugins + ) + )) + ]; }; + }; } diff --git a/modules/programs/zsh/plugins/oh-my-zsh.nix b/modules/programs/zsh/plugins/oh-my-zsh.nix index 9aaee57a..f4461dc0 100644 --- a/modules/programs/zsh/plugins/oh-my-zsh.nix +++ b/modules/programs/zsh/plugins/oh-my-zsh.nix @@ -7,9 +7,7 @@ let inherit (lib) mkOption optionalString types; - relToDotDir = - file: - (lib.optionalString (config.programs.zsh.dotDir != null) (config.programs.zsh.dotDir + "/")) + file; + inherit (import ../lib.nix { inherit config lib; }) dotDirRel; cfg = config.programs.zsh; @@ -34,7 +32,7 @@ let custom = mkOption { default = ""; type = types.str; - example = "$HOME/my_customizations"; + example = "\${config.home.homeDirectory}/my_customizations"; description = '' Path to a custom oh-my-zsh package to override config of oh-my-zsh. See @@ -76,7 +74,7 @@ in packages = [ cfg.oh-my-zsh.package ]; file = { - "${relToDotDir ".zshenv"}".text = '' + "${dotDirRel}/.zshenv".text = '' ZSH="${cfg.oh-my-zsh.package}/share/oh-my-zsh"; ZSH_CACHE_DIR="${config.xdg.cacheHome}/oh-my-zsh"; ''; diff --git a/modules/programs/zsh/plugins/prezto.nix b/modules/programs/zsh/plugins/prezto.nix index 4f90b82c..3fdf6fbf 100644 --- a/modules/programs/zsh/plugins/prezto.nix +++ b/modules/programs/zsh/plugins/prezto.nix @@ -14,9 +14,7 @@ let cfg = config.programs.zsh.prezto; - relToDotDir = - file: - (optionalString (config.programs.zsh.dotDir != null) (config.programs.zsh.dotDir + "/")) + file; + inherit (import ../lib.nix { inherit config lib; }) dotDirRel; preztoModule = types.submodule { options = { @@ -425,25 +423,25 @@ in { home.packages = [ cfg.package ]; - home.file."${relToDotDir ".zprofile"}".text = '' + home.file."${dotDirRel}/.zprofile".text = '' # Generated by Nix source ${cfg.package}/share/zsh-prezto/runcoms/zprofile ''; - home.file."${relToDotDir ".zlogin"}".text = '' + home.file."${dotDirRel}/.zlogin".text = '' # Generated by Nix source ${cfg.package}/share/zsh-prezto/runcoms/zlogin ''; - home.file."${relToDotDir ".zlogout"}".text = '' + home.file."${dotDirRel}/.zlogout".text = '' # Generated by Nix source ${cfg.package}/share/zsh-prezto/runcoms/zlogout ''; # Using mkAfter to make sure we load Home-Manager's environment # variables first (see modules/prgrams/zsh.nix) - home.file."${relToDotDir ".zshenv"}".text = lib.mkAfter '' + home.file."${dotDirRel}/.zshenv".text = lib.mkAfter '' # Generated by Nix source ${cfg.package}/share/zsh-prezto/runcoms/zshenv ''; - home.file."${relToDotDir ".zpreztorc"}".text = '' + home.file."${dotDirRel}/.zpreztorc".text = '' # Generated by Nix ${optionalString (cfg.caseSensitive != null) '' zstyle ':prezto:*:*' case-sensitive '${lib.hm.booleans.yesNo cfg.caseSensitive}' diff --git a/tests/modules/programs/zsh/default.nix b/tests/modules/programs/zsh/default.nix index 94c88f0e..f73cdb66 100644 --- a/tests/modules/programs/zsh/default.nix +++ b/tests/modules/programs/zsh/default.nix @@ -1,16 +1,19 @@ { zsh-abbr = ./zsh-abbr.nix; zsh-aliases = ./aliases.nix; + zsh-dotdir-absolute = import ./dotdir.nix "absolute"; + zsh-dotdir-default = import ./dotdir.nix "default"; + zsh-dotdir-relative = import ./dotdir.nix "relative"; zsh-history-ignore-pattern = ./history-ignore-pattern.nix; zsh-history-path-new-custom = ./history-path-new-custom.nix; zsh-history-path-new-default = ./history-path-new-default.nix; - zsh-history-path-old-custom = ./history-path-old-custom.nix; zsh-history-path-old-default = ./history-path-old-default.nix; + zsh-history-path-old-custom = ./history-path-old-custom.nix; zsh-history-substring-search = ./history-substring-search.nix; zsh-plugins = ./plugins.nix; zsh-prezto = ./prezto.nix; - zsh-zprof = ./zprof.nix; zsh-session-variables = ./session-variables.nix; zsh-syntax-highlighting = ./syntax-highlighting.nix; + zsh-zprof = ./zprof.nix; zshrc-contents-priorities = ./zshrc-content-priorities.nix; } diff --git a/tests/modules/programs/zsh/dotdir.nix b/tests/modules/programs/zsh/dotdir.nix new file mode 100644 index 00000000..0f4594cc --- /dev/null +++ b/tests/modules/programs/zsh/dotdir.nix @@ -0,0 +1,52 @@ +case: +{ + config, + lib, + options, + ... +}: +let + home = config.home.homeDirectory; + + dotDir = + let + subDir = "subdir/subdir2"; + in + if case == "absolute" then + "${home}/${subDir}" + else if case == "relative" then + subDir + else if case == "default" then + options.programs.zsh.dotDir.default + else + abort "Test condition not provided."; + + absDotDir = lib.optionalString (!lib.hasPrefix home dotDir) "${home}/" + dotDir; + relDotDir = lib.removePrefix home dotDir; +in +{ + config = { + programs.zsh = { + enable = true; + inherit dotDir; + }; + + test.stubs.zsh = { }; + + nmt.script = lib.concatStringsSep "\n" [ + # check dotDir entrypoint exists + "assertFileExists home-files/${relDotDir}/.zshenv" + + # for non-default dotDir only: + (lib.optionalString (case != "default") '' + # check .zshenv in homeDirectory sources .zshenv in dotDir + assertFileRegex home-files/.zshenv \ + "source [\"']\?${absDotDir}/.zshenv[\"']\?" + + # check that .zshenv in dotDir exports ZDOTDIR + assertFileRegex home-files/${relDotDir}/.zshenv \ + "export ZDOTDIR=[\"']\?${absDotDir}[\"']\?" + '') + ]; + }; +} diff --git a/tests/modules/programs/zsh/history-path-new-default.nix b/tests/modules/programs/zsh/history-path-new-default.nix index 4dfca0d0..813239fa 100644 --- a/tests/modules/programs/zsh/history-path-new-default.nix +++ b/tests/modules/programs/zsh/history-path-new-default.nix @@ -3,6 +3,7 @@ programs.zsh.enable = true; nmt.script = '' - assertFileRegex home-files/.zshrc '^HISTFILE="$HOME/.zsh_history"$' + assertFileRegex home-files/.zshrc \ + '^HISTFILE="${config.home.homeDirectory}/.zsh_history"$' ''; } diff --git a/tests/modules/programs/zsh/history-path-old-custom.nix b/tests/modules/programs/zsh/history-path-old-custom.nix index f4f5ac38..0b8fa451 100644 --- a/tests/modules/programs/zsh/history-path-old-custom.nix +++ b/tests/modules/programs/zsh/history-path-old-custom.nix @@ -6,6 +6,7 @@ }; nmt.script = '' - assertFileRegex home-files/.zshrc '^HISTFILE="$HOME/some/directory/zsh_history"$' + assertFileRegex home-files/.zshrc \ + '^HISTFILE="${config.home.homeDirectory}/some/directory/zsh_history"$' ''; } diff --git a/tests/modules/programs/zsh/history-path-old-default.nix b/tests/modules/programs/zsh/history-path-old-default.nix index 8e4debb3..e2a7a384 100644 --- a/tests/modules/programs/zsh/history-path-old-default.nix +++ b/tests/modules/programs/zsh/history-path-old-default.nix @@ -3,6 +3,7 @@ programs.zsh.enable = true; nmt.script = '' - assertFileRegex home-files/.zshrc '^HISTFILE="$HOME/.zsh_history"$' + assertFileRegex home-files/.zshrc \ + '^HISTFILE="${config.home.homeDirectory}/.zsh_history"$' ''; } diff --git a/tests/modules/programs/zsh/plugins.nix b/tests/modules/programs/zsh/plugins.nix index 658a9b5b..4756be3e 100644 --- a/tests/modules/programs/zsh/plugins.nix +++ b/tests/modules/programs/zsh/plugins.nix @@ -23,9 +23,9 @@ in test.stubs.zsh = { }; nmt.script = '' - assertFileRegex home-files/.zshrc '^path+="$HOME/.zsh/plugins/mockPlugin"$' - assertFileRegex home-files/.zshrc '^fpath+="$HOME/.zsh/plugins/mockPlugin"$' - assertFileRegex home-files/.zshrc '^fpath+=("$HOME/.zsh/plugins/mockPlugin/share/zsh/site-functions" "$HOME/.zsh/plugins/mockPlugin/share/zsh/vendor-completions")$' + assertFileRegex home-files/.zshrc '^path+="/home/hm-user/.zsh/plugins/mockPlugin"$' + assertFileRegex home-files/.zshrc '^fpath+="/home/hm-user/.zsh/plugins/mockPlugin"$' + assertFileRegex home-files/.zshrc '^fpath+=("/home/hm-user/.zsh/plugins/mockPlugin/share/zsh/site-functions" "/home/hm-user/.zsh/plugins/mockPlugin/share/zsh/vendor-completions")$' ''; }; }