______________________________________________________________________ date: 2025-06-04 title: zsh and GNU stow tags: - software - linux - development ______________________________________________________________________ In [my last post exploring `nvim`](https://blog.mcknight.tech/2025/05/21/nvim/), I mentioned some potential next projects on my agenda. Well, I wasted no time continuing down the path of trying to perfect my shell experience. I very quickly updated my [dotfiles repository](https://forge.mcknight.tech/d_mcknight/dotfiles) to be compatible with GNU `stow` and then went on to work on my `.zshrc` file. Neither of these are major projects, so I figured they can share this one post. ## GNU stow There are a few ways to use GNU stow and I always recommend people to [RTFM](https://www.gnu.org/software/stow/manual/stow.html#Invoking-Stow) if you ever run into problems or have questions about CLI arguments. Alternatively, this package is old and stable enough that Claude or ChatGPT can easily answer any questions you may have. For my use, I am [already keeping my dotfiles](https://blog.mcknight.tech/2024/06/21/Dotfiles/) in `~/.dotfiles`, so it makes sense for me to make that repository look like my home directory. By default, `stow` will apply the contents of my stow directory (`~/.dotfiels`) to its parent directory (`~/`). You can see my [dotfiles repository](https://forge.mcknight.tech/d_mcknight/dotfiles/src/commit/e23e66f801b0549d6195d7115ed6f033ed4318e6) now contains the same files it did before, but they are organized as if the repository root is my Home directory. One potential downside of this organization is that it adds complexity if I wanted to apply only specific dotfiles or directories, but I can't think of anything here that I would want to selectively apply to any environments. It is also worth noting that I had to remove any existing files or symlinks before running `stow .` from my `~/.dotfiles` directory, otherwise `stow` would refuse to overwrite existing files. There may be an argument to force overwrite destination files, but I prefer to manually delete things, just to make sure I'm not deleting/overwriting something I want to keep. ### Some dotfiles updates There are a few updates to my dotfiles that I never documented in my [original dotfiles post](https://blog.mcknight.tech/2024/06/21/Dotfiles/), or [Neovim post](https://blog.mcknight.tech/2025/05/21/nvim/). This may not be exhaustive, but here are some of the highlights: #### `alacritty.toml` I have been using [Alacritty](https://alacritty.org/) for my regular terminal emulator, so I have [some customizations](https://forge.mcknight.tech/d_mcknight/dotfiles/src/commit/e23e66f801b0549d6195d7115ed6f033ed4318e6/.config/alacritty/alacritty.toml) I like to apply. Below is my configuration with annotations explaining everything. ```toml [general] # Apply a GitHub dark theme for a consistent look import = ["./github_dark_high_contrast.toml" ] [colors.primary] # Override background to a neutral dark color background = "#111111" [colors.normal] # I picked this color when configuring tmux, override terminal text color to match cyan = '#008b8b' [font.normal] # Use a Nerd Font for extra symbols. I have this included in my `dotfiles` repository family = 'JetBrainsMono Nerd Font Mono' style = 'Regular' [cursor] # I prefer a blinking input cursor blink_interval = 500 blink_timeout = 0 [cursor.style] # I tried `Underline`, but I think I prefer the default block blinking = "Always" #shape = "Underline" [window] # Make the window slightly transparent to peek at what's behind opacity = 0.95 # Hide the top bar because I never close the terminal and use gTile to position it on screen decorations = "None" # I have `level` set, but it doesn't appear to do anything in Cinnamon :/ level = "AlwaysOnTop" # dynamic_padding splits extra vertical/horizontal space which makes tmux and nvim status bars look a little nicer dynamic_padding = true ``` #### JetBrains Nerd Font [Nerd Fonts](https://www.nerdfonts.com/font-downloads) let you get a consistent font that includes extra characters like filetype icons, emojis, and ligatures (special characters for things like `==`, `->`, and other character combinations). Since I have this configured in my terminal config, it makes sense to make sure the font is always available so I just include it in my dotfiles repository. ## zsh [`zsh`](https://www.zsh.org/) is a shell, like [`bash`](https://www.gnu.org/software/bash/), but with some different features that are interesting. It's worth nothing that I fully intend on using `bash` for scripting since it is far more ubiquitous than `zsh` and I am more familiar with it and its quirks. I will also note that zsh is NOT a [POSIX shell](https://en.wikipedia.org/wiki/POSIX); this is a common complaint that I see. Personally, I think I am okay with this since I haven't run into any issues thus far and `zsh` is good enough to be the default shell in popular operating systems, including macOS and TrueNAS. There are a couple reasons I decided to try `zsh`, the first being tab completion which I find helpful when completing a path or command where there are only a couple options to tab through. The other reasons I wanted to experiment with `zsh` are a bit more complex. ### RC Files [Run Commands files](https://en.wikipedia.org/wiki/RUNCOM) are basically files that are executed when a program starts. I [previously detailed my .bashrc file](https://blog.mcknight.tech/2024/03/27/Shell-Customizations/#BASH-Configuration), which is executed whenever I open a new `bash` shell. I wanted to experiment with `zsh` configuration because it feels a little more modern and powerful to me compared to `bash`. For example, my `bash` shell prompt looks like: ```bash color_off="\[\033[0m\]" # Text Reset # Regular Colors black="\[\033[0;30m\]" red="\[\033[0;31m\]" green="\[\033[0;32m\]" yellow="\[\033[0;33m\]" blue="\[\033[0;34m\]" purple="\[\033[0;35m\]" cyan="\[\033[0;36m\]" white="\[\033[0;37m\]" path_color=$blue chrome_color=$purple context_color=$cyan prompt_symbol=@ # 🚀💲 prompt='\$' if [ "$EUID" -eq 0 ]; then # Change prompt colors for root user context_color=$red prompt_symbol=💀 fi PROMPT_COMMAND='if [[ $? != 0 && $? != 130 ]];then echo -e "⚠️ \a";else echo -e "\a";fi' PS1="$chrome_color┌──"'${debian_chroot:+('${path_color}'$debian_chroot'${chrome_color}')─}${VIRTUAL_ENV:+('${path_color}'$(realpath $VIRTUAL_ENV --relative-to $PWD --relative-base /home)'${chrome_color}')─}'"[${context_color}\u${chrome_color}${prompt_symbol}${context_color}\h${chrome_color}]─(${path_color}\w${chrome_color})\n${chrome_color}└${context_color}${prompt}${color_off} " PS2="$chrome_color└>$color_off " ``` and in zsh: ```zsh function precmd { # Check previous command output and notify if [[ $? != 0 && $? != 130 ]];then echo -e "⚠️ \a" else echo -e "\a" fi if [ "$EUID" -eq 0 ];then chrome_color="{red}" else chrome_color="{magenta}" fi # Static prefix prefix="%F$chrome_color┌──[%F{cyan}%n%F$chrome_color@%F{cyan}%m%F$chrome_color]-" # Calculate extra path if [[ ${debian_chroot} ]]; then path_extra="(%F{red}${debian_chroot}%F$chrome_color)-" elif [[ ${VIRTUAL_ENV} ]]; then rel_venv=$(realpath $VIRTUAL_ENV --relative-to $PWD --relative-base /home) path_extra="[%F{blue}${rel_venv}%F$chrome_color]-" else; path_extra="" fi # Static suffix suffix="(%F{blue}%~%F${chrome_color})"$'\n'"└%F{cyan}%#%F{white} " PROMPT=$prefix$path_extra$suffix PS2="%F$chrome_color└%F{cyan}>%F{white} " ``` I find the `zsh` version to be much more readable and easier to modify since I have a method to generate the prompt instead of a single variable to cram everything into. I believe I have these two prompts looking identical in all cases and it only took me about an hour to get my `zshrc` working identically to my `bashrc`; this included moving some refactoring from `.bashrc` into `.bash_aliases` and `.profile`. I also made sure `.profile` is always sourced in `bash` and `zsh` shells to avoid duplicating code in rc files. I considered using a common `aliases` file, but decided against it since I use different aliases for different shells (i.e. `sudosu` is shell-specific). ### `zsh` Plugins Another interesting feature of `zsh` is that it supports plugins. Now just like with Neovim, there are a number of different plugin managers that can be used with `zsh`. I don't know if there is a "best" choice, but after some light reading on the popular options I could fine and some [LLM summary comparisons](https://search.brave.com/search?q=zinit+vs+omz&source=desktop&summary=1&conversation=f5495011020a89faf13bf1), I settled on [Zinit](https://github.com/zdharma-continuum/zinit) as a lightweight and apparently maintained option. #### OMZ extract This convenience command lets me extract files without having to remember the syntax for extracting `.tar.xz`, `.zip`, `.tar.gz`, etc. A simple `extract ` #### OMZ colored-man-pages This adds some color to man pages which I think makes it a little easier to skim to find CLI args and section headers. Its not the *best* IMO, but something is better than nothing here when trying to skim through what can be pretty dense documentation. #### OMZ encode64 It isn't every day that I need to get a b64-encoded representation of a string, but its handy to be able to do so quickly and easily. #### OMZ pip I like having tab completion for pip. I haven't used it too much yet, but I already see how this will save me from trying to `pip isntall` when I really mean `pip install`. I do this more than I'd like to admit. Other than that, its nice to have reminders for the less commonly used flags. #### OMZ sudo The Oh My Zsh sudo plugin adds a convenience keybind (`esc`+`esc`) to prepend `sudo` to the current command or the previous command if the input is empty. I find this to be convenient as it is fairly common to re-run the previous command with elevated privileges or to prepend `sudo` if I forgot to start with that. #### zsh-autosuggestions This plugin works much like suggestions in an IDE, providing a suggested command completion that can be filled in with a bound key (I am using `Shift`+`Tab`). I find this mapping more convenient than the default `->`, since I can reach it without moving my fingers from the home row and it is easy to remember `tab` and `shift`+`tab` are both a kind of completion. #### zsh-syntax-highlighting This plugin highlights syntax as you type in a command. This clearly identifies unresolved commands or files to help catch errors before trying to run an incomplete command. It also helps to identify un-escaped characters in a quoted string. ### `.zshrc` Now that I've explained it in parts, here's my `.zshrc` file in its entirety: ``` # Lines configured by zsh-newuser-install HISTFILE=~/.histfile HISTSIZE=1000 SAVEHIST=1000 setopt autocd notify unsetopt beep bindkey -v # End of lines configured by zsh-newuser-install # SSH completion zstyle ':completion:*:(ssh|scp|ftp|sftp|rsync):*' hosts $hosts # The following lines were added by compinstall zstyle :compinstall filename '/home/d_mcknight/.zshrc' autoload -Uz compinit compinit # End of lines added by compinstall # Source common envvars [ -f ~/.profile ] && source ~/.profile # Prompt function precmd { # Check previous command output and notify if [[ $? != 0 && $? != 130 ]];then echo -e "⚠️ \a" else echo -e "\a" fi if [ "$EUID" -eq 0 ];then chrome_color="{red}" else chrome_color="{magenta}" fi # Static prefix prefix="%F$chrome_color┌──[%F{cyan}%n%F$chrome_color@%F{cyan}%m%F$chrome_color]-" # Calculate extra path if [[ ${debian_chroot} ]]; then path_extra="(%F{red}${debian_chroot}%F$chrome_color)-" elif [[ ${VIRTUAL_ENV} ]]; then rel_venv=$(realpath $VIRTUAL_ENV --relative-to $PWD --relative-base /home) path_extra="[%F{blue}${rel_venv}%F$chrome_color]-" else; path_extra="" fi # Static suffix suffix="(%F{blue}%~%F${chrome_color})"$'\n'"└%F{cyan}%#%F{white} " PROMPT=$prefix$path_extra$suffix PS2="%F$chrome_color└%F{cyan}>%F{white} " } # Aliases alias .=source alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' alias k9=k9s alias rsync="rsync -e 'ssh -o RemoteCommand=none'" alias sudosu="sudo ZDOTDIR=$(dirname ${0:a}) zsh" alias ll='ls -alFh' alias ls="ls --color=auto" alias lsl="ls --color=auto -lah" alias ssh=ssh # Use tmux for local unelevated shells if [ -z "${SUDO_USER}" ] && [ -z "${SSH_CONNECTION}" ] && [ -z "${TERM_PROGRAM}" ]; then tmux new -A -s local_tmux fi # Define a function to use autossh with a custom bashrc function assh() { remote_file=$(mktemp) if $(ssh "$@" "cat > ${remote_file}" < ~/.bashrc > /dev/null 2>&1); then # Successfully copied bashrc to the remote. Source it upon ssh autossh -t "$@" "bash --rcfile ${remote_file}; rm ${remote_file}" else # SSH Config specifies a RemoteCommand; connect normally autossh "$@" fi } # Use ssh completion for autossh compdef autossh=ssh compdef assh=ssh # Custom dircolors [ -f ~/.dircolors ] && eval "$(dircolors ~/.dircolors)" # Kubernetes Completion which kubectl 1> /dev/null && source <(kubectl completion zsh) which helm 1> /dev/null && source <(helm completion zsh) # doctl completion which doctl 1> /dev/null && source <(doctl completion zsh) # Start in the home directory cd ~ # Ensure zinit is installed ZINIT_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/zinit/zinit.git" [ ! -d $ZINIT_HOME ] && mkdir -p "$(dirname $ZINIT_HOME)" [ ! -d $ZINIT_HOME/.git ] && git clone https://github.com/zdharma-continuum/zinit.git "$ZINIT_HOME" source "${ZINIT_HOME}/zinit.zsh" # Load zinit plugins zinit snippet OMZP::extract zinit snippet OMZP::colored-man-pages zinit snippet OMZP::encode64 zinit snippet OMZP::gh zinit snippet OMZP::pip zinit snippet OMZP::sudo zinit light zsh-users/zsh-autosuggestions zinit light zsh-users/zsh-syntax-highlighting # Key bindings config KEYTIMEOUT=5 # ^[ for esc; ^I for tab bindkey '^[[Z' autosuggest-accept ``` #### Some notes on `autossh` I have only been using `autossh` for a couple hours at this point, based on internet recommendations that it will do better at resuming with `tmux` (via `tmux-resurrect`). Based on initial testing, it appears to be working but I don't yet know if it is markedly better than plain `ssh`. Not necessarily related to `autossh`, the `assh` function I included allows for connecting to a server and applying my `.bashrc` without making permanent changes to the remote server. This does not apply to connections that use a `RemoteCommand` in the SSH config, which is intentional; I have remotes that run a `tmux` session for remote connections and I wouldn't want to mess with shell configurations when multiple connections will be attaching the same `tmux` session. ### Conclusion I don't think my shell configuration will ever be "done", but I've reached a point where I'm satisfied for now. GNU `stow` has simplified my dotfile management and made it easier to manage more configurations as I add tools to my repertoire. I now have `zsh` looking like my `bash` shell and all of my aliases and `PATH` management better organized to minimize redundant code in shell-specific config files. I've enabled a few zsh plugins that create a more pleasant shell experience with extra text highlighting and shortkeys. I have no immediate plans for what to work on next, though I [still have some ideas](https://blog.mcknight.tech/2025/05/21/nvim/#What-to-do-next). I may continue my search for a good visual file manager in the terminal, or try out Pop!_OS for its window tiling features, although I might wait for their [Cosmic DE](https://system76.com/cosmic/) to graduate to beta and try that. I also still have some [IDE exploration to do](https://blog.mcknight.tech/2025/05/18/Code-Server/#Future-Plans). As I spend more time using `nvim`, I am starting to use it more for coding tasks and it may become my primary "IDE". In any case, I am actively messing with my `nvim` configuration, so I probably have enough thoughts for another post about that.