本文记录一下我目前自用的终端主题样式。
Oh My Zsh 是一个管理 Zsh 配置的框架,提供了主题、插件和一些常用配置。
我最初是为了美化终端,才选择了 Zsh 和 Oh My Zsh 这套方案。

虽然官方提供的主题样式很多,但我尝试之后,总觉得大部分太花哨,简洁一些的又不太符合我的审美,所以最终还是自己设计了一个自用的终端主题。

后面我将从现有配置开始,把自定义主题的方法、主题配置中常用的提示符,以及我正在使用的插件整理一下。

# 当前效果

基础主题:当前目录、Git 分支和表示命令结果的提示符

主题本身只有六行:

PROMPT="%{$fg[cyan]%}%c%{$reset_color%}"
PROMPT+=' $(git_prompt_info)%(?:%{$fg_bold[green]%}%1{%} :%{$fg_bold[red]%}%1{%} )'
ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[blue]%}git:(%{$fg[red]%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} "
ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[blue]%})"
ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg[blue]%})"

第一行显示当前目录的末一级;第二行接上 Git 信息和状态符号。
后四行规定 Git 信息的前缀、后缀,以及工作区干净或有改动时显示的内容。
这里我把 DIRTY 和 CLEAN 写得一样,所以这个主题只显示分支,不区分工作区是否有改动。

# 配置自定义主题

Oh My Zsh 的自定义规范允许把主题放在 $ZSH_CUSTOM/themes 中。
这样自己的配置有独立文件,后续查找和修改都方便。

默认安装下,自定义的主题文件放在: ~/.oh-my-zsh/custom/themes 目录下,以 .zsh-theme 结尾。

新建主题时,要先准备目录。如果设置过环境变量 ZSH_CUSTOM ,可以使用自己设置的目录:

mkdir -p "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes"

把前面的六行配置保存为该目录下的 sycamore-simple.zsh-theme 文件,然后再修改 ~/.zshrc 中已有的主题设置,这里的主题名称不带 .zsh-theme 后缀。
以我自用的主题为例,如下图所示:

在 .zshrc 中设置自定义主题

这行配置应放在 source $ZSH/oh-my-zsh.sh 之前,由 Oh My Zsh 初始化时加载。

保存后记得打开一个新终端检查效果。

# 主题配置详解

这一节主要解释提示符中的配置项。
如果暂时不想逐项了解,可以直接跳到下一节常用实例演示,简单看看效果后,再回来查找更复杂的写法。

# 谁负责显示

在解释具体的可配置项之前,先要说明白是什么在负责显示。
ZSH_THEME 负责让 Oh My Zsh 找到主题文件, git_prompt_info 是它提供的辅助函数。
而真正决定提示符内容的是 Zsh 的变量和提示符展开语法。

变量 用途
PROMPT / PS1 每次等待输入时的主提示符,两者是同一参数的名称
RPROMPT / RPS1 编辑命令行时的右侧提示符
PS2 / PROMPT2 引号、循环等输入尚未结束时的续行提示符
RPS2 续行时的右侧提示符
PS3 select 选择菜单的提示符
PS4 开启执行跟踪时,每条跟踪记录的前缀

大部分主题主要调整前两项,其他变量可以根据需要配置。
比如命令还没输入完整时, PS2 可以提示还缺少哪部分; PS3 用于选择菜单; PS4 则更适合调试脚本。

下面列几个简单的配置例子:

PS2='%_> '
PS3='请选择编号:'
PS4='+%N:%i> '

PS2 中的 %_ 会显示尚未结束的语法结构,例如引号或循环。按回车后出现这个提示,说明命令还需要继续输入。
PS3 会出现在 select 循环的选择提示中,不影响普通命令输入。
PS4 中的 %N 和 %i 分别显示当前脚本或函数等结构的名称及其中的行号,运行 set -x 后就能在执行跟踪中看到;调试结束后,用 set +x 关闭。

# 提示符的可配置内容

Zsh 会在显示提示符时展开 % 序列,下面按用途梳理官方提示符的配置项:
(这里的内容仅包含提示符,不是 Zsh 的全部参数,具体定义可查 Zsh 提示符文档)

