从零恢复我的终端开发环境:Ubuntu 24.04 与 macOS

这是一份按依赖顺序编排的开发环境恢复手册:从系统前置条件开始,依次恢复字体、终端、Shell、现代命令行工具和语言工具链。它关注的是拿到新机器后如何得到一套可验证的工作环境,而不是逐项讲解每个工具的全部功能。

适用范围与验证边界

项目本文范围
Ubuntu24.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
ShellZsh、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/ppaGit 项目列出的 latest stable 渠道,但不是 Ubuntu 官方仓库;继续前应接受这一额外维护方的信任边界。

macOS Apple Silicon

本文的 Homebrew 路径按 Apple Silicon 设计。开始前确认系统仍在 Homebrew 当前支持范围内。

先安装 Xcode Command Line Tools

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. 字体安装

最终回退顺序只有一处权威定义:

  1. JetBrainsMono Nerd Font 渲染拉丁字符和 Nerd Font 图标;
  2. Sarasa Mono SC 渲染简体中文;
  3. Maple Mono NF CN 只作为末级兜底。
项目UbuntumacOS
安装位置~/.local/share/fontsHomebrew Cask 管理
JetBrains Mono从 Nerd Fonts 当前发布解析资产font-jetbrains-mono-nerd-font
Maple Mono从 Maple Mono 当前发布解析资产font-maple-mono-nf-cn
Sarasa只选择 hinted SarasaMonoSC-TTF-*.7zfont-sarasa-gothic
缓存安装后执行 fc-cache -f由系统与 Homebrew 管理

Ubuntu 24.04

Nerd FontsMaple 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 FontSarasa 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 节的回退链;首项也提供 Starshipeza --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 结果如下:

字符预期字体
AJetBrainsMono 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 中无条件命中的 prependbinding="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-faceGHOSTTY_LOG=debug可见字体、实际字形与运行时解析
Fontconfig 匹配fc-matchfc-match -s首选字体与候选顺序
Fontconfig 规则FC_DEBUGgrep哪条规则改写了 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-autosuggestionszsh-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;完成后续章节后,zy 都应是函数,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-fuzzyfalse、启用的 sync records,以及 enabled = trueautostart = 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 分别提供 ezabatfdrg

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、PDFffmpeg7zjqpdftoppm
文件名、内容与目录跳转fdrgfzfzoxide
SVG 与更多图片格式resvgmagick
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 官方稳定归档同时包含 yaziya 和 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-fullimagemagick-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

ueberzugppchafa 和未选用的剪贴板后端可以显示缺失,不影响 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 会带回当前目录。
这篇怎么样?