出发点
终端是开发者的家。Zsh 本身足够强大,但裸 Zsh 和调教后的 Zsh 差距如同毛坯房和精装房。我的目标是:功能丰富前提下启动时间不超过 300ms,配置易于维护而非屎山。
经过几轮重构,最终形成了一套 Zi 异步插件 + conf.d 模块化 的配置方案。这篇文章不贴完整代码(代码在 dotfiles 里),只讲设计思路和关键细节。
目录结构
~/.config/zsh/
├── .zshrc # 主控:计时启动、加载 conf.d、绑定 precmd
├── .zshenv # 环境变量,仅设置 ZDOTDIR 指向本目录
├── preload.zsh # Phase 0:Zi 插件管理器自举 + 性能开关
├── plugins.zsh # Phase 1:所有插件的异步加载声明
├── lastload.zsh # Phase 2:Starship 提示符(最后加载)
├── conf.d/ # 模块化配置,按数字前缀顺序加载
│ ├── 00-functions.zsh # 基础工具函数
│ ├── 01-environment.zsh # PATH / PNPM / SSH keys
│ ├── 02-optins.zsh # Zsh 选项 + fzf 配置 + kill 补全修复
│ ├── 04-grml.zsh # GRML 风格函数
│ ├── 05-aliases.zsh # 别名集合
│ ├── 06-functions.zsh # 自定义函数 (yazi proxy 等)
│ ├── 100-eza-aliases.zsh # Eza 分层别名体系
│ ├── 101-archive-helper.plugin.zsh # 压缩命令速查
│ ├── 103-conda.zsh # Conda 懒加载
│ ├── 104-expend-alias.zsh # Ctrl+X 别名展开
│ └── 106-cmdh-expand.zsh # Ctrl+G 命令建议
└── zsh-dependencies.txt # 外部工具依赖清单
核心原则:每个文件只做一件事,数字前缀严格定义加载顺序。00 级是基础函数,01 级是环境变量,02 级是核心配置,04-06 是用户侧函数与别名,100+ 是工具集成。添加新功能只需要新建一个 conf.d 文件,不会污染已有逻辑。
三个加载阶段
.zshrc 是总指挥,按三个阶段分步加载:
Phase 0: preload.zsh — 系统最开始的 50ms。关闭 compinit 全局初始化、设置补全缓存路径、声明 Zi 的 HOME_DIR 和 BIN_DIR。如果 Zi 本体不存在,自动 git clone。设置 ZVM_INIT_MODE=sourcing 确保 zsh-vi-mode 同步加载(vi-mode 不能异步,否则按键绑定会乱)。EDITOR=nvim 也在这里声明,避免插件初始化时变量为空。
Phase 1: plugins.zsh — 所有插件的 turbo 异步加载声明都集中在这个文件里。Zi 用 ice wait"X" 语法控制每个插件的加载时机,X 是单个字母,字母序决定了加载批次:
| 插件 | wait 值 | 策略 |
|---|---|---|
| zsh-vi-mode | wait"0" |
同步,必须最先就位 |
| zsh-completions | wait"0a" |
第一批异步,加载后调 zicompinit |
| zsh-autosuggestions | wait"0c" |
第二批,加载后启动异步建议引擎 |
| fzf-tab | wait"0d" |
第三批,加载后 eval "$(fzf --zsh)" |
| fast-syntax-highlighting | wait"0e" |
第四批,语法高亮最后就位 |
| atuin | wait"0b" |
第二批,加载后绑定 ^R |
理解这个设计:wait"0" 会阻塞 Phase 0 完成之前,0a-0e 按字母序依次在后台加载。这样 zsh-autosuggestions 加载完再上 fzf-tab,最后再上语法高亮,保证依赖关系正确。所有带 lucid 标记的插件不会干扰提示符刷新。
Phase 2: lastload.zsh — 一行逻辑:如果终端是图形终端(非 TTY),启动 Starship 提示符;如果是纯控制台,回退到极简提示符。Starhship 放在最后是因为它依赖前面的环境就绪。
额外的细节:.zshrc 的 precmd 钩子里挂了一个一次性的启动计时函数,第一次 precmd 时计算 EPOCHREALTIME 差值并导出给 Starship 显示,之后自动卸载自己。最终看到的效果类似:
~/projects work* 227ms
>
七个你可能会想抄的功能
1. Vi 模式 + jj 退出
zsh-vi-mode 让你在命令行中用 Vim 键位编辑。插入模式下连按 jj 返回 NORMAL 模式:
ZVM_VI_INSERT_ESCAPE_BINDKEY="jj"
比按 Escape 快得多,手指不需要离开主行。
2. Ctrl+V:在编辑器中编辑当前命令
长命令在行内改很痛苦。按 Ctrl+V 会把当前缓冲区发到 Neovim,在那里可以自由编辑。保存退出后内容写回命令行,光标位置也会精确还原——因为函数内部通过 Neovim 的 autocmd CursorMoved 实时追踪光标行号列号。
这个功能参考了 edit-command-line 的通用实现,但增加了 Neovim 专属的光标同步逻辑。代码约 100 行,不要被长度吓到,实际逻辑就是:打开临时文件 -> 追踪光标 -> 写回 -> 清理。
3. Ctrl+X:递归展开别名
gc 是什么?k 是什么?按 Ctrl+X 会把当前缓冲区的别名递归展开到最终命令。支持防止死循环(检测自引用别名,跟原始命令完全一致才停止),最大深度 100 层。
实际用例:输入 gc ... 后按 Ctrl+X,变成 git clone ...。
4. Ctrl+G:自然语言转命令 (cmdh)
输入中文描述,按 Ctrl+G,调用 cmdh 脚本将其转换为实际命令。例如 "压缩当前目录" -> tar -czf ...。这是一个自定义工具,不是公开包。
5. 递归别名:不可逆但很爽的短命令
部分别名故意设计为不可逆——别名展开后不等于原始命令:
alias v='nvim' # v file.txt -> nvim file.txt
alias yay=paru # Arch 包管理器
alias npm=pnpm # 防止 npm 误用
alias lg=lazygit
alias k="ps aux | fzf | awk '{print \$2}' | xargs kill -9" # 交互式杀进程
k 是最佳例子:列出所有进程 -> fzf 筛选 -> kill -9。不需要记 PID,眼睛看名字选就行。
6. Eza 别名分层体系
将 Eza 别名按用途分成三个层级:
- Script 级:
ls/l/la— 无图标无表头,输出干净,可以 pipe 给 awk - 交互级:
lls/lla/ll/llt/llS— 带图标带表头,给人看 - Tree 级:
tree2— 以树形显示两层
区分这两级是关键。很多人的配置把 ls 设成 "eza --icons",导致脚本里 ls | while read 解析出乱码。
7. Proxy 开关函数
一键切换 HTTP/HTTPS/SOCKS5 代理:
proxy on # 打开代理 (127.0.0.1:7897)
proxy off # 关闭代理
proxy status # 查看状态
同时设置小写和大写变量,兼容不同工具的习惯。
FZF 生态整合
FZF 是这套配置的交互核心,主要配置了三件事:
通用界面: 统一配色(prompt 青色、pointer 红色、marker 绿色)、Nerd Font 图标、50% 高度、预览窗右侧。
fzf-tab 补全: 按 Tab 时不是弹出传统补全菜单,而是 fzf 交互式列表。对 kill 命令做了特殊处理——列出当前用户所有进程(不是仅 session 任务),PID 标红,预览窗显示进程命令行。对 cd 命令,预览窗直接显示目标目录的 ls --color 输出。
Zoxide 集成: z 跳转到常用目录,zl 弹出 fzf 交互选择,c 选择但不回车(回显到命令行等确认)。三者各有用途。
工具链依赖
运行这套配置需要以下外部工具:
| 工具 | 用途 |
|---|---|
| starship | 提示符引擎 |
| fzf | 模糊查找核心 |
| fd | FZF_DEFAULT_COMMAND,比 find 快 |
| ripgrep | 全文搜索 |
| bat | 文件预览 |
| eza | ls 替代 |
| zoxide | 目录跳转 |
| atuin | SQLite 历史管理 |
全部可以通过 paru 一键安装。不需要全部装齐才能用——缺工具只是对应功能不生效,不会报错。
实际启动时间:冷启动约 200-250ms(含所有插件 + Starship),热启动(zsh 守护进程模式下)更快。关键是不会有 "加载卡顿" 的感觉,因为所有 I/O 操作都被 Zi 的 turbo 模式异步化了。
完整配置托管在 GitHub: snemc/dotfiles。如果你也有值得展示的 zsh 配置,欢迎交流。
评论区