From be0a1be663df5c0c16085dad9f1f0f93aad9470d Mon Sep 17 00:00:00 2001 From: user3301 <26126682+user3301@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:50:42 +1000 Subject: [PATCH 1/3] Add minimal bash configuration as a portable fallback Bash gets a stow-shaped bash/ package (.bashrc, .bash_profile) symlinked into $HOME with mkOutOfStoreSymlink, matching how zsh/ is wired. This keeps the same files usable with GNU Stow on non-Nix machines (macOS, Archlinux). - programs.bash is dropped: it generates its own ~/.bashrc, which collides with the symlink. bash-completion replaces what enableCompletion provided. - zoxide added; mcfly moved from common.nix to shell.nix so both shell integrations sit beside the configs that initialize them. Every home profile imports both modules, so availability is unchanged. - .bashrc creates ~/.bash_history when missing; mcfly refuses to start without it. - vi-insert gets \C-l, \C-a, \C-e and \C-k, which readline leaves unbound there but zsh's viins keymap provides. - New bash-syntax flake check, wired into CI alongside zsh-syntax. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NyrQoVjNGCECw6fUdYLNDr --- .github/workflows/ci.yml | 3 +- .gitignore | 1 + bash/.bash_profile | 3 ++ bash/.bashrc | 109 +++++++++++++++++++++++++++++++++++++++ docs/README.nix.md | 10 ++-- flake.nix | 11 ++++ home/modules/common.nix | 1 - home/modules/shell.nix | 25 ++++++--- 8 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 bash/.bash_profile create mode 100644 bash/.bashrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b949072..b859272 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,8 @@ jobs: nix build --no-link \ .#checks.x86_64-linux.nix-quality \ .#checks.x86_64-linux.lua-format \ - .#checks.x86_64-linux.zsh-syntax + .#checks.x86_64-linux.zsh-syntax \ + .#checks.x86_64-linux.bash-syntax - name: Evaluate WSL configuration run: nix eval --raw '.#nixosConfigurations.nixos-wsl.config.system.build.toplevel.drvPath' diff --git a/.gitignore b/.gitignore index a3ab5e1..8525522 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Local/work-specific configuration files .zshenv.local +.bashrc.local git/.config/git/config.local # Herdr runtime state (written into the symlinked config dir; only config.toml is tracked) diff --git a/bash/.bash_profile b/bash/.bash_profile new file mode 100644 index 0000000..e053780 --- /dev/null +++ b/bash/.bash_profile @@ -0,0 +1,3 @@ +# Login shells read this instead of ~/.bashrc, so source ~/.bashrc here to keep +# login and non-login interactive shells identical. +[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc" diff --git a/bash/.bashrc b/bash/.bashrc new file mode 100644 index 0000000..f7a0bf6 --- /dev/null +++ b/bash/.bashrc @@ -0,0 +1,109 @@ +# Minimal bash configuration, kept as a portable fallback to zsh/.zshrc. +# +# Layout mirrors the zsh config: environment first (so non-interactive login +# shells get PATH too, the job zsh/.zshenv does), interactive-only setup after +# the guard. Login shells reach this file through ~/.bash_profile. + +# --------------------------------------------------------------------------- +# Environment (mirrors zsh/.zshenv) +# --------------------------------------------------------------------------- +export EDITOR="nvim" +export VISUAL="nvim" +export DOTFILES="${DOTFILES:-$HOME/dotfiles}" + +export PATH="$HOME/.local/bin:$PATH" +export PATH="$HOME/go/bin:$PATH" +# This is required for asdf (0.16 or later) +export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH" +# reorder PATH to make sure Nix's home-manager PATH comes before +# system path, so it uses nix installed package first +export PATH="$HOME/.nix-profile/bin:$PATH" + +export CLOUDSDK_PYTHON=/usr/bin/python3 + +# GPG TTY for commit signing +GPG_TTY=$(tty 2>/dev/null) +export GPG_TTY + +# Home Manager session variables, when this machine is managed by Nix. +# The first file sourced sets a guard, so the second is a no-op. +for __hm_vars in \ + "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh" \ + "/etc/profiles/per-user/$USER/etc/profile.d/hm-session-vars.sh"; do + [ -r "$__hm_vars" ] && . "$__hm_vars" +done +unset __hm_vars + +# --------------------------------------------------------------------------- +# Interactive shells only +# --------------------------------------------------------------------------- +case $- in + *i*) ;; + *) return ;; +esac + +# History. mcfly bails out with an error unless the file already exists, so +# create it here rather than waiting for the first shell to exit. +HISTFILE="${HISTFILE:-$HOME/.bash_history}" +[ -e "$HISTFILE" ] || (umask 077 && : > "$HISTFILE") +HISTSIZE=50000 +HISTFILESIZE=100000 +HISTCONTROL=ignoreboth:erasedups +HISTIGNORE="ls:ll:la:cd:pwd:exit:clear:history" +HISTTIMEFORMAT="%F %T " +shopt -s histappend cmdhist + +# Shell behaviour (options unknown to bash 3.2 are skipped quietly) +shopt -s checkwinsize globstar autocd cdspell dirspell no_empty_cmd_completion 2>/dev/null + +# vi keybindings, matching the zsh vi-mode plugin +set -o vi +# Keys bash leaves unbound in vi-insert but zsh's viins keymap provides +bind -m vi-insert '"\C-l": clear-screen' 2>/dev/null +bind -m vi-insert '"\C-a": beginning-of-line' 2>/dev/null +bind -m vi-insert '"\C-e": end-of-line' 2>/dev/null +bind -m vi-insert '"\C-k": kill-line' 2>/dev/null +# Open the current buffer line in $EDITOR, as ^x^e does in zsh +bind -m vi-insert '"\C-x\C-e": edit-and-execute-command' 2>/dev/null + +# bash-completion (Nix profile, system, or Homebrew) +for __bc in \ + "$HOME/.nix-profile/share/bash-completion/bash_completion" \ + "/etc/profiles/per-user/$USER/share/bash-completion/bash_completion" \ + /usr/share/bash-completion/bash_completion \ + /opt/homebrew/etc/profile.d/bash_completion.sh \ + /usr/local/etc/profile.d/bash_completion.sh; do + if [ -r "$__bc" ]; then + . "$__bc" + break + fi +done +unset __bc + +# Prompt: user@host:cwd (git-branch) +__bash_git_branch() { + local branch + branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 0 + printf ' (%s)' "$branch" +} +PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[33m\]$(__bash_git_branch)\[\e[0m\]\$ ' + +# General aliases +alias v="nvim" + +# eza as default lister +if command -v eza >/dev/null 2>&1; then + alias ls='eza --group-directories-first --icons=auto' + alias ll='eza -l --group-directories-first --icons=auto --git' + alias la='eza -la --group-directories-first --icons=auto --git' + alias lt='eza --tree --level=2 --icons=auto' +fi + +# zoxide: smarter cd, provides `z` and `zi` +command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init bash)" + +# mcfly: fuzzy ^R history search (keep last, it wraps the prompt hooks) +command -v mcfly >/dev/null 2>&1 && eval "$(mcfly init bash)" + +# Load local-specific configuration if it exists +[ -f "$DOTFILES/bash/.bashrc.local" ] && . "$DOTFILES/bash/.bashrc.local" diff --git a/docs/README.nix.md b/docs/README.nix.md index 87767ad..2fc0432 100644 --- a/docs/README.nix.md +++ b/docs/README.nix.md @@ -96,7 +96,7 @@ Before activation, customize `home.username` and `home.homeDirectory` in `home/d ### Development Tools - **Editors**: Neovim - **Terminal**: Herdr, Wezterm, Yazi -- **Shell**: Zsh with completion and syntax highlighting +- **Shell**: Zsh (primary, oh-my-zsh + mcfly) with a minimal Bash fallback (zoxide + mcfly) - **Version Control**: Git, GitHub CLI, Lazygit - **CLI Tools**: ripgrep, fd, bat, fzf, jq, and more - **Rust Toolchain**: rustc, Cargo, Clippy, and rustfmt (managed by Nix) @@ -105,6 +105,7 @@ Before activation, customize `home.username` and `home.homeDirectory` in `home/d ### Configuration Management - **Modular**: Shared modules + platform-specific overrides - **Symlinked**: Your existing dotfiles are symlinked (edit directly, no rebuild needed) +- **Stow-compatible**: each top-level config directory is also a GNU Stow package, so the same repo works on non-Nix machines (`stow bash zsh nvim`) - **Reproducible**: Same environment across all machines - **Declarative**: Everything in version control @@ -117,7 +118,7 @@ dotfiles/ ├── home/ # Home Manager configurations │ ├── modules/ # Shared modules │ │ ├── common.nix # Base settings -│ │ ├── shell.nix # Zsh configuration +│ │ ├── shell.nix # Zsh + Bash configuration │ │ ├── dev-tools.nix # Development packages │ │ ├── neovim.nix # Neovim + LSPs │ │ └── terminal.nix # Herdr, Wezterm, Yazi @@ -140,7 +141,8 @@ dotfiles/ │ ├── herdr/.config/herdr/ │ ├── wezterm/.config/wezterm/ │ ├── yazi/.config/yazi/ -│ └── zsh/ +│ ├── zsh/ +│ └── bash/ # Minimal fallback shell │ └── docs/ ├── DEPLOYMENT.md # Detailed deployment guide @@ -248,7 +250,7 @@ home-manager switch --rollback **Current approach**: Manual management for simplicity - Generate SSH keys manually on each machine: `ssh-keygen -t ed25519` - Keep sensitive credentials outside version control -- Use local overrides (`.zshenv.local` pattern) for machine-specific secrets +- Use local overrides (`.zshenv.local` / `.bashrc.local` pattern) for machine-specific secrets ### Development Shells Create project-specific environments: diff --git a/flake.nix b/flake.nix index 185d7e4..e9d8e57 100644 --- a/flake.nix +++ b/flake.nix @@ -339,6 +339,17 @@ zsh -n ${self}/zsh/.zshrc touch "$out" ''; + + bash-syntax = + pkgs.runCommand "bash-syntax" + { + nativeBuildInputs = [ pkgs.bash ]; + } + '' + bash -n ${self}/bash/.bash_profile + bash -n ${self}/bash/.bashrc + touch "$out" + ''; } ); diff --git a/home/modules/common.nix b/home/modules/common.nix index cc1e3b2..bbac733 100644 --- a/home/modules/common.nix +++ b/home/modules/common.nix @@ -25,6 +25,5 @@ # Misc utilities wget curl - mcfly ]; } diff --git a/home/modules/shell.nix b/home/modules/shell.nix index 6a1525d..ffd901f 100644 --- a/home/modules/shell.nix +++ b/home/modules/shell.nix @@ -2,10 +2,17 @@ { home = { - # Install oh-my-zsh package (not managed by Home Manager's programs.zsh.oh-my-zsh) - # This allows your .zshrc to have full control packages = with pkgs; [ + # Install oh-my-zsh package (not managed by Home Manager's programs.zsh.oh-my-zsh) + # This allows your .zshrc to have full control oh-my-zsh + + # Completions for the bash fallback (programs.bash is off, see below) + bash-completion + + # Shell integrations wired up in zsh/.zshrc and bash/.bashrc + mcfly + zoxide ]; file = { @@ -19,12 +26,14 @@ # Symlink .zshenv ".zshenv".source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfiles/zsh/.zshenv"; - }; - }; - # Bash configuration (fallback) - programs.bash = { - enable = true; - enableCompletion = true; + # Bash fallback. programs.bash stays disabled so these stay plain symlinks + # into the repo, which keeps them usable via GNU Stow on non-Nix machines. + ".bashrc".source = + config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfiles/bash/.bashrc"; + + ".bash_profile".source = + config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfiles/bash/.bash_profile"; + }; }; } From ab589219c7cd41ab418c4cd10b6b6eb3e2788d05 Mon Sep 17 00:00:00 2001 From: user3301 <26126682+user3301@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:05:01 +1000 Subject: [PATCH 2/3] Only set GPG_TTY when stdin is a terminal `tty` writes "not a tty" to stdout (not stderr) and exits non-zero when stdin is not a terminal, so the previous `GPG_TTY=$(tty 2>/dev/null)` stored that literal string and clobbered any inherited value. Bash sources ~/.bashrc for non-interactive ssh commands and zsh sources .zshenv for every shell, so this broke GPG signing over ssh. Assign only when `tty` succeeds. Also correct the mcfly comment: the local override file is what runs last, not mcfly. Note there that a local PROMPT_COMMAND assignment drops zoxide's hook, since zoxide occupies index 0 of the array. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NyrQoVjNGCECw6fUdYLNDr --- bash/.bashrc | 15 ++++++++++----- zsh/.zshenv | 8 ++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/bash/.bashrc b/bash/.bashrc index f7a0bf6..b962078 100644 --- a/bash/.bashrc +++ b/bash/.bashrc @@ -21,9 +21,12 @@ export PATH="$HOME/.nix-profile/bin:$PATH" export CLOUDSDK_PYTHON=/usr/bin/python3 -# GPG TTY for commit signing -GPG_TTY=$(tty 2>/dev/null) -export GPG_TTY +# GPG TTY for commit signing. `tty` writes "not a tty" to stdout and fails when +# stdin is not a terminal, so assign only on success and keep any inherited value. +if __tty=$(tty 2>/dev/null); then + export GPG_TTY="$__tty" +fi +unset __tty # Home Manager session variables, when this machine is managed by Nix. # The first file sourced sets a guard, so the second is a no-op. @@ -102,8 +105,10 @@ fi # zoxide: smarter cd, provides `z` and `zi` command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init bash)" -# mcfly: fuzzy ^R history search (keep last, it wraps the prompt hooks) +# mcfly: fuzzy ^R history search (after zoxide, so it wraps that prompt hook) command -v mcfly >/dev/null 2>&1 && eval "$(mcfly init bash)" -# Load local-specific configuration if it exists +# Load local-specific configuration if it exists. It is sourced last so it can +# override everything above; append to PROMPT_COMMAND with += rather than +# assigning, or zoxide's hook is dropped. [ -f "$DOTFILES/bash/.bashrc.local" ] && . "$DOTFILES/bash/.bashrc.local" diff --git a/zsh/.zshenv b/zsh/.zshenv index 9284849..de855d3 100755 --- a/zsh/.zshenv +++ b/zsh/.zshenv @@ -6,8 +6,12 @@ export PATH="$HOME/go/bin:$PATH" export PATH="$HOME/.nix-profile/bin:$PATH" export CLOUDSDK_PYTHON=/usr/bin/python3 -# GPG TTY for commit signing -export GPG_TTY=$(tty) +# GPG TTY for commit signing. `tty` writes "not a tty" to stdout when stdin is +# not a terminal, so assign only on success and keep any inherited value. +if __tty=$(tty 2>/dev/null); then + export GPG_TTY=$__tty +fi +unset __tty # General aliases alias v="nvim" From 8d89f9771f8aa1b1501b325117d5cb1c7b6b7f9d Mon Sep 17 00:00:00 2001 From: user3301 <26126682+user3301@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:14:25 +1000 Subject: [PATCH 3/3] Source .bashrc.local before the zoxide and mcfly hooks macOS ships bash 3.2.57, where PROMPT_COMMAND is a plain string rather than the array bash 5.1+ uses. Sourcing the local override after the integrations meant a bare `PROMPT_COMMAND=` assignment there wiped both hooks on 3.2, and zoxide's hook even on modern bash, since it sits at index 0. Moving it ahead of them makes both tools append to whatever the local file leaves behind, on either PROMPT_COMMAND flavour. The local file still overrides everything above it and gains the ability to set MCFLY_*/_ZO_* before the tools read them; it only loses the ability to clobber the hooks, which is the point. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NyrQoVjNGCECw6fUdYLNDr --- bash/.bashrc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bash/.bashrc b/bash/.bashrc index b962078..1658d26 100644 --- a/bash/.bashrc +++ b/bash/.bashrc @@ -102,13 +102,14 @@ if command -v eza >/dev/null 2>&1; then alias lt='eza --tree --level=2 --icons=auto' fi +# Load local-specific configuration if it exists. Deliberately before the two +# integrations below: on bash < 5.1 (macOS /bin/bash is 3.2) PROMPT_COMMAND is a +# plain string, so a bare assignment here would wipe their hooks. Both append to +# whatever it leaves behind, and it can still set MCFLY_*/_ZO_* to configure them. +[ -f "$DOTFILES/bash/.bashrc.local" ] && . "$DOTFILES/bash/.bashrc.local" + # zoxide: smarter cd, provides `z` and `zi` command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init bash)" # mcfly: fuzzy ^R history search (after zoxide, so it wraps that prompt hook) command -v mcfly >/dev/null 2>&1 && eval "$(mcfly init bash)" - -# Load local-specific configuration if it exists. It is sourced last so it can -# override everything above; append to PROMPT_COMMAND with += rather than -# assigning, or zoxide's hook is dropped. -[ -f "$DOTFILES/bash/.bashrc.local" ] && . "$DOTFILES/bash/.bashrc.local"