# shells.nix — Home-Manager module # # Reads: # ${flakeRoot}/assets/conf/dev/terminal/enabled_shells.conf # ${flakeRoot}/assets/conf/dev/terminal/aliases.conf # # For each enabled shell in [enabled_shells]: # - installs/enables shell (where HM has an enable option) # - if ${flakeRoot}/assets/conf/dev/terminal/.conf exists, sources it # - ensures a *user-editable* aliases file exists in the shell’s default location # - if a shell is disabled, its aliases file is removed # . # Notes on “editable”: # - We do NOT manage the aliases file with xdg.configFile/home.file (those would be overwritten). # - Instead, we create/remove files via home.activation (create only if missing). { config, pkgs, lib, flakeRoot, ... }: let terminalDir = flakeRoot + "/assets/conf/dev/terminal"; enabledFile = terminalDir + "/enabled_shells.conf"; aliasesFile = terminalDir + "/aliases.conf"; trim = lib.strings.trim; # ---------- minimal INI-ish parser (sections + raw lines) ---------- readMaybe = p: if builtins.pathExists p then builtins.readFile p else ""; normalizeLine = l: trim (lib.replaceStrings [ "\r" ] [ "" ] l); parseSections = text: let lines = map normalizeLine (lib.splitString "\n" text); isHeader = l: let s = l; in lib.hasPrefix "[" s && lib.hasSuffix "]" s && builtins.stringLength s >= 3; nameOf = l: lib.removeSuffix "]" (lib.removePrefix "[" l); folded = builtins.foldl' (st: l: if l == "" then st else if isHeader l then st // { current = nameOf l; } else let cur = st.current; prev = st.sections.${cur} or []; in st // { sections = st.sections // { ${cur} = prev ++ [ l ]; }; } ) { current = "__root__"; sections = {}; } lines; in folded.sections; enabledSections = parseSections (readMaybe enabledFile); aliasSections = parseSections (readMaybe aliasesFile); # [enabled_shells] lines: key = yes/no enabledShells = let raw = enabledSections.enabled_shells or []; parseKV = l: let m = builtins.match ''^([A-Za-z0-9_-]+)[[:space:]]*=[[:space:]]*(.*)$'' l; in if m == null then null else { k = trim (builtins.elemAt m 0); v = lib.toLower (trim (builtins.elemAt m 1)); }; kvs = builtins.filter (x: x != null) (map parseKV raw); in map (x: x.k) (builtins.filter (x: x.v == "yes" || x.v == "true" || x.v == "1") kvs); shellEnabled = shell: builtins.elem shell enabledShells; # ---------- per-shell repo config file (.conf) ---------- shellConfPath = shell: terminalDir + "/${shell}.conf"; shellConfExists = shell: builtins.pathExists (shellConfPath shell); sourceIfExistsSh = p: '' if [ -f "${toString p}" ]; then source "${toString p}" fi ''; # ---------- aliases section helpers ---------- secLines = name: aliasSections.${name} or []; secText = name: lib.concatStringsSep "\n" (secLines name); # Default alias-file locations bashAliasesPath = "${config.home.homeDirectory}/.bash_aliases"; zshAliasesPath = "${config.home.homeDirectory}/.zsh_aliases"; fishAliasesPath = "${config.xdg.configHome}/fish/conf.d/aliases.fish"; # Seeds (created once; user can edit afterwards) bashSeed = '' # Created once from: ${toString aliasesFile} # Edit freely; Home Manager will not overwrite this file. # ${secText "bash_zsh"} ${secText "bash_specific"} ''; zshSeed = '' # Created once from: ${toString aliasesFile} # Edit freely; Home Manager will not overwrite this file. # ${secText "bash_zsh"} ${secText "zsh_specific"} ''; # Fish: translate [bash_zsh] POSIX alias lines + append [fish_specific] as-is parsePosixAlias = l: let m = builtins.match ''^[[:space:]]*alias[[:space:]]+([A-Za-z0-9_+-]+)=(.*)$'' l; in if m == null then null else let name = trim (builtins.elemAt m 0); rhs0 = trim (builtins.elemAt m 1); unquote = if lib.hasPrefix "'" rhs0 && lib.hasSuffix "'" rhs0 then lib.removeSuffix "'" (lib.removePrefix "'" rhs0) else if lib.hasPrefix "\"" rhs0 && lib.hasSuffix "\"" rhs0 then lib.removeSuffix "\"" (lib.removePrefix "\"" rhs0) else rhs0; in { inherit name; cmd = unquote; }; escapeForFish = s: lib.replaceStrings [ "\\" "\"" "$" "`" ] [ "\\\\" "\\\"" "\\$" "\\`" ] s; fishTranslated = let parsed = builtins.filter (x: x != null) (map parsePosixAlias (secLines "bash_zsh")); in lib.concatStringsSep "\n" (map (a: ''alias ${a.name} "${escapeForFish a.cmd}"'') parsed); fishSeed = '' # Created once from: ${toString aliasesFile} # Edit freely; Home Manager will not overwrite this file. status is-interactive; or exit # Translated from [bash_zsh]: ${fishTranslated} # From [fish_specific]: ${secText "fish_specific"} ''; in { xdg.enable = true; # Install/enable shells (no login-shell changes) programs.bash.enable = shellEnabled "bash"; programs.zsh.enable = shellEnabled "zsh"; programs.fish.enable = shellEnabled "fish"; home.packages = (lib.optionals (shellEnabled "dash") [ pkgs.dash ]) ++ (lib.optionals (shellEnabled "nushell") [ pkgs.nushell ]); # Source per-shell repo config (if present) AND source the user alias file (if it exists). # Important: define each option only ONCE. programs.bash.bashrcExtra = lib.mkIf (shellEnabled "bash") (lib.mkAfter '' ${lib.optionalString (shellConfExists "bash") (sourceIfExistsSh (shellConfPath "bash"))} if [ -f "${bashAliasesPath}" ]; then source "${bashAliasesPath}" fi ''); programs.zsh.initContent = lib.mkIf (shellEnabled "zsh") (lib.mkAfter '' ${lib.optionalString (shellConfExists "zsh") (sourceIfExistsSh (shellConfPath "zsh"))} if [ -f "${zshAliasesPath}" ]; then source "${zshAliasesPath}" fi ''); programs.fish.interactiveShellInit = lib.mkIf (shellEnabled "fish") (lib.mkAfter '' ${lib.optionalString (shellConfExists "fish") '' if test -f "${toString (shellConfPath "fish")}" source "${toString (shellConfPath "fish")}" end ''} if test -f "${fishAliasesPath}" source "${fishAliasesPath}" end ''); # Create/remove alias files based on enabled shells home.activation.shellAliasesFiles = lib.hm.dag.entryAfter [ "writeBoundary" ] '' set -euo pipefail # bash ------------------------------------------------------- if ${if shellEnabled "bash" then "true" else "false"}; then cat > "${bashAliasesPath}" <<'EOF' ${bashSeed} EOF else rm -f "${bashAliasesPath}" fi # zsh ------------------------------------------------------- if ${if shellEnabled "zsh" then "true" else "false"}; then cat > "${zshAliasesPath}" <<'EOF' ${zshSeed} EOF else rm -f "${zshAliasesPath}" fi # fish ------------------------------------------------------- if ${if shellEnabled "fish" then "true" else "false"}; then mkdir -p "$(dirname "${fishAliasesPath}")" cat > "${fishAliasesPath}" <<'EOF' ${fishSeed} EOF else rm -f "${fishAliasesPath}" fi # fish if ${if shellEnabled "fish" then "true" else "false"}; then mkdir -p "$(dirname "${fishAliasesPath}")" if [ ! -f "${fishAliasesPath}" ]; then cat > "${fishAliasesPath}" <<'EOF' ${fishSeed} EOF fi else rm -f "${fishAliasesPath}" fi ''; }