From 85d5b0f0c4a7a07e65911aa61bc29a444488dda6 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 4 Jun 2025 22:20:50 -0700 Subject: [PATCH] Add zsh and GNU stow post --- 2025-06-04_zsh-gnu-stow.md | 391 +++++++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 2025-06-04_zsh-gnu-stow.md diff --git a/2025-06-04_zsh-gnu-stow.md b/2025-06-04_zsh-gnu-stow.md new file mode 100644 index 0000000..be4af7a --- /dev/null +++ b/2025-06-04_zsh-gnu-stow.md @@ -0,0 +1,391 @@ +______________________________________________________________________ + +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. +