类别 写法 显示内容
目录 %c 当前目录末一级,家目录可缩写
目录 %~ 完整路径,家目录等可缩写为 ~
目录 %/ 、 %d 不缩写家目录的完整路径
身份 %n 、 %m 、 %M 用户名、短主机名、完整主机名
终端 %l 、 %y 当前终端名称,前缀处理有所不同
状态 %? 、 %j 、 %L 上条命令退出码、作业数、Shell 层级
权限 %# 有特权时显示 # ,否则显示 %
历史 %h 、 %! 当前历史事件编号
时间 %T 、 %* 24 小时制时分、时分秒
日期 %D{%Y-%m-%d} 按指定格式显示日期
样式 %F{cyan} 、 %f 设置、恢复前景色
样式 %K{black} 、 %k 设置、恢复背景色
样式 %B / %b 、 %U / %u 开关粗体、下划线
样式 %S / %s 开关突出显示模式
字面量 %% 、 %) 显示百分号、右括号

目录序列前还可以加数字。例如 %2~ 保留末尾两级,适合既想知道项目名,又不想让路径占满一行的情况。

续行和调试提示符中常用的 %_ 、 %N 、 %i ,前一节已经结合 PS2 和 PS4 说明。
其他相关写法还有 %^ (按相反顺序显示未结束的语法结构)、 %x 和 %I (代码定义所在的文件及行号),以及 %e (求值深度),需要时可以再查阅官方文档。

需要把提示符分成两行时,可以在字符串中直接换行,或拼接 $'\n' 。
普通单引号中的 \n 只是两个字符,不会自动变成换行。

日期与时间还可以按下面的格式显示:

写法 内容
%t 、 %@ 带 am/pm 的 12 小时制时间
%D 、 %w 、 %W 分别为两位年 - 月 - 日、星期缩写 - 日、月 / 日 / 两位年
%D{格式} 交给 strftime 格式化;例如 %Y 四位年、 %m 月、 %d 日、 %H:%M:%S 时分秒

花括号内有一套日期格式,不能与外层提示符代码混用。
Zsh 在这里补充了不带前导零的 %f (日)、 %K (24 小时)、 %L (12 小时),以及小数秒 %. ;
%6. 请求六位小数, %9. 或 %N 请求九位,实际精度取决于系统时钟。
%-d 等写法可去掉前导零或空格,其他格式扩展是否可用由系统支持决定。

还有几项与显示有关的写法,可以分开理解:

  • %E :清除光标右侧直到行尾的内容。
  • %{...%} :把颜色控制码等不占字符宽度的内容包起来,避免 Zsh 把它们算进提示符长度。它不是用来包住普通文字的。
  • %G :在 %{...%} 内补充字符宽度。例如图标实际占一格,可以用 %G 告诉 Zsh;占两格则用 %2G 。

目录还有几种旧写法: %c 和 %. 显示路径末一级, %C 类似,但不会把家目录换成 ~ 。
官方在线手册已不推荐这几种写法。新写主题时,可以用 %1~ 代替 %c 、 %. ,用 %1/ 代替 %C 。
前面展示的是我现有的主题代码,所以仍保留了 %c 。

如果需要把自己计算的文字交给提示符,也可以使用 psvar 数组: %v 读取第一项, %2v 读取第二项,负数下标从末尾计数。
例如先设置 psvar=(demo) ,提示符中的 %v 就显示 demo 。
这个方法让函数先更新值,再让提示符负责排版。

# 根据条件改变显示内容

以我自用的主题为例,主题文件第二行中的 %(?:成功内容:失败内容) 是条件展开:
上条命令成功时使用前一段,否则使用后一段。
因此,最后的绿色或红色符号反映的是上一条命令的退出状态。

如果想看到实际退出码,可以把 %? 加到提示符中。

在 %(?:成功内容:失败内容) 中, ? 表示检查退出状态,两个 : 分别隔开条件、成功时的内容和失败时的内容。
这里的 : 就是分隔符。也可以同时换成点号,写成 %(?.成功内容.失败内容) ,效果相同;不能只改其中一个。

所以,一般形式可以写成 %(条件.成立内容.不成立内容) 。
需要给条件指定一个比较值时,可以在左括号前或后加数字 n ,省略时为零。除了退出状态,还能判断这些信息:

