Customizing Zsh

發表於
分類於 心得

This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .

Zsh is a well-known shell, and thanks to Oh My Zsh, it allows users to easily have a beautiful and convenient shell.

I originally used it too, but I found its performance a bit slow, and its customization wasn't as strong as I wanted. So, I researched how to achieve some custom functions by writing my own .zshrc and decided to share my approach.

To see what my shell looks like when I write articles, you can check: https://asciinema.org/a/7vvdQ5xpUNJu3FAWdLvUIDsDc

This is the .zshrc I had when writing this article. You can check it out if you're interested. This dotfiles repo also includes other dotfiles I use on WSL.

Zinit and Third-Party Plugins

Although I moved away from Oh My Zsh, there are still some features from Oh My Zsh that I wanted to continue using. So, I looked for a way to use only parts of its functionality. After some research, I discovered that Zsh has plugin managers like Antigen, and Zinit is one of them.

Zinit is a powerful plugin manager with many features, but it is also quite complex and has a learning curve. It's recommended to start by reading the Introduction on the official website. It also has a turbo mode that delays loading some features to improve Zsh startup performance, making it ready to use immediately.

I used it to load some Oh My Zsh features and other useful plugins, and for the prompt, I used Powerlevel10k, also loaded with Zinit. Finally, I used it to automatically download and load some handy tools like jq and rg.

# ZInit
source ~/.zinit/bin/zinit.zsh

zinit snippet OMZL::history.zsh # Blocking 載入 Oh My Zsh 歷史的功能
zinit wait lucid for \ # Turbo mode 載入其他的一些 Oh My Zsh 插件 
  OMZL::key-bindings.zsh \
  OMZP::sudo

zinit wait lucid light-mode for \
  zsh-users/zsh-history-substring-search \ # 按下 "up" 時會自動針對目前輸入的指令搜尋
  atinit"zicompinit; zicdreplay" \
      zdharma/fast-syntax-highlighting \ # Syntax Highlighting
  atload"_zsh_autosuggest_start" \
      zsh-users/zsh-autosuggestions \ # 自動從歷史中建議
  blockf atpull"zinit creinstall -q ." \
      zsh-users/zsh-completions # 各種常用的指令 Completion

zinit ice depth=1
zinit light romkatv/powerlevel10k # Powerlevel10k

zinit lucid for \
  as"program" pick"bin/n" tj/n # 類似 nvm 的 node.js 管理器

zinit lucid from"gh-r" as"program" for \
  pick"jq-*" mv"jq-* -> jq" stedolan/jq \ # 處裡 json
  pick"ripgrep-*-linux-*" extract mv"*/rg -> rg" BurntSushi/ripgrep \ # grep 加強版
  pick"exa-linux-*" extract mv"exa-* -> exa" ogham/exa \ # ls 加強版
  pick"bat-linux-*" extract mv"*/bat -> bat" @sharkdp/bat # cat 加強版

WSL Related Settings

I mainly use this in a WSL2 environment, so there are some settings related to WSL.

# WSL specific
if [[ -v WSL_DISTRO_NAME ]] then
	alias ex=/mnt/c/Windows/explorer.exe # 可以用 ex <directory> 來打開檔案總管
	# Copy .ssh
	upd_ssh(){ # 從 Windows 的 .ssh 中複製檔案到 ~/.ssh
		rm -rf ~/.ssh
		/bin/cp -rf "/mnt/c/Users/$(whoami)/.ssh" ~/.ssh
		chmod 600 ~/.ssh/*
	}
	# Windows Path handling for performance (見下文)
	WIN_PATH=$(echo $PATH | tr ':' '\n' | grep '/mnt/c' | tr '\n' ':' | sed 's/.$//')
	export PATH=$(echo $PATH | tr ':' '\n' | grep -v '/mnt/c' | tr '\n' ':' | sed 's/.$//')
	zinit wait'3' lucid atinit'export PATH="$PATH:$WIN_PATH"' nocd for /dev/null
fi

Performance Issues of Zsh in WSL2

By default, WSL automatically adds Windows PATH to WSL PATH, which should generally be fine. However, Zsh automatically searches your PATH for autocomplete, and accessing NTFS in WSL2 is slow, leading to startup performance issues. Refer to WSL#4256.

One solution is to disable adding Windows PATH as shown below, but this makes it inconvenient to access some Windows features.

# /etc/wsl.conf
[interop]
appendWindowsPath = false

So, I wondered if I could use Zinit's delay feature to delay the PATH, and it indeed achieved my goal. The result is the three lines above:

WIN_PATH=$(echo $PATH | tr ':' '\n' | grep '/mnt/c' | tr '\n' ':' | sed 's/.$//')
export PATH=$(echo $PATH | tr ':' '\n' | grep -v '/mnt/c' | tr '\n' ':' | sed 's/.$//')
zinit wait'3' lucid atinit'export PATH="$PATH:$WIN_PATH"' nocd for /dev/null

SSH

Agent

# Keychain
if (( $+commands[keychain] )) && [[ -a ~/.ssh/id_rsa ]] then
	eval `keychain --quiet --eval --agents ssh id_rsa`
fi

This part is just a few lines above, but it uses keychain to retain the agent's session, so you need to install it first. On Debian-based systems, you can install it with the following command:

sudo apt install keychain

Autocomplete

Zsh's SSH autocomplete by default comes from many places, including domain names in /etc/hosts, but not from ~/.ssh/config...

So, I wanted it to autocomplete only the servers in my ~/.ssh/config, so I did some research and came up with the following snippet.

# Fix ssh autocomplete
zstyle ':completion:*:ssh:argument-1:*' tag-order hosts
h=()
if [[ -r ~/.ssh/config ]]; then # 讀取 config 然後把 Host 弄出來
  h=($h ${${${(@M)${(f)"$(cat ~/.ssh/config)"}:#Host *}#Host }:#*[*?]*})
fi
if [[ $#h -gt 0 ]]; then
  zstyle ':completion:*:ssh:*' hosts $h
  zstyle ':completion:*:slogin:*' hosts $h
fi