.zshrc from scratch

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 the cd builtin.
  • autopushd: Any time the shell changes directory, it pushes the previous directory onto a stack. Use popd 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. slls). This will prompt the user, who can take a few actions including accepting the correction with y.
  • interactivecomments: For consistency, and the ability to copy-paste shell into the line editor which have comments.
  • extendedglob: You’re gonna have to just read man 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 base N numbers as [N]#[Ngits]. These settings tells Zsh to write hex numbers as 0xFF and octal numbers as 077, instead of 16#FF and 8#77.

History, finally!

Time to squeeze in some history configuration!

  • extended_history: Adds timestamps to the history file
  • hist_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)

zsh  config  linux