207 lines
7.5 KiB
Nix
207 lines
7.5 KiB
Nix
# shells.nix — Home-Manager module
|
||
#
|
||
# Reads:
|
||
# ${flakeRoot}/assets/conf/shells.nixdev/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/<shell>.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 (<shell>.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
|
||
'';
|
||
}
|