zsh and GNU stow				#14
		
		
	
					 1 changed files with 406 additions and 0 deletions
				
			
		
							
								
								
									
										406
									
								
								2025-06-05_zsh-gnu-stow.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										406
									
								
								2025-06-05_zsh-gnu-stow.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,406 @@ | |||
| --- | ||||
| 
 | ||||
| date: 2025-06-05 | ||||
| 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 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. | ||||
| I also wanted to try out some of the plugins and the configuration, which I find much easier to work with, | ||||
| compared to `bashrc`. | ||||
| 
 | ||||
| ### 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 | ||||
| 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 those 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 | ||||
| up on the popular options I could find 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 <file>` is much easier | ||||
| to remember than the specific commands for each compression algorithm. | ||||
| 
 | ||||
| #### 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 | ||||
| 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 (saving 5 keystrokes compared to | ||||
| `sudo !!`) | ||||
| 
 | ||||
| #### zsh-autosuggestions | ||||
| 
 | ||||
| This plugin feels 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 the components, 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. | ||||
| This also highlights that I still do use `bash` for most of my remote connections, since `bash` | ||||
| is available by default on every Linux distribution I've come across and `zsh` is far less ubiquitous. | ||||
| 
 | ||||
| ### 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'll likely 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 adding to my `nvim` configuration, so I probably have enough thoughts for another post | ||||
| about that. | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue