8 min read #tools

Zsh Environment

My zsh configuration is only a couple of hundred lines long. The prompt is Starship, there are many aliases, but none of these are particularly noteworthy. The only area I put real effort into, and the only one worth documenting, is completion — I’ve configured Tab to trigger a menu with previews, and I had to wrestle a bit with carapace to make it work.

The completion priority is: native compinit as the base, with carapace only as a fallback. This order is crucial. By default, carapace initializes by using compdef to take over over 700 commands. While this sounds convenient, it actually overrides zsh’s native, more granular completions and breaks fzf-tab’s dependency on subcommand context (e.g., git checkout needs to preview based on branches). Therefore, I filter out its compdef calls and only let carapace kick in when native completion fails:

source <(carapace _carapace zsh | grep -v '^compdef ')
zstyle ':completion:*' completer _complete _carapace _ignored

After completion, fzf-tab passes the candidates to fzf and provides different previews depending on the command: files are rendered with bat, directories are listed with eza, kill shows processes, systemctl displays unit status directly, git checkout lists recent commits, and git add shows diffs. This is the area that provides the biggest daily productivity boost — often, you don’t need to finish typing the command and press Enter to see the result; a quick Tab preview is enough to judge.

zstyle ':fzf-tab:complete:systemctl:*' fzf-preview 'systemctl status --no-pager $word | head -30'
zstyle ':fzf-tab:complete:git-(checkout|switch):*' fzf-preview 'git log --oneline -n 5 $word'

This preview setup looks polished, but it has its pitfalls, which I’ve documented in the comments: passing an unrecognized flag to fzf causes it to exit immediately or leave the completion list inexplicably empty; setting fzf-min-height too large leaves empty space at the bottom when there are few candidates. So those --no-sort and fzf-pad 0 settings aren’t copied from somewhere else — they’re the result of tuning.

Beyond completion, there are a few small tweaks that make the shell feel “responsive.” I’ll mention each briefly:

zsh-autosuggestions provides fish-style grayed-out history suggestions — as you type, it appends the most recently matching full command in gray text, which you can accept with or Ctrl+f. It fetches suggestions asynchronously (ZSH_AUTOSUGGEST_USE_ASYNC), so even a long history doesn’t cause keypress lag.

zsh-syntax-highlighting validates as you type: valid commands are highlighted in green, invalid ones in red, and unclosed quotes change color immediately. You can spot typos before pressing Enter. It must be sourced last in .zshrc — it needs to wrap the outermost hook for all ZLE widgets; loading it earlier would cause subsequent plugins to override it.

starship serves as the prompt: a single TOML configuration, cross-shell compatible, displaying git branch/status, language versions, k8s context, etc., based on directory context. It only appears when relevant, avoiding constant screen clutter.

ssh completion source changed: by default, it scans known_hosts (a mess of IPs, possibly hashed and unreadable). I’ve switched it to complete only from Host aliases in ~/.ssh/config. Now, ssh <TAB> directly lists machines I’ve named, making it clear who is who.

Details about zoxide’s z for jumping directories, replacing the entire ls family with eza, and using delta for git pager are covered in the CLI Toolbox.