条件字符 成立条件
! 当前 Shell 具有特权
# 、 g 有效用户 ID、有效组 ID 等于 n
? 上条命令退出码等于 n
_ 已开始的 Shell 语法结构至少有 n 个
/ 、 C 绝对路径至少有 n 层,根目录算零层
c 、 . 、 ~ 做过家目录或命名目录替换后的路径至少有 n 层
D 、 d 、 w 月份、日期、星期等于 n ;月份从零计,星期日为零
T 、 t 当前小时、分钟等于 n
e 、 j 、 L 求值深度、作业数、Shell 层级至少为 n
S SECONDS 至少为 n ,它不是上一条命令的耗时
v 、 V psvar 至少有 n 项;或第 n 项已设置且非空
l 当前行已输出至少 n 个字符;负数时判断到对侧边缘至少还剩多少格

例如 %(!.#.>) 会在有特权时显示 # ,否则显示 > ,这里使用的是点号分隔。
如果成立时要显示的文字本身包含点号,就应该改用其他分隔符,以免 Zsh 提前把这段文字判断为结束。分支里需要显示右括号时,可以写 %) 。
条件也可以嵌套。除 l 的剩余宽度用法外,负数条件参数按绝对值处理。

# 显示动态信息

Git 分支会随着切换分支而变化,所以不能只在加载主题时读取一次。
我在主题第二行写了 $(git_prompt_info) ,让提示符每次显示时都调用这个函数,把返回的分支信息放到对应位置。
这种把命令输出放进字符串的写法,叫作命令替换。

要让提示符支持这种写法,需要开启 PROMPT_SUBST :

setopt PROMPT_SUBST

Oh My Zsh 已经开启了这个选项,使用它时一般不需要重复设置。
如果脱离 Oh My Zsh 单独测试,就需要自己开启;前面主题中的颜色变量和 git_prompt_info 函数仍然需要 Oh My Zsh 提供。

还要注意引号的区别:

# 设置时保留函数调用,每次显示提示符时再执行
PROMPT='$(git_prompt_info) '
# 设置时立即执行函数,保存这一次返回的结果
PROMPT="$(git_prompt_info) "

因此,我在自用主题的第二行使用了单引号。
后面显示电量和 Python 环境的例子,也采用相同的方式。

# 常用实例演示

下面的例子都基于前面 自用主题 的内容,每次只修改其中一项,方便对比效果。

# 显示完整路径

如果想显示完整目录,可以把主题的第一行替换为:

PROMPT="%{$fg[cyan]%}%/%{$reset_color%}"

完整路径示例:提示符显示当前目录的绝对路径

如果希望 home 目录缩写为 ~ ,就把 %/ 换成 %~ 。两种写法都保留目录层级,只是 home 目录的表达不同。

# 显示用户名

如果希望同时显示用户名和主机名,可以在主题后追加一行,把它们放到目录前面:

PROMPT='%n@%m '"$PROMPT"

用户名示例:目录前增加当前登录用户名和主机名

只需要用户名时,把 %n@%m 换成 %n 即可。

# 在右侧加上时间

在主题后追加这一行,可把时间放在左侧提示符末尾:

PROMPT+=' %F{yellow}%*%f '

如果不想挤占目录和输入之间的位置,则单独使用右侧提示符:

RPROMPT='%F{244}%D{%H:%M:%S}%f'

右侧时间示例:左侧保留主题,右侧显示时分秒

两种写法选一种即可。
后面的电量和 Python 例子也使用 RPROMPT ,每次赋值都会替换原有的右侧内容,不会自动叠加。
这里显示的是提示符展开时的时间,不是持续跳动的时钟;
命令行变长后,右侧提示符也会让出输入空间。

# 显示设备电量

如果想在提示符里显示电量,就需要先读取系统信息。这里以 macOS 为例,在主题后加入下面的函数和右侧提示符配置:

sycamore_battery() {
  local value
  value=$(pmset -g batt 2>/dev/null | sed -nE 's/.*[[:space:]]([0-9]+)%;.*/\1/p' | head -n 1)
  [[ -n $value ]] && print -rn -- "bat ${value}%%"
  return 0
}
RPROMPT='$(sycamore_battery)'

电量示例:右侧显示系统返回的电池百分比

没有电池信息就不显示。函数输出中的 %% 会在提示符展开后变成一个百分号;
这也是动态文本和普通终端输出需要区分的地方。
电量查询会随提示符刷新执行,不会后台轮询。

# Python 项目、环境和版本

如果经常在不同的 Python 项目和环境之间切换,也可以把项目标记、环境名称和解释器版本放到提示符中。

