16 KiB
date | title | tags | |||
---|---|---|---|---|---|
2025-06-05 | zsh and GNU stow |
|
In my last post exploring 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 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 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 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
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, or Neovim post. This may not be exhaustive, but here are some of the highlights:
alacritty.toml
I have been using Alacritty for my regular terminal emulator, so I have some customizations I like to apply. Below is my configuration with annotations explaining everything.
[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 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
is a shell, like 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;
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 are basically files that
are executed when a program starts. I
previously detailed my .bashrc
file,
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:
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:
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,
I settled on 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. 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 to graduate to beta and try that.
I also still have some IDE exploration to do.
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.