从零恢复我的终端开发环境:Ubuntu 24.04 与 macOS
这是一份按依赖顺序编排的开发环境恢复手册:从系统前置条件开始,依次恢复字体、终端、Shell、现代命令行工具和语言工具链。它关注的是拿到新机器后如何得到一套可验证的工作环境,而不是逐项讲解每个工具的全部功能。
适用范围与验证边界¶
| 项目 | 本文范围 |
|---|---|
| Ubuntu | 24.04 LTS、x86_64;主机配置已审计,脚本已通过静态检查与隔离安装验证 |
| macOS | 当前软件支持范围内的 Apple Silicon;命令按官方资料核对,未做实机验证 |
| Shell | 两个平台统一使用 Zsh,共享长期配置 |
| 不包含 | Docker、Kubernetes、Neovim、dotfiles 管理、bootstrap、Atuin 服务端与 Git 签名 |
最终环境¶
| 层级 | 选择 |
|---|---|
| 终端与字体 | Ghostty;JetBrainsMono Nerd Font → Sarasa Mono SC → Maple Mono NF CN |
| Shell | Zsh、Starship、Atuin、Zoxide |
| 命令行工具 | Yazi、eza、bat、fd、ripgrep、Git |
| 语言工具链 | fnm / Node Current、Go、rustup / Rust stable |
版本策略与全文约定¶
- 本文不记录工具版本号,也不固定 release tag;“最新”统一指执行时所选渠道提供的最新稳定版,不包含 nightly、beta 或
HEAD。 - APT 与 Homebrew 安装前先刷新索引;发行版仓库不够新或缺少所需能力时,改用项目官方稳定发布渠道。
- Zsh 三个纯脚本插件按上游推荐跟随默认分支,这是本文唯一的滚动提交例外。
- 远程安装器必须先完整下载到临时文件,再交给对应 Shell 执行。
- 安装器生成的初始化片段不进入第二个配置文件;本文给出的
.zshrc是唯一长期来源。
0. 系统前置准备¶
这里只安装后续章节会直接使用的系统依赖,不提前引入完整开发工具栈。
Ubuntu 24.04¶
刷新 APT 索引,并一次装好下载、归档、签名校验、JSON 处理、字体缓存和 Zsh 所需的基础包:
(
set -euo pipefail
sudo apt update
sudo apt upgrade -y
sudo apt install -y \
7zip build-essential ca-certificates curl file fontconfig jq pkg-config \
software-properties-common unzip xz-utils zsh
sudo add-apt-repository -y ppa:git-core/ppa
sudo apt update
sudo apt install -y git
)
ppa:git-core/ppa 是 Git 项目列出的 latest stable 渠道,但不是 Ubuntu 官方仓库;继续前应接受这一额外维护方的信任边界。
macOS Apple Silicon¶
本文的 Homebrew 路径按 Apple Silicon 设计。开始前确认系统仍在 Homebrew 当前支持范围内。
xcode-select --install
这条命令会打开系统安装对话框;完成对话框中的安装后,再运行 Homebrew 官方安装器:
(
set -euo pipefail
HOMEBREW_INSTALLER="$(mktemp)"
trap 'rm -f "$HOMEBREW_INSTALLER"' EXIT
curl -fsSL \
https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh \
-o "$HOMEBREW_INSTALLER"
test -s "$HOMEBREW_INSTALLER"
/bin/bash "$HOMEBREW_INSTALLER"
)
Homebrew 在 Apple Silicon 上的默认前缀是 /opt/homebrew。把官方 brew shellenv 初始化放进登录 Shell 读取的 ~/.zprofile:
# ~/.zprofile
if [[ -x /opt/homebrew/bin/brew ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
保存 .zprofile 后需要启动一个新的登录 Shell(最直接的方式是关闭并重新打开终端窗口),Homebrew 的 PATH 设置才会在后续步骤中生效。
重新打开终端后刷新 Homebrew 元数据;后文所有 formula 与 cask 都从这份最新索引安装:
brew update
brew install git
1. 字体安装¶
最终回退顺序只有一处权威定义:
JetBrainsMono Nerd Font渲染拉丁字符和 Nerd Font 图标;Sarasa Mono SC渲染简体中文;Maple Mono NF CN只作为末级兜底。
| 项目 | Ubuntu | macOS |
|---|---|---|
| 安装位置 | ~/.local/share/fonts | Homebrew Cask 管理 |
| JetBrains Mono | 从 Nerd Fonts 当前发布解析资产 | font-jetbrains-mono-nerd-font |
| Maple Mono | 从 Maple Mono 当前发布解析资产 | font-maple-mono-nf-cn |
| Sarasa | 只选择 hinted SarasaMonoSC-TTF-*.7z | font-sarasa-gothic |
| 缓存 | 安装后执行 fc-cache -f | 由系统与 Homebrew 管理 |
Ubuntu 24.04¶
Nerd Fonts 与 Maple Mono 使用当前发布资产。
Sarasa Gothic 通过 GitHub Release API 严格匹配 hinted SarasaMonoSC-TTF-<版本>.7z。
所有资产会先在临时目录下载、解压并确认包含字体;只有三组都通过后才清理旧字体并复制新文件,避免网络失败留下半套更新。
(
set -euo pipefail
FONT_TMP="$(mktemp -d)"
trap 'rm -rf "$FONT_TMP"' EXIT
install -d \
"$HOME/.local/share/fonts/JetBrainsMonoNerd" \
"$HOME/.local/share/fonts/SarasaMonoSC" \
"$HOME/.local/share/fonts/MapleMonoNFCN"
mkdir -p "$FONT_TMP/jetbrains" "$FONT_TMP/sarasa" "$FONT_TMP/maple"
curl -fL \
https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.tar.xz \
-o "$FONT_TMP/JetBrainsMono.tar.xz"
tar -xJf "$FONT_TMP/JetBrainsMono.tar.xz" -C "$FONT_TMP/jetbrains"
SARASA_RELEASE="$(
curl -fsSL \
https://api.github.com/repos/be5invis/Sarasa-Gothic/releases/latest
)"
SARASA_FILTER='[.assets[] | select(.name | test("^SarasaMonoSC-TTF-[0-9]+(\\.[0-9]+)+\\.7z$"))]'
[[ "$(jq -r "$SARASA_FILTER | length" <<< "$SARASA_RELEASE")" == 1 ]]
SARASA_URL="$(
jq -r "$SARASA_FILTER | .[0].browser_download_url // empty" \
<<< "$SARASA_RELEASE"
)"
test -n "$SARASA_URL"
curl -fL "$SARASA_URL" -o "$FONT_TMP/SarasaMonoSC.7z"
7z x -y "$FONT_TMP/SarasaMonoSC.7z" "-o$FONT_TMP/sarasa"
curl -fL \
https://github.com/subframe7536/maple-font/releases/latest/download/MapleMono-NF-CN.zip \
-o "$FONT_TMP/MapleMono-NF-CN.zip"
unzip -jo "$FONT_TMP/MapleMono-NF-CN.zip" '*.ttf' \
-d "$FONT_TMP/maple"
[[ -n "$(find "$FONT_TMP/jetbrains" -type f \
\( -name '*.ttf' -o -name '*.otf' \) -print -quit)" ]]
[[ -n "$(find "$FONT_TMP/sarasa" -type f -name '*.ttf' -print -quit)" ]]
[[ -n "$(find "$FONT_TMP/maple" -type f -name '*.ttf' -print -quit)" ]]
find "$HOME/.local/share/fonts/JetBrainsMonoNerd" -type f \
\( -name '*.ttf' -o -name '*.otf' \) -delete
find "$HOME/.local/share/fonts/SarasaMonoSC" -type f -name '*.ttf' -delete
find "$HOME/.local/share/fonts/MapleMonoNFCN" -type f -name '*.ttf' -delete
find "$FONT_TMP/jetbrains" -type f \
\( -name '*.ttf' -o -name '*.otf' \) \
-exec install -m 0644 {} "$HOME/.local/share/fonts/JetBrainsMonoNerd/" \;
find "$FONT_TMP/sarasa" -type f -name '*.ttf' \
-exec install -m 0644 {} "$HOME/.local/share/fonts/SarasaMonoSC/" \;
find "$FONT_TMP/maple" -type f -name '*.ttf' \
-exec install -m 0644 {} "$HOME/.local/share/fonts/MapleMonoNFCN/" \;
fc-cache -f
)
macOS Apple Silicon¶
Homebrew 提供 JetBrainsMono Nerd Font 和 Sarasa Gothic cask。
另装 Maple Mono NF CN cask:
brew install --cask \
font-jetbrains-mono-nerd-font \
font-sarasa-gothic \
font-maple-mono-nf-cn
2. Ghostty¶
安装与信任边界¶
警告
- macOS:Ghostty 安装文档提供签名、公证的官方二进制;Homebrew cask 重新打包官方
.dmg。- Ubuntu:mkasberg/ghostty-ubuntu 提供社区软件包与脚本,并非 Ghostty 官方 Linux 二进制;信任边界包含维护者及其构建流程。
Ubuntu 24.04 使用社区项目当前推荐的 PPA,以便后续随 APT 获得该系统可用的最新兼容构建:
sudo add-apt-repository -y ppa:mkasberg/ghostty-ubuntu
sudo apt update
sudo apt install -y ghostty
macOS 通过 Homebrew 安装 Ghostty cask:
brew install --cask ghostty
Homebrew cask 安装的是 /Applications/Ghostty.app,不会把其中的可执行文件链接到 PATH。启动 Ghostty 后,macOS 的检查命令应使用应用包内的完整路径。
两个平台共用一份配置¶
两个平台共用 ~/.config/ghostty/config.ghostty;macOS 还会稍后加载 ~/Library/Application Support/com.mitchellh.ghostty/config.ghostty,因此不要在两处保留冲突副本。
theme = Flexoki Dark
font-family = JetBrainsMono Nerd Font
font-family = Sarasa Mono SC
font-family = Maple Mono NF CN
font-size = 13
background-opacity = 1
cursor-style = block
cursor-style-blink = false
copy-on-select = true
mouse-hide-while-typing = true
shell-integration = detect
scrollback-limit = 50000000
按配置参考,重复的 font-family 依出现顺序形成第 1 节的回退链;首项也提供 Starship 和 eza --icons 图标。
共用配置不设置 command:Ghostty 跟随下一节配置的登录 Shell,并由 shell-integration = detect 自动检测。覆盖时须按平台分别设置:
- Ubuntu 可选:
command = /usr/bin/zsh -l - macOS 可选:
command = /bin/zsh -l
验证字体解析与配置¶
Ubuntu 同时检查 Fontconfig 的 family 匹配和 Ghostty 的实际选字结果:
fc-match 'JetBrainsMono Nerd Font'
fc-match 'Sarasa Mono SC'
fc-match 'Maple Mono NF CN'
ghostty +validate-config
ghostty +show-config
ghostty +show-face --string='A中文'
审计主机的 ghostty +show-face 结果如下:
| 字符 | 预期字体 |
|---|---|
A | JetBrainsMono Nerd Font;输出可能显示 family alias JetBrainsMono NF |
中文 | Sarasa Mono SC |
macOS 不使用 fc-* 命令。先启动 Ghostty,再在新窗口中用应用包内的检查接口验证配置与 CoreText 选字结果:
GHOSTTY_BIN='/Applications/Ghostty.app/Contents/MacOS/ghostty'
"$GHOSTTY_BIN" +validate-config
"$GHOSTTY_BIN" +show-config
"$GHOSTTY_BIN" +show-face --string='A中文'
unset GHOSTTY_BIN
3. Fontconfig:为什么英文被 Noto CJK 接管¶
这一节只适用于使用 Fontconfig 的 Linux。macOS 上的 Ghostty 通过 CoreText 选字,不应套用这里的配置文件和诊断命令。
从现象追到改写后的 family 列表¶
| 阶段 | 观察 |
|---|---|
| 现象 | Ghostty 请求 JetBrainsMono Nerd Font,英文却由 Noto Sans Mono CJK SC 渲染 |
| 证据 | Fontconfig 的最终 family 列表已经把 Noto 提到 JetBrainsMono 前面 |
| 根因 | ~/.config/fontconfig/fonts.conf 中无条件命中的 prepend 与 binding="strong" 改写了应用请求 |
| 修复方向 | 删除无条件强绑定,让 Ghostty 自己声明字体回退顺序 |
下面的 XML 是根据原始故障重建的历史片段,不是当前磁盘上 fonts.conf.bak 的原样内容:
<match target="pattern">
<edit name="family" mode="prepend" binding="strong">
<string>Noto Sans Mono CJK SC</string>
</edit>
</match>
<match> 没有 <test>,所以无条件匹配;prepend 把 Noto 放到请求列表前面,strong binding 又使它成为强绑定候选。Noto 自带拉丁字形,因而在英文阶段便已命中。
相关语义见 Fontconfig 官方用户文档。
诊断时只以 FC_DEBUG 显示的实际加载文件为准;.bak 备份不等于活动规则,也不能拿来证明当前匹配链。
完整诊断链¶
遇到类似问题时,按从应用配置到最终规则文件的顺序执行完整诊断链:
ghostty +show-config
GHOSTTY_LOG=debug ghostty +show-config
ghostty +list-fonts
ghostty +show-face --string='A中文'
fc-match 'JetBrainsMono Nerd Font'
fc-match -s 'JetBrainsMono Nerd Font,Sarasa Mono SC'
fc-pattern -c 'JetBrainsMono Nerd Font,Sarasa Mono SC'
FC_DEBUG=1024 fc-match 'JetBrainsMono Nerd Font'
FC_DEBUG=1024 的输出非常嘈杂,实际排查时最好重定向到文件后再搜索路径和 family:
FC_DEBUG=1024 fc-match 'JetBrainsMono Nerd Font' \
> /tmp/fontconfig-debug.log 2>&1
grep -En 'JetBrainsMono|Noto|fonts\.conf' /tmp/fontconfig-debug.log
| 层级 | 命令 | 观察对象 |
|---|---|---|
| Ghostty 配置 | ghostty +show-config | 最终读取的配置与字体顺序 |
| Ghostty 字体 | +list-fonts、+show-face、GHOSTTY_LOG=debug | 可见字体、实际字形与运行时解析 |
| Fontconfig 匹配 | fc-match、fc-match -s | 首选字体与候选顺序 |
| Fontconfig 规则 | FC_DEBUG、grep | 哪条规则改写了 family 列表 |
fc-pattern -c 补充显示经过配置替换后的完整 pattern,用来确认应用请求在哪一步被改写。
修复:把应用回退顺序还给 Ghostty¶
首选修复是删除或停用那条无条件 Fontconfig 规则,让上一节中按顺序重复声明的 font-family 自己控制回退:JetBrainsMono Nerd Font 负责拉丁字形和图标,Sarasa Mono SC 负责主要中文回退,Maple Mono NF CN 留在链尾。既然 Ghostty 已明确给出 family 列表,就不需要再用全局强规则替它重排。
如果还希望给只请求 generic monospace 的其他终端程序提供中文回退,可以把规则明确限定到 monospace,并以 weak append 追加,而不是无条件抢到所有 family 前面:
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
<match target="pattern">
<test name="family" qual="any">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="weak">
<string>Sarasa Mono SC</string>
<string>Maple Mono NF CN</string>
</edit>
</match>
</fontconfig>
修改规则或清理重复字体后,重建缓存,再回到 Ghostty 验证实际选字,而不是仅凭 fc-cache 成功就宣告修复:
fc-cache -f -v
ghostty +show-face --string='A中文'
预期结果仍是 A 命中 JetBrainsMono Nerd Font,中文 命中 Sarasa Mono SC。若结果不同,就沿上面的诊断链继续查看 substituted pattern 和实际加载的规则文件。
最后还有一个容易制造“明明禁用了,为什么仍被扫描”的目录陷阱:把字体目录改名为 .disabled 并不会让它退出 Fontconfig 的递归扫描。只要它仍位于任一已配置字体目录之下,目录名带点或带 disabled 都不是排除标记。
应把重复字体移到所有已配置字体目录之外,或者删除重复文件,然后再次运行 fc-cache -f -v。
4. Zsh 与插件¶
安装 Zsh 并切换登录 Shell¶
第 0 节已从 Ubuntu 官方仓库安装 Zsh。这里直接切换账户的登录 Shell:
chsh -s /usr/bin/zsh
chsh 修改的是账户在未来登录会话中使用的 Shell,不会替换当前进程。执行后应完整注销桌面会话并重新登录,再重开 Ghostty;仅在当前窗口运行 zsh 不能验证登录 Shell 已经变更。
macOS 已自带 /bin/zsh,Apple 的 Terminal 设置文档也把 Zsh 列为默认登录 Shell。正常情况下无需再安装一份 Homebrew Zsh;先确认:
printf '%s\n' "$SHELL"
预期输出 /bin/zsh。只有从旧系统迁移而来的账户仍使用其他登录 Shell 时,才运行 chsh -s /bin/zsh,然后注销并重新登录。
第 2 节的 Ghostty 配置没有写死 command,因此它会在重开后跟随各平台的账户登录 Shell。
把三个插件安装到共用路径¶
两个平台都使用 ~/.zsh/plugins,这样后面的 .zshrc 无需平台分支。
zsh-completions 必须在 compinit 前加入 fpath。
zsh-autosuggestions 和 zsh-syntax-highlighting 则直接加载各自的脚本。
下面的函数会区分三种状态:已有 Git clone 时只接受 fast-forward 更新;目标路径存在但不是 Git clone 时停止,避免覆盖本地文件;路径不存在时才做浅克隆。
(
set -euo pipefail
mkdir -p "$HOME/.zsh/plugins"
install_or_update_zsh_plugin() {
local repository="$1"
local destination="$2"
if [[ -d "$destination/.git" ]]; then
git -C "$destination" pull --ff-only
elif [[ -e "$destination" ]]; then
printf 'Refusing to overwrite non-Git path: %s\n' "$destination" >&2
return 1
else
git clone --depth=1 "$repository" "$destination"
fi
}
install_or_update_zsh_plugin \
https://github.com/zsh-users/zsh-completions.git \
"$HOME/.zsh/plugins/zsh-completions"
install_or_update_zsh_plugin \
https://github.com/zsh-users/zsh-autosuggestions.git \
"$HOME/.zsh/plugins/zsh-autosuggestions"
install_or_update_zsh_plugin \
https://github.com/zsh-users/zsh-syntax-highlighting.git \
"$HOME/.zsh/plugins/zsh-syntax-highlighting"
unset -f install_or_update_zsh_plugin
)
这些 clone 跟随各项目的默认分支,pull --ff-only 也会更新到该分支的最新提交;这更偏向及时更新,而不是逐字节复现。若需要严格可复现,应另外记录并 checkout 已审核的 commit,而不是把这里的滚动分支误当成版本锁定。
一次写入完整的 ~/.zshrc¶
下面是两个平台共用的完整文件,后续安装工具时不再向它零散追加初始化片段。macOS 的 Homebrew 初始化只留在第 0 节已经建立的 ~/.zprofile,不要再复制到这里。
# PATH
typeset -U path PATH
path=("$HOME/.local/bin" "$HOME/.atuin/bin" $path)
# Go
export GOPATH="${GOPATH:-$HOME/go}"
[[ -d "$HOME/.local/go/bin" ]] && path=("$HOME/.local/go/bin" $path)
path=("$GOPATH/bin" $path)
# Rust
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
export PATH
# 历史
HISTFILE="$HOME/.zsh_history"
HISTSIZE=100000
SAVEHIST=100000
setopt EXTENDED_HISTORY
setopt HIST_IGNORE_ALL_DUPS
setopt HIST_IGNORE_SPACE
setopt HIST_EXPIRE_DUPS_FIRST
setopt HIST_FIND_NO_DUPS
setopt HIST_SAVE_NO_DUPS
setopt SHARE_HISTORY
# SHARE_HISTORY 已负责终端间的增量共享,不再同时设置 INC_APPEND_HISTORY。
# 补全:先扩展 fpath,再初始化补全系统
fpath=(
"$HOME/.local/share/zsh/site-functions"
"$HOME/.zsh/plugins/zsh-completions/src"
$fpath
)
[[ -d /opt/homebrew/share/zsh/site-functions ]] &&
fpath=(/opt/homebrew/share/zsh/site-functions $fpath)
autoload -Uz compinit
compinit
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list \
'm:{a-zA-Z}={A-Za-z}' \
'r:|[._-]=* r:|=*'
setopt AUTO_CD
# 键位
bindkey -e
# fnm
FNM_PATH="$HOME/.local/share/fnm"
if [[ -d "$FNM_PATH" ]]; then
export PATH="$FNM_PATH:$PATH"
fi
if command -v fnm >/dev/null 2>&1 &&
[[ -z "${_FNM_ZSHRC_INITIALIZED:-}" ]]; then
if eval "$(fnm env --use-on-cd --shell zsh)"; then
typeset -g _FNM_ZSHRC_INITIALIZED=1
fi
fi
# 目录跳转
if command -v zoxide >/dev/null 2>&1; then
eval "$(zoxide init zsh)"
fi
# Yazi:退出时把当前目录带回 Shell
function y() {
local tmp cwd yazi_status
tmp="$(mktemp -t 'yazi-cwd.XXXXXX')" || return 1
command yazi "$@" --cwd-file="$tmp"
yazi_status=$?
if [[ -f "$tmp" ]]; then
IFS= read -r -d '' cwd < "$tmp"
[[ -n "$cwd" && "$cwd" != "$PWD" && -d "$cwd" ]] &&
builtin cd -- "$cwd"
fi
command rm -f -- "$tmp"
return "$yazi_status"
}
# 让 y 复用 yazi 的补全
compdef _yazi y
# 别名
if command -v eza >/dev/null 2>&1; then
alias ls='eza --icons --group-directories-first'
alias ll='eza -la --icons --git --group-directories-first'
alias tree='eza --tree --icons'
fi
if command -v bat >/dev/null 2>&1; then
alias cat='bat'
fi
alias g='git'
# Autosuggestions 要在 Atuin 之前加载
[[ -f "$HOME/.zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh" ]] &&
source "$HOME/.zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh"
# 提示符与历史搜索
if command -v starship >/dev/null 2>&1 &&
[[ -z "${_STARSHIP_ZSHRC_INITIALIZED:-}" ]]; then
if STARSHIP_INIT="$(starship init zsh)" &&
[[ -n "$STARSHIP_INIT" ]] &&
eval "$STARSHIP_INIT"; then
typeset -g _STARSHIP_ZSHRC_INITIALIZED=1
else
print -u2 -- "warning: Starship initialization failed"
fi
unset STARSHIP_INIT
fi
if command -v atuin >/dev/null 2>&1 &&
[[ -z "${_ATUIN_ZSHRC_INITIALIZED:-}" ]]; then
if ATUIN_INIT="$(atuin init zsh)" &&
[[ -n "$ATUIN_INIT" ]] &&
eval "$ATUIN_INIT"; then
typeset -g _ATUIN_ZSHRC_INITIALIZED=1
else
print -u2 -- "warning: Atuin initialization failed"
fi
unset ATUIN_INIT
fi
# 可选的本机敏感信息;不要把文件内容提交到 dotfiles
[[ -f "$HOME/.secrets" ]] && source "$HOME/.secrets"
# Syntax highlighting 必须最后加载
[[ -f "$HOME/.zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" ]] &&
source "$HOME/.zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"
typeset -U path PATH保证重复 source 后 PATH 仍然唯一。.secrets只保存本机变量,权限设为600,且永不提交。- Autosuggestions 在 Atuin 前加载,Syntax Highlighting 保持最后加载。
- Starship、fnm 与 Atuin 的非导出哨兵让重复 source 幂等,同时不影响子 Zsh 独立初始化。
touch "$HOME/.secrets"
chmod 600 "$HOME/.secrets"
验证语法、插件与键位¶
先验证完整文件的静态语法:
zsh -n "$HOME/.zshrc"
语法检查成功后、进入第 5 节前,用新 Zsh 替换当前进程。这样刚写入的 ~/.local/bin、初始化器和条件 PATH 会在后续安装步骤中生效:
exec zsh
在新 Shell 中验证一次真实的交互式启动:
zsh -ic 'exit'
再检查补全目录、两个插件函数、Zoxide 与 Yazi 的函数,以及 Ctrl-R 的最终绑定:
zsh -ic '
(( ${fpath[(Ie)$HOME/.zsh/plugins/zsh-completions/src]} )) &&
echo "zsh-completions: loaded"
(( ${+functions[_zsh_autosuggest_start]} )) &&
echo "zsh-autosuggestions: loaded"
(( ${+functions[_zsh_highlight]} )) &&
echo "zsh-syntax-highlighting: loaded"
whence -w z
whence -w y
bindkey "^R"
'
安装三个插件后,前三项应打印 loaded;完成后续章节后,z 与 y 都应是函数,Ctrl-R 应绑定到 Atuin。
重复 source、子 Shell、hook 数量与哨兵的完整断言集中在第 14 章;完成 Atuin 与 fnm 安装后,可运行其中的可选完整验收检查。
5. Starship¶
安装¶
Ubuntu 24.04 使用 Starship 官方安装器,并明确把二进制写入第 4 节已经加入 PATH 的 ~/.local/bin:
(
set -euo pipefail
mkdir -p "$HOME/.local/bin"
STARSHIP_INSTALLER="$(mktemp)"
trap 'rm -f "$STARSHIP_INSTALLER"' EXIT
curl -fsSL https://starship.rs/install.sh -o "$STARSHIP_INSTALLER"
test -s "$STARSHIP_INSTALLER"
/bin/sh "$STARSHIP_INSTALLER" \
--bin-dir "$HOME/.local/bin" \
--yes
)
脚本会先把远程内容下载到临时文件,确认非空后交给 /bin/sh,并写入或更新指定目录的 starship;结束时清理临时文件。需要固定供应链输入时,应先审阅并固定脚本内容。
macOS 通过 Homebrew 的 Starship formula 安装:
brew install starship
生成 Nerd Font preset¶
第 1 节已经安装 Nerd Font,因此直接让 Starship 生成官方的 Nerd Font Symbols preset。如果配置文件已经存在,先备份其中的本机定制:
(
set -euo pipefail
mkdir -p "$HOME/.config"
STARSHIP_CONFIG="$HOME/.config/starship.toml"
if [[ -f "$STARSHIP_CONFIG" ]]; then
STARSHIP_BACKUP="${STARSHIP_CONFIG}.backup.$(date -u +%Y%m%dT%H%M%SZ)"
[[ ! -e "$STARSHIP_BACKUP" ]]
cp -p "$STARSHIP_CONFIG" "$STARSHIP_BACKUP"
printf 'Starship backup: %s\n' "$STARSHIP_BACKUP"
fi
starship preset nerd-font-symbols -o "$STARSHIP_CONFIG"
)
-o 会替换目标文件,因此脚本先生成带 UTC 时间戳的备份。preset 会随 Starship 更新,不在文章中复制整张 symbols 表。
验证¶
starship --version
starship print-config >/dev/null
第二条命令成功返回只能证明合并后的配置可解析;还应新开一个 Zsh,目视确认提示符图标能由 Nerd Font 正常显示。
6. Atuin¶
安装及 Shell 配置边界¶
Ubuntu 直接使用 Atuin latest stable release 安装器,跳过会自动改写多个 Shell 文件和 AI 工具 hooks 的 setup 包装脚本:
(
set -euo pipefail
ATUIN_INSTALLER="$(mktemp)"
trap 'rm -f "$ATUIN_INSTALLER"' EXIT
curl --proto '=https' --tlsv1.2 -fsSL \
https://github.com/atuinsh/atuin/releases/latest/download/atuin-installer.sh \
-o "$ATUIN_INSTALLER"
test -s "$ATUIN_INSTALLER"
ATUIN_NO_MODIFY_PATH=1 /bin/sh "$ATUIN_INSTALLER"
)
安装器只负责把已校验的二进制放进 ~/.atuin/bin;第 4 节统一管理 PATH,并且只执行一次 atuin init zsh。
macOS 使用 Homebrew:
brew install atuin
最小配置¶
创建 ~/.config/atuin/config.toml。TOML 的顶层键必须放在 [sync] 和 [daemon] 之前,否则它们会变成表内字段;本文只公开这组最小配置:
search_mode = "daemon-fuzzy"
enter_accept = false
[sync]
records = true
[daemon]
enabled = true
autostart = true
警告
- 未设置
sync_address时使用 Atuin 默认同步服务;自建服务应先审核端点,再显式加入该键。records = true会把 Shell 历史纳入同步数据边界;先检查历史并审阅隐私、保留与备份策略,secrets filter 只能辅助,不能代替审查。daemon-fuzzy依赖已启用且自动启动的 daemon;enter_accept = false避免选中结果后立即执行。- 注册与登录二选一;密码和加密密钥只交给 Atuin 交互式读取,不写入 TOML、脚本、
.zshrc或 Git。
注册新账户¶
输入用户名和邮箱:
read 'ATUIN_USERNAME?Atuin username: '
read 'ATUIN_EMAIL?Atuin email: '
# New account: Atuin prompts for the password interactively.
atuin register -u "$ATUIN_USERNAME" -e "$ATUIN_EMAIL" &&
atuin sync
登录已有账户¶
输入用户名:
read 'ATUIN_USERNAME?Atuin username: '
# Existing account: Atuin prompts for the password and encryption key.
atuin login -u "$ATUIN_USERNAME" &&
atuin sync
快速验证¶
atuin --version
atuin config get search_mode --resolved
atuin config get enter_accept --resolved
atuin config get sync --resolved
atuin config get daemon --resolved
atuin daemon status
解析后的结果应包含 daemon-fuzzy、false、启用的 sync records,以及 enabled = true、autostart = true 的 daemon 配置。若最后一条报告 daemon 已停止,应运行 atuin daemon start 并调查、修复它输出的错误,再重新检查状态;不要用重定向或 || true 隐藏失败。
7. Zoxide¶
Zoxide 与 Autojump、fasd 同属目录跳转工具,但用 frecency 数据让 z 比反复输入长 cd 路径更省事;第 4 节的完整 .zshrc 已按守卫执行 zoxide init zsh,这里不再追加初始化片段。
Ubuntu 执行项目提供的远程安装脚本:
(
set -euo pipefail
ZOXIDE_INSTALLER="$(mktemp)"
trap 'rm -f "$ZOXIDE_INSTALLER"' EXIT
curl -fsSL \
https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh \
-o "$ZOXIDE_INSTALLER"
test -s "$ZOXIDE_INSTALLER"
/bin/sh "$ZOXIDE_INSTALLER"
)
安装脚本会解析上游 latest stable release;需要供应链人工审阅时,应在 /bin/sh 前暂停检查。macOS 使用 Homebrew:
brew install zoxide
只检查二进制与新 Zsh 中的初始化结果:
command -v zoxide
zsh -ic 'whence -w z'
8. Rust¶
Ubuntu 与 macOS 都使用 Rust 官方 rustup 安装器,并禁止它修改 Shell 文件;第 4 节的 .zshrc 已经负责读取 Cargo 环境文件:
(
set -euo pipefail
RUSTUP_INSTALLER="$(mktemp)"
trap 'rm -f "$RUSTUP_INSTALLER"' EXIT
curl --proto '=https' --tlsv1.2 -fsSL \
https://sh.rustup.rs \
-o "$RUSTUP_INSTALLER"
test -s "$RUSTUP_INSTALLER"
/bin/sh "$RUSTUP_INSTALLER" \
-y \
--profile default \
--default-toolchain stable \
--no-modify-path
)
让当前 Shell 获得 Cargo 环境,再更新到执行时最新的 stable 工具链:
source "$HOME/.cargo/env"
rustup update stable
rustup 的 default profile包含 rustfmt 与 Clippy;--no-modify-path 防止安装器追加另一套 PATH。
9. eza、bat、fd 与 ripgrep¶
Ubuntu 24.04¶
Ubuntu 固定仓库中的版本可能落后于上游。上一节已经准备好 Rust stable,因此四个工具都按项目官方支持的 Cargo 路径安装最新 crates.io 稳定发布:
cargo install --locked eza
cargo install --locked bat
cargo install --locked fd-find
cargo install --locked ripgrep
安装依据:
--locked 使用发布附带的锁文件,不固定工具版本;四个 crate 分别提供 eza、bat、fd 与 rg。
macOS Apple Silicon¶
四个工具都通过已刷新索引的 Homebrew 安装:
brew install eza bat fd ripgrep
验证¶
eza --version
bat --version
fd --version
rg --version
在 Ubuntu 上,这四条命令也验证了第 4 节对 ~/.cargo/bin 的 PATH 初始化。
10. Yazi¶
Yazi 官方安装清单把 file 作为必需依赖,并用以下可选工具补全预览、搜索与跳转能力:
| 能力 | 依赖 |
|---|---|
| 视频、压缩包、JSON、PDF | ffmpeg、7z、jq、pdftoppm |
| 文件名、内容与目录跳转 | fd、rg、fzf、zoxide |
| SVG 与更多图片格式 | resvg、magick |
| Linux 剪贴板 | Wayland 的 wl-copy;X11 的 xclip |
| 图标与图片预览 | 第 1 节的 Nerd Font;Ghostty 内建图片协议 |
Ubuntu 24.04:官方稳定发布与完整依赖¶
先安装适合从系统仓库维护的运行依赖。libfuse2t64 用于运行后面安装的 ImageMagick 官方 AppImage:
sudo apt update
sudo apt install -y \
7zip ffmpeg file jq libfuse2t64 poppler-utils wl-clipboard xclip
Yazi 官方稳定归档同时包含 yazi、ya 和 Zsh completion。Ubuntu 仓库里的 fzf 与 ImageMagick 可能不满足 Yazi 当前能力要求,resvg 也不在 Ubuntu 24.04 仓库中;下面统一从四个项目的 latest stable API 严格匹配一个 x86_64 资产,并用 API 返回的 SHA-256 校验下载内容。
Ubuntu:展开完整 Yazi 与预览依赖安装脚本
(
set -euo pipefail
[[ "$(uname -m)" == "x86_64" ]] || {
printf 'This Ubuntu guide expects x86_64; got %s\n' "$(uname -m)" >&2
exit 1
}
RELEASE_TMP="$(mktemp -d)"
trap 'rm -rf "$RELEASE_TMP"' EXIT
download_latest_asset() {
local repository="$1" pattern="$2" output="$3"
local release asset url digest
release="$(
curl -fsSL --retry 3 \
"https://api.github.com/repos/${repository}/releases/latest"
)"
asset="$(
jq -cer --arg pattern "$pattern" '
[.assets[] | select(.name | test($pattern))]
| if length == 1 then .[0]
else error("expected one matching asset, got \(length)")
end
' <<< "$release"
)"
url="$(jq -er '.browser_download_url' <<< "$asset")"
digest="$(
jq -er \
'.digest | select((type == "string") and startswith("sha256:"))' \
<<< "$asset"
)"
curl -fL --retry 3 "$url" -o "$output"
printf '%s %s\n' "${digest#sha256:}" "$output" | sha256sum -c -
}
download_latest_asset sxyazi/yazi \
'^yazi-x86_64-unknown-linux-gnu\.zip$' \
"$RELEASE_TMP/yazi.zip"
download_latest_asset junegunn/fzf \
'^fzf-[0-9]+(\.[0-9]+)*-linux_amd64\.tar\.gz$' \
"$RELEASE_TMP/fzf.tar.gz"
download_latest_asset linebender/resvg \
'^resvg-linux-x86_64\.tar\.gz$' \
"$RELEASE_TMP/resvg.tar.gz"
download_latest_asset ImageMagick/ImageMagick \
'^ImageMagick-[0-9][0-9A-Za-z.-]*-gcc-x86_64\.AppImage$' \
"$RELEASE_TMP/imagemagick.AppImage"
unzip -q "$RELEASE_TMP/yazi.zip" -d "$RELEASE_TMP"
tar -xzf "$RELEASE_TMP/fzf.tar.gz" -C "$RELEASE_TMP"
tar -xzf "$RELEASE_TMP/resvg.tar.gz" -C "$RELEASE_TMP"
YAZI_DIR="$RELEASE_TMP/yazi-x86_64-unknown-linux-gnu"
install -d "$HOME/.local/bin" "$HOME/.local/share/zsh/site-functions"
install -m 0755 "$YAZI_DIR/yazi" "$YAZI_DIR/ya" "$HOME/.local/bin/"
install -m 0755 "$RELEASE_TMP/fzf" "$HOME/.local/bin/fzf"
install -m 0755 "$RELEASE_TMP/resvg" "$HOME/.local/bin/resvg"
install -m 0755 "$RELEASE_TMP/imagemagick.AppImage" "$HOME/.local/bin/magick"
install -m 0644 \
"$YAZI_DIR/completions/_yazi" \
"$YAZI_DIR/completions/_ya" \
"$HOME/.local/share/zsh/site-functions/"
unset -f download_latest_asset
"$HOME/.local/bin/yazi" --version
"$HOME/.local/bin/ya" --version
"$HOME/.local/bin/fzf" --version
"$HOME/.local/bin/resvg" --version
"$HOME/.local/bin/magick" -version
)
若共享出口触发 GitHub API 的 403 rate limit,等待配额恢复后重跑。四个资产全部下载并校验成功后才会覆盖现有文件。
ImageMagick 官方 AppImage 默认采用开放策略;预览不可信文件前,应按其安全策略文档建立适合本机的 ~/.config/ImageMagick/policy.xml。
这里只安装 fzf 二进制,不加载它的 Shell key bindings;Ctrl-R 继续由 Atuin 独占。
macOS Apple Silicon¶
Homebrew 按 Yazi 官方清单安装完整依赖。ffmpeg-full 与 imagemagick-full 是 keg-only,强制 link 前先确认不会覆盖需要保留的同名命令:
brew install \
yazi ffmpeg-full sevenzip jq poppler fd ripgrep fzf zoxide resvg \
imagemagick-full
brew link ffmpeg-full imagemagick-full -f --overwrite
第 1 节已有完整 Nerd Font,因此不再重复安装 symbols-only 字体。Ghostty 受 Yazi 内建图片协议支持,不需要 Überzug++;同时不要手动覆盖 $TERM、$TERM_PROGRAM 或 $XDG_SESSION_TYPE。
Shell 集成与验证¶
第 4 节已经加入官方推荐的 y wrapper:用 y 进入 Yazi,按 q 退出后把目录带回当前 Shell,按 Q 则保留原目录。安装后重载 Zsh,再检查二进制、completion、依赖与 Ghostty 图片适配器:
exec zsh
command -v yazi ya file ffmpeg ffprobe jq pdftoppm fd rg fzf zoxide resvg magick
command -v 7zz >/dev/null || command -v 7z
[[ "$(uname -s)" != Linux ]] || command -v wl-copy xclip
whence -w y _yazi _ya
(( ${+functions[_yazi]} && ${+functions[_ya]} ))
[[ "${_comps[y]:-}" == _yazi ]]
yazi --version
ya --version
yazi --debug | grep -E 'Adapter\.matches[[:space:]]*:[[:space:]]*Kgp'
这些命令应在 Ghostty 窗口中运行。Yazi 与 Ya 来自同一归档;显式列出的预览依赖必须全部存在,图片 Adapter 应为 Kgp。
ueberzugpp、chafa 和未选用的剪贴板后端可以显示缺失,不影响 Ghostty 的内建图片预览。
11. fnm 与 Node.js¶
安装 fnm¶
Ubuntu 使用 fnm 官方安装器,但禁止它再修改 Shell 配置:
(
set -euo pipefail
FNM_INSTALLER="$(mktemp)"
trap 'rm -f "$FNM_INSTALLER"' EXIT
curl -fsSL https://fnm.vercel.app/install -o "$FNM_INSTALLER"
test -s "$FNM_INSTALLER"
/bin/bash "$FNM_INSTALLER" --skip-shell
)
--skip-shell 会阻止安装器向 Shell 启动文件追加初始化代码。第 4 节的完整 .zshrc 已经统一设置 fnm 的 PATH、初始化和目录切换 hook,因此不能再保留另一份由安装器生成的重复初始化。
macOS 使用 Homebrew:
brew install fnm
安装最新 Node Current¶
安装 fnm 后,先让当前终端进入读取了完整配置的新 Zsh:
exec zsh
新 Zsh 启动后,执行本文选择的 Current 路径,并把这次解析到的版本设为默认值:
fnm install --latest --use
fnm default "$(fnm current)"
这会在执行时解析最新 Current,并把本次结果设为默认值;以后恢复或升级环境时重新执行即可。
12. Go¶
Ubuntu 24.04:动态解析并校验官方归档¶
Ubuntu 按 版本端点动态解析版本,并从 发布 JSON取得归档的 SHA-256,不在手册中硬编码版本或摘要。脚本只支持 x86_64;其他架构会让子 Shell 失败,不退出外层 Zsh。
Ubuntu:展开完整安全安装/升级脚本
(
set -euo pipefail
mkdir -p "$HOME/.local"
GO_STAGE_ROOT="$(mktemp -d "$HOME/.local/.go-stage.XXXXXXXX")"
GO_ROLLBACK_ROOT=""
GO_ROLLBACK=""
GO_KEEP_STAGE=0
cleanup_go_staging() {
local exit_status="$?"
if [[ "$GO_KEEP_STAGE" == 1 ]]; then
printf 'Preserved Go staging directory: %s\n' "$GO_STAGE_ROOT" >&2
elif ! rm -rf "$GO_STAGE_ROOT"; then
printf 'Failed to remove Go staging directory: %s\n' \
"$GO_STAGE_ROOT" >&2
if (( exit_status == 0 )); then
exit_status=1
fi
fi
if [[ -n "$GO_ROLLBACK_ROOT" ]]; then
rmdir "$GO_ROLLBACK_ROOT" 2>/dev/null || true
fi
trap - EXIT
exit "$exit_status"
}
trap cleanup_go_staging EXIT
case "$(uname -m)" in
x86_64) GO_ARCH="amd64" ;;
*)
echo "This Ubuntu guide expects x86_64; got $(uname -m)" >&2
exit 1
;;
esac
GO_VERSION="$(curl -fsSL 'https://go.dev/VERSION?m=text' | sed -n '1p')"
GO_ARCHIVE="${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
GO_SHA256="$(
curl -fsSL 'https://go.dev/dl/?mode=json' |
jq -r --arg file "$GO_ARCHIVE" \
'.[] | .files[] | select(.filename == $file) | .sha256' |
sed -n '1p'
)"
if [[ -z "$GO_SHA256" || "$GO_SHA256" == "null" ]]; then
echo "No official SHA-256 found for $GO_ARCHIVE" >&2
exit 1
fi
GO_ARCHIVE_PATH="$GO_STAGE_ROOT/$GO_ARCHIVE"
curl -fL "https://go.dev/dl/${GO_ARCHIVE}" -o "$GO_ARCHIVE_PATH"
(
cd "$GO_STAGE_ROOT"
printf '%s %s\n' "$GO_SHA256" "$GO_ARCHIVE" | sha256sum -c -
)
tar -C "$GO_STAGE_ROOT" -xzf "$GO_ARCHIVE_PATH"
STAGED_GO="$GO_STAGE_ROOT/go"
[[ -x "$STAGED_GO/bin/go" ]] || {
echo "Staged Go binary is missing or not executable" >&2
exit 1
}
"$STAGED_GO/bin/go" version
LIVE_GO="$HOME/.local/go"
if [[ -e "$LIVE_GO" || -L "$LIVE_GO" ]]; then
[[ -d "$LIVE_GO" && ! -L "$LIVE_GO" ]] || {
echo "Refusing to replace non-directory path: $LIVE_GO" >&2
exit 1
}
GO_ROLLBACK_ROOT="$(mktemp -d "$HOME/.local/.go-rollback.XXXXXXXX")"
GO_ROLLBACK="$GO_ROLLBACK_ROOT/go"
mv "$LIVE_GO" "$GO_ROLLBACK"
fi
if mv "$STAGED_GO" "$LIVE_GO"; then
:
else
activation_status="$?"
if [[ -n "$GO_ROLLBACK" && -d "$GO_ROLLBACK" && ! -e "$LIVE_GO" ]] &&
mv "$GO_ROLLBACK" "$LIVE_GO"; then
printf 'Go activation failed; restored previous installation.\n' >&2
GO_ROLLBACK=""
elif [[ -n "$GO_ROLLBACK" && -d "$GO_ROLLBACK" ]]; then
printf 'Go activation and automatic restore failed; old installation remains at %s\n' \
"$GO_ROLLBACK" >&2
fi
exit "$activation_status"
fi
if "$LIVE_GO/bin/go" version; then
if [[ -n "$GO_ROLLBACK" ]]; then
rm -rf "$GO_ROLLBACK"
rmdir "$GO_ROLLBACK_ROOT"
GO_ROLLBACK=""
GO_ROLLBACK_ROOT=""
fi
else
validation_status="$?"
FAILED_GO="$GO_STAGE_ROOT/failed-go"
if mv "$LIVE_GO" "$FAILED_GO"; then
if [[ -n "$GO_ROLLBACK" && -d "$GO_ROLLBACK" ]]; then
if [[ ! -e "$LIVE_GO" && ! -L "$LIVE_GO" ]] &&
mv "$GO_ROLLBACK" "$LIVE_GO"; then
printf 'Activated Go failed validation; restored previous installation.\n' >&2
GO_ROLLBACK=""
else
GO_KEEP_STAGE=1
printf 'Activated Go failed validation and automatic restore failed.\n' >&2
printf 'Failed new tree: %s\n' "$FAILED_GO" >&2
printf 'Old rollback tree: %s\n' "$GO_ROLLBACK" >&2
if [[ -e "$LIVE_GO" || -L "$LIVE_GO" ]]; then
printf 'Unexpected live path also remains: %s\n' "$LIVE_GO" >&2
else
printf 'Live path is absent: %s\n' "$LIVE_GO" >&2
fi
fi
else
printf 'Activated Go failed validation; no previous installation exists.\n' >&2
printf 'Live path was left absent: %s\n' "$LIVE_GO" >&2
fi
else
GO_KEEP_STAGE=1
printf 'Activated Go failed validation and could not be isolated.\n' >&2
if [[ -e "$LIVE_GO" || -L "$LIVE_GO" ]]; then
printf 'Failed live tree: %s\n' "$LIVE_GO" >&2
fi
if [[ -e "$FAILED_GO" || -L "$FAILED_GO" ]]; then
printf 'Failed staged tree: %s\n' "$FAILED_GO" >&2
fi
if [[ -n "$GO_ROLLBACK" && -d "$GO_ROLLBACK" ]]; then
printf 'Old rollback tree: %s\n' "$GO_ROLLBACK" >&2
fi
printf 'Diagnostic staging root: %s\n' "$GO_STAGE_ROOT" >&2
fi
exit "$validation_status"
fi
)
第一次 exec zsh 时,~/.local/go/bin 可能还不存在,条件 PATH 尚未加入它。激活成功后再次替换当前 Shell,让 Go 路径生效:
exec zsh
macOS Apple Silicon¶
macOS 使用 Homebrew 的 Go formula:
brew install go
- 不设置
GOROOT:手动安装与 Homebrew 的真实根目录不同。 - 不设置
GOBIN:保持 Go 默认的$GOPATH/bin行为。
13. Git¶
Git 已在第 0 节安装,确保后面的插件 clone 从一开始就使用所选最新稳定渠道。这里只配置脱敏身份:
先复制安全占位值,再按 Git 首次配置文档替换为自己的姓名和邮箱:
git config --global user.name "YOUR NAME"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
本文不强制 core.editor;请先安装自选编辑器,再按需配置。
本文不配置凭据 helper;尤其不要启用 credential.helper store,因为 Git 官方文档说明它会把凭据以未加密形式长期存盘。其他 helper 的存储方式应分别按官方文档审查。
14. 最终文件布局与验证清单¶
这里只列静态配置和主要安装目录;Atuin 数据库、fnm multishell、Zsh 历史、缓存与工作区等运行数据不在清单中。
~/.zprofile
~/.zshrc
~/.secrets # optional, never committed
~/.config/ghostty/config.ghostty
~/.config/starship.toml
~/.config/atuin/config.toml
~/.atuin/
~/.zsh/plugins/
~/.local/bin/
~/.local/share/zsh/site-functions/ # Ubuntu
~/.local/share/fonts/ # Ubuntu
~/.local/share/fnm/
~/.local/go/ # Ubuntu
~/.cargo/
下面的快速验收不会安装、升级或删除任何内容;请在 Ghostty 窗口中运行:
zsh -n "$HOME/.zshrc"
command -v starship atuin zoxide yazi ya fnm node go rustc git
command -v eza bat fd rg fzf file ffmpeg ffprobe jq pdftoppm resvg magick
command -v 7zz >/dev/null || command -v 7z
[[ "$(uname -s)" != Linux ]] || command -v wl-copy xclip
if [[ "$(uname -s)" == Darwin ]]; then
GHOSTTY_BIN='/Applications/Ghostty.app/Contents/MacOS/ghostty'
else
GHOSTTY_BIN="$(command -v ghostty)"
fi
[[ -x "$GHOSTTY_BIN" ]]
"$GHOSTTY_BIN" +validate-config
"$GHOSTTY_BIN" +show-face --string='A中文'
starship print-config >/dev/null
atuin daemon status
yazi --debug | grep -E 'Adapter\.matches[[:space:]]*:[[:space:]]*Kgp'
zsh -ic '
setopt ERR_EXIT
whence -w z y _yazi _ya
(( ${+functions[_yazi]} && ${+functions[_ya]} ))
[[ "${_comps[y]:-}" == _yazi ]]
'
unset GHOSTTY_BIN
fnm default
[[ "$(node --version)" == "$(fnm current)" ]]
go version
rustc --version
git --version
完整检查会加载 .zshrc,可能刷新 .zcompdump,会 source 已审阅的 .secrets,也可能更新 Atuin 本地运行状态;确认这些本机文件后再执行。
可选:运行完整自动验收与幂等性检查
(
setopt ERR_EXIT PIPE_FAIL
zsh -n "$HOME/.zshrc"
command -v zsh starship atuin zoxide yazi ya fnm node go rustup rustc cargo git
command -v eza bat fd rg fzf file ffmpeg ffprobe jq pdftoppm resvg magick
command -v 7zz >/dev/null || command -v 7z
[[ "$(uname -s)" != Linux ]] || command -v wl-copy xclip
if [[ "$(uname -s)" == Darwin ]]; then
GHOSTTY_BIN='/Applications/Ghostty.app/Contents/MacOS/ghostty'
else
GHOSTTY_BIN="$(command -v ghostty)"
fi
[[ -x "$GHOSTTY_BIN" ]]
"$GHOSTTY_BIN" +validate-config
"$GHOSTTY_BIN" +show-face --string='A中文'
starship print-config >/dev/null
atuin daemon status
yazi --debug | grep -E 'Adapter\.matches[[:space:]]*:[[:space:]]*Kgp'
zsh -ic '
setopt ERR_EXIT PIPE_FAIL
[[ -n "${ZSH_VERSION:-}" ]] || {
print -u2 -- "FAIL: verification shell is not Zsh"
exit 1
}
platform="$(uname -s)"
case "$platform" in
Linux)
expected_shell="$(command -v zsh)"
;;
Darwin)
expected_shell="/bin/zsh"
;;
*)
print -u2 -- "FAIL: unsupported platform: $platform"
exit 1
;;
esac
printf "configured shell: %s\n" "$SHELL"
[[ "$SHELL" == "$expected_shell" ]] || {
print -u2 -- \
"FAIL: configured shell is $SHELL; expected $expected_shell"
exit 1
}
(( ${fpath[(Ie)$HOME/.zsh/plugins/zsh-completions/src]} )) || {
print -u2 -- "FAIL: zsh-completions is not on fpath"
exit 1
}
(( ${+functions[_zsh_autosuggest_start]} )) || {
print -u2 -- "FAIL: zsh-autosuggestions is not loaded"
exit 1
}
(( ${+functions[_zsh_highlight]} )) || {
print -u2 -- "FAIL: zsh-syntax-highlighting is not loaded"
exit 1
}
source "$HOME/.zshrc"
source "$HOME/.zshrc"
unique_path=(${(u)path})
(( ${#path} == ${#unique_path[@]} )) || {
print -u2 -- "FAIL: PATH contains duplicates after repeated source"
exit 1
}
[[ "${_STARSHIP_ZSHRC_INITIALIZED:-}" == 1 ]] || {
print -u2 -- "FAIL: Starship sentinel is not initialized"
exit 1
}
starship_precmd_hooks=(${(M)precmd_functions:#prompt_starship_precmd})
starship_preexec_hooks=(${(M)preexec_functions:#prompt_starship_preexec})
(( ${#starship_precmd_hooks[@]} == 1 && ${#starship_preexec_hooks[@]} == 1 )) || {
print -u2 -- \
"FAIL: expected one Starship precmd/preexec hook, got ${#starship_precmd_hooks[@]}/${#starship_preexec_hooks[@]}"
exit 1
}
[[ "${_FNM_ZSHRC_INITIALIZED:-}" == 1 ]] || {
print -u2 -- "FAIL: fnm sentinel is not initialized"
exit 1
}
fnm_hooks=(${(M)chpwd_functions:#_fnm_autoload_hook})
(( ${#fnm_hooks[@]} == 1 )) || {
print -u2 -- "FAIL: expected one fnm hook, got ${#fnm_hooks[@]}"
exit 1
}
[[ "${_ATUIN_ZSHRC_INITIALIZED:-}" == 1 ]] || {
print -u2 -- "FAIL: Atuin sentinel is not initialized"
exit 1
}
atuin_strategies=(${(M)ZSH_AUTOSUGGEST_STRATEGY:#atuin})
atuin_preexec_hooks=(${(M)preexec_functions:#_atuin_preexec})
atuin_precmd_hooks=(${(M)precmd_functions:#_atuin_precmd})
(( ${#atuin_strategies[@]} == 1 )) || {
print -u2 -- \
"FAIL: expected one Atuin strategy, got ${#atuin_strategies[@]}"
exit 1
}
(( ${#atuin_preexec_hooks[@]} == 1 && ${#atuin_precmd_hooks[@]} == 1 )) || {
print -u2 -- \
"FAIL: expected one Atuin preexec/precmd hook, got ${#atuin_preexec_hooks[@]}/${#atuin_precmd_hooks[@]}"
exit 1
}
z_definition="$(whence -w z)"
printf "%s\n" "$z_definition"
[[ "$z_definition" == *function* ]] || {
print -u2 -- "FAIL: z is not a function"
exit 1
}
y_definition="$(whence -w y)"
printf "%s\n" "$y_definition"
[[ "$y_definition" == *function* ]] || {
print -u2 -- "FAIL: y is not a function"
exit 1
}
(( ${+functions[_yazi]} && ${+functions[_ya]} )) || {
print -u2 -- "FAIL: Yazi completion functions are not loaded"
exit 1
}
[[ "${_comps[y]:-}" == _yazi ]] || {
print -u2 -- "FAIL: y does not reuse Yazi completion"
exit 1
}
ctrl_r_binding="$(bindkey "^R")"
printf "%s\n" "$ctrl_r_binding"
[[ "$ctrl_r_binding" == *atuin* ]] || {
print -u2 -- "FAIL: Ctrl-R is not bound to Atuin"
exit 1
}
print -- "Zsh plugins and repeated-source checks: passed"
'
fnm_current="$(fnm current)"
fnm_default="$(fnm default)"
node_current="$(node --version)"
printf 'fnm current: %s\n' "$fnm_current"
printf 'fnm default: %s\n' "$fnm_default"
printf 'node: %s\n' "$node_current"
[[ -n "$fnm_default" ]]
[[ "$node_current" == "$fnm_current" ]] || {
print -u2 -- \
"FAIL: node version $node_current does not match fnm current $fnm_current"
exit 1
}
npm --version
go version
rustc --version
git --version
)
- 新 Ghostty 窗口中的 Starship 图标完整,没有空白方块;
- 拉丁字符使用 JetBrainsMono Nerd Font(可能显示
JetBrainsMono NF),中文使用 Sarasa Mono SC; -
Ctrl-R打开 Atuin 搜索,z可以完成目录跳转; - Yazi 能预览图片、视频、PDF、压缩包和 SVG,
q退出后y会带回当前目录。