把以下函数和 RPROMPT 放在主题六行之后。
它仅检查当前目录中的 pyproject.toml 或 requirements.txt ;虚拟环境则读 VIRTUAL_ENV 或 CONDA_DEFAULT_ENV 。
版本显示当前 PATH 能找到的 python3 :

sycamore_python() {
  local env_name version text=''
  [[ -f pyproject.toml || -f requirements.txt ]] && text='Python project'
  if [[ -n $VIRTUAL_ENV ]]; then
    env_name=${VIRTUAL_ENV:t}
  elif [[ -n $CONDA_DEFAULT_ENV ]]; then
    env_name=$CONDA_DEFAULT_ENV
  fi
  [[ -n $env_name ]] && text+=" env:${env_name}"
  if [[ -n $text ]] && (( $+commands[python3] )); then
    version=$(python3 --version 2>/dev/null)
    text+=" ${version}"
  fi
  print -rn -- "${text//\%/%%}"
  return 0
}
RPROMPT='$(sycamore_python)'

Python 示例:项目文件线索、环境名称与当前解释器版本

这里根据当前目录中的两个配置文件判断是否显示 Python 项目标记,环境名称来自环境变量,版本则来自当前 python3 命令。
这个例子中,没有向父目录查找,也没有读取版本管理器的配置。
如果频繁刷新提示符,可以在切换环境后再查询并保存版本,避免每次都启动解释器。

# 区分 Git 工作区改动

前面我把两种状态设置成了一样的内容。如果想区分工作区是否有改动,只要修改 DIRTY 这一行,就能在有改动时增加一个星号:

ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[blue]%}) *"

Git 改动示例:分支后出现星号

它使用 Oh My Zsh 的 Git 状态检测,具体是否包含未跟踪文件还会受相关配置影响。
这里用星号提醒有改动,但如果要查看改了什么,仍然需要使用 git status 。

# 插件

我目前启用的是下面六个插件:

plugins=(git zsh-autosuggestions zsh-syntax-highlighting z extract web-search)

这行同样写在 .zshrc 中,且要放在 Oh My Zsh 初始化之前。
主题负责提示符,插件则补充命令和输入行为。
另外,名字写进列表之前,对应插件必须已经安装。

# git

git 插件提供常用 Git 别名和辅助功能。
例如 gst 对应 git status , gco 对应 git checkout 。
这些别名可以减少重复输入。
主题里的 Git 分支提示由 Oh My Zsh 自带的 git_prompt_info 函数提供,不需要启用 git 插件。

# zsh-autosuggestions

输入命令时,它会在光标后显示建议,默认可根据历史记录提出补全。
例如曾经执行过 python test.py ,再输入开头的几个单词,就有机会直接补齐。

输入建议对比:同一命令前缀在启用与临时禁用时的显示

图中上面一行是未启用建议的情况,下面一行启用了建议,灰色部分是尚未接受的补全内容。

# zsh-syntax-highlighting

这个插件能给输入的命令着色,帮助区分命令、参数、引号以及部分不合法输入。
(这里的颜色改变是输入部分的内容,与提示符中的目录颜色无关)

语法高亮对比:同一输入在启用与临时禁用时的显示

图中依次是未启用高亮、启用后的有效命令,以及未找到的命令。

# z

z 根据访问过的目录记录进行跳转。
例如进入过项目目录之后,可以用 z 项目名片段 匹配它。
首次使用前,需要先进入目标目录,让 z 留下访问记录。

# extract

extract filename ,根据压缩文件类型调用对应解压工具,省去每次选择解压命令的步骤。
此插件统一了入口,非常好用。

web-search 提供从终端发起网页搜索的命令,例如 google zsh prompt 。
它会把查询交给浏览器打开,适合在写命令时顺手查资料。

# 字体和显示

最后再说一下字体和显示效果。我目前的 iTerm 默认配置使用 JetBrainsMonoNFM-Regular ,字号为 12。
使用其他终端时,也需要在对应的设置中选择合适的字体,因为部分主题包括图标字符,如果用错了字体则大概率显示不出来。

我的自用主题中的 就是图标字符,需要字体包含对应字形才能显示。
如果看到该字符显示成了方框,就要考虑更换为合适的字体。当然也可以直接在主题文件中把该字符换成普通的 > ,不必让一个符号的显示影响使用。

除了能否显示图标,还要注意字体的字符占用的宽度。
我的自用主题中,用 %1{%} 声明这个片段占一个字符单元。如果终端实际把它画成两格,就可能发生光标错位。