我的 Zsh 配置哲学:启动 200ms 的模块化终端环境

一套基于 Zi 异步插件的 Zsh 配置方案,涵盖模块化设计、启动时间优化、Vi 模式、fzf 补全生态和日常效率工具链。

出发点

终端是开发者的家。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 放在最后是因为它依赖前面的环境就绪。

额外的细节:.zshrcprecmd 钩子里挂了一个一次性的启动计时函数,第一次 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 配置,欢迎交流。

Interaction

读完之后

分享海报
Interaction

评论区