My zsh shell files are at over 3000 lines of code, not including 16 plugins of various complexity.
Having built that up over the course of many years, I decided it would be interesting to see what the “best” Zsh config I could write with some constraints.
The Rules
I know how to golf with Zsh, so don’t doubt that I can make these configs smaller, but I want the best config that fits within a certain line limit. I want to avoid excessive one-lining whenever possible, but I’m not putting some hard cap on line lengths.
I’m just going by feel, you know?
It’s subjective, but for example, each of the following would be disallowed, as they should count as two lines:
autoload compinit; compinit
for map (vicmd visual vicmd) bindkey -M "$map" '^ ' push-line-or-edit
Also, comments don’t count!
Let’s begin!
For our config sizes, I choose 5, 20, 50, and 100 lines.
5 Lines:
I love Zsh’s completion menu, and it takes just 3 lines to set up.
Beyond that, we can squeeze in some options and a nice looking prompt.
# Options
setopt autocd autopushd histverify noclobber correct interactivecomments extendedglob
# Completion setup
autoload -Uz compinit
compinit -d "${XDG_CACHE_DIR:-$HOME/.cache}/zcompdump"
# Enable completion menu
zstyle ':completion:*' menu select true
# Command prompt
# user@host ~/current/directory (job count) [exit status]
# ❭
PS1=$'%(!.%B%F{red}%m%b.%F{magenta}%n@%m)%f:%F{blue}%~%f%(1j. %F{cyan}(%j%)%f.)%(?..%F{red} [%?]%f)\n❭ '
What are those options?
autocd
: Allows changing directory without using thecd
builtin.autopushd
: Any time the shell changes directory, it pushes the previous directory onto a stack. Usepopd
to go back.histverify
: Whenever the user enters a line with!history
expansion, the shell will not execute the line, but expand it and reload the editing buffer.noclobber
:> $file
fails if$file
exists, use>!
or>|
to force the file to be truncated. Likewise,>> $file
fails if$file
does not exist, and>>!
or>>|
is needed to create it.correct
: Try to correct the spelling of commands (e.g.sl
→ls
). This will prompt the user, who can take a few actions including accepting the correction withy
.interactivecomments
: For consistency, and the ability to copy-paste shell into the line editor which have comments.extendedglob
: You’re gonna have to just readman zshexpn
if you want the details here.
What is that prompt?
If you want to step through every last detail,
check the end of man zshmisc
. It has all the prompt sequences.
%(!.%B%F{red}%m%b.%F{magenta}%n@%m)
: If we’re root, print$HOST
in bold red. Otherwise, print$USER@$HOST
.%F{blue}%~%f
: The current directory in blue, with tilde-contraction. This substitutes$HOME
with~
, but also other user’s home directories with~other
.%(1j. %F{cyan}(%j%)%f.)
: If there are background jobs, print how many of them we have in cyan in parentheses.%(?..%F{red} [%?]%f)
: If the last command exited non-zero, print it’s exit code in red with square brackets.\n❭
: Newline, and a relatively safe choice as a prompt character.
20 Lines
With the five-line config, only the prompt had any color.
It’s time to fix that.
I am only running Zsh on Linux, so I’m using dircolors
here.
Zsh supports its format to color files in its menu completion as well.
I’m also enabling groups in the completions menu, as well as our first keybind. You can cancel menu completion with Ctrl-C by default, but Escape feels more natural to me.
## OPTIONS
setopt autocd autopushd histverify noclobber correct interactivecomments extendedglob
## COLORS
# ls colors!
eval "$(dircolors -b)"
alias ls='ls --color=auto'
# Other color aliases
alias gcc='gcc -fdiagnostics-color=auto' ip='ip -color=auto' grep='grep --color=auto'
## COMPLETIONS
autoload -Uz compinit
compinit -d "${XDG_CACHE_DIR:-$HOME/.cache}/zcompdump"
# Enable completion menu
zstyle ':completion:*' menu select true
# Use ls colors in completion as well
zstyle -e ':completion:*' list-colors 'reply=("${(s.:.)LS_COLORS}")'
# Completion groups
zstyle ':completion:*' group-name ''
zstyle ':completion:*:*:-command-:*' group-order aliases global-aliases functions builtins reserved-words commands
zstyle ':completion:*' group-order original corrections expansions all-expansions
# Make messages and warnings a bit nicer.
zstyle ':completion:*:descriptions' format '➤ %B%d%b (%B%F{cyan}%n%f%b)'
zstyle ':completion:*:messages' format "%F{yellow}%d%f"
zstyle ':completion:*:warnings' format '%F{red}No %d completions found.%f'
# Show and insert `man` sections.
zstyle ':completion:*' insert-sections yes
zstyle ':completion:*' separate-sections yes
## PROMPTS
# Command prompt
# user@host ~/current/directory (job count) [exit status]
# ❭
PS1=$'%(!.%B%F{red}%m%b.%F{magenta}%n@%m)%f:%F{blue}%~%f%(1j. %F{cyan}(%j%)%f.)%(?..%F{red} [%?]%f)\n❭ '
# More obvious correction prompt
SPROMPT='Correct %B%F{red}%U%R%b%f%u to %F{green}%r%f? [%By%bes|%BN%bo|%Be%bdit|%Ba%bbort] '
## KEY BINDINGS
# Cancel menu completion with escape
bindkey -M menuselect '^[' send-break
50 Lines: Autoloading for speed
Instead of pasting the full contents of the files in this post,
you can get the raw .zshrc
here,
and the raw functions/completion-setup
here.
What is autoload
, anyway?
Completion initialization takes by far the most time of our setup. We have a dump file, but we can go further by delaying that initialization until we hit Tab for the first time.
We’ve already been using autoload -Uz compinit
,
but I haven’t explained what that does.
The function compinit
is in fact in /usr/share/zsh/functions/Completion/compinit
.
When we run autoload -Uz compinit
,
Zsh simply adds the name compinit
as an available function.
When we actually run compinit
,
Zsh will look through each directory in the array $fpath
until it finds a file which has the name compinit
,
and use the contents of that file as the function body.
(There are details I’m glossing over here;
if you’re interested you should check the “Autoloading Functions” section of man zshmisc
.)
Using this trick, we can move all our completion code into an autoloaded completion-setup
function,
which we will trigger and unload the first time we hit Tab.
More options:
I could have enabled all these options at the start on a single long line, but that would have made them hard to read. Using three lines worth now, I’m adding some more:
rc_quotes
: Concatenating single-quoted strings puts a single quote between them:'What''s the password?'
→What's the password
glob_star_short
:**.txt
for recursive globbing, instead of requiring**/*.txt
.c_bases
octal_zeroes
: By default, Zsh writes baseN
numbers as[N]#[Ngits]
. These settings tells Zsh to write hex numbers as0xFF
and octal numbers as077
, instead of16#FF
and8#77
.
History, finally!
Time to squeeze in some history configuration!
extended_history
: Adds timestamps to the history filehist_expire_dups_first
: When history needs to be trimmed, remove the oldest duplicate event.hist_ignore_dups
: If the current event is the same as the immediately previous one, don’t add it to the history.hist_ignore_space
: If the current event begins with a space, don’t add it to history.inc_append_history_time
: Write the history event out once the command completes, so the elapsed time can be recorded.
Hooks!
There’s more hooks that we could add, but at this stage we’ll just add a hook which updates the terminal title.
Using a clever parameter expansion, we can show user@host
only when we’re connected over ssh:
printf '\e]0;%s\a' "${(%):-${SSH_CONNECTION+%n@%m:}%~} − ${1+$1 − }${(C)${TERM_PROGRAM:-$TERM}}"
We’ll see more hooks in the 100 lines config.
Extra completion setup
There’s a few new things which you can find in the completion setup function.
We request caching for completions which support it,
we split directories into a separate group from files,
we generate hosts
completions from ssh config
and known_host
files,
and we add a few more keybindings while we’re in the menu.
Honestly, this might be the best place to start your Zsh config from.
100 Lines
Every step up is more than doubling the number of lines, but adding less functionality. Nowhere is that more apparent than here in our 100 line config. (TODO)