文章

【笔记】Starship 提示符的配置模型与实践

【笔记】Starship 提示符的配置模型与实践

理解 Starship 的难点不在于颜色怎么写,而在于三个边界:它与 Zsh、Oh My Zsh 各负责什么;顶层模块和模块内部变量是什么关系;内置状态、环境变量和自定义命令怎样收敛为同一种编排对象。

Starship 不是 Shell,也不是 Oh My Zsh 的替代品。它只接管 prompt,即 Shell 等待输入时显示的提示符。进入 Starship 以后,配置也不是把所有数据变量直接塞进一个大模板,而是先形成模块,再编排模块。

一句话心智模型是:数据来源先被封装成模块,模块用自己的 format 渲染局部变量,顶层 format 再把模块编排成完整提示符。

flowchart LR
    A[内置采集] --> D[模块实例]
    B[环境变量] --> D
    C[自定义命令] --> D
    D --> E[模块内部 format]
    E --> F[顶层 format]
    F --> G[最终提示符]

1. Starship 与 Zsh、Oh My Zsh 的边界

三者处在不同层次:

组件职责
ZshShell 本身,解析并执行命令,提供 prompt 机制
Oh My Zsh管理 Zsh 插件、主题、补全、别名等交互配置
Starship跨 Shell 的 prompt 渲染器

容易产生的第一个误解是“已经使用 Oh My Zsh,就不能再使用 Starship”。实际上,两者只在主题渲染这一小块重叠。Oh My Zsh 主题和 Starship 都会修改 prompt,因此最终渲染者只能有一个;插件、补全和别名仍可继续由 Oh My Zsh 管理。

当前配置让 Oh My Zsh 保留插件能力,只关闭主题:

1
2
3
4
5
ZSH_THEME=""
source "$ZSH/oh-my-zsh.sh"

eval "$(mise activate zsh)"
eval "$(starship init zsh)"

Starship 初始化放在 Oh My Zsh 和 Mise 之后。这样 Oh My Zsh 不再加载 ys 主题,Starship 又能从 Mise 提供的 PATH 中被找到。

关闭 ys 主题不会移除 gitzsudo、自动建议和语法高亮等插件。失去的只是 ys 原来绘制的用户名、主机名、目录、VCS、时间和退出码样式,这些内容由 Starship 重新实现。

2. 两级编排模型

Starship 的配置文件默认是 ~/.config/starship.toml。初看配置时,很容易把 $username$user$time 都理解成可在任意位置使用的预定义变量,进而把 Starship 想成一个全局字符串模板。真正的结构是两级编排:顶层编排模块,模块内部再编排自己的局部变量。

2.1 顶层 format 编排模块

顶层 format 决定使用哪些模块、模块顺序、固定文字、空格和换行:

1
2
3
format = """\
[#](bold blue) $username [@](white) $hostname [in](white) $directory $time$status
${custom.prompt_symbol}"""

这里的 $username$hostname$directory$time$status 都表示完整模块。${custom.prompt_symbol} 也是完整模块,只是模块名包含 .,所以必须用 ${...} 划定名称边界。

如果没有显式设置顶层 format,Starship 使用默认模块集合,可近似理解为:

1
format = "$all"

显式列出模块则形成显示白名单:没有放进顶层 format 的 Git、语言版本、云环境等模块不会进入最终提示符,不必逐个设置 disabled = true。这也是“保留 Oh My Zsh 的 git 插件,但不显示 Git 状态”能够同时成立的原因:插件是否加载与 Starship 是否编排 Git 模块是两件事。

2.2 模块自己的 format 编排内部变量

模块负责采集一类状态,并把状态暴露为模块内部变量:

1
2
3
4
5
[username]
show_always = true
format = "[$user]($style)"
style_user = "bold cyan"
style_root = "bold black bg:yellow"

这段配置存在两个不同层级:

1
2
3
4
5
顶层 $username
    ↓ 调用 username 模块
模块获取 $user 和 $style
    ↓ 执行 [username].format
输出渲染后的用户名片段

$user$styleusername 模块的内部变量,不能直接放进顶层代替 $username。同理,顶层 $directory 引用目录模块,而 [directory].format 中的 $path 才是实际路径值。

$time 容易造成错觉:

1
2
3
4
format = "$time"            # 顶层:引用 time 模块

[time]
format = "[$time]($style)"  # 模块内部:引用具体时间值

名字相同不代表是同一个作用域。判断一个 $name 时,第一步不是查它的值,而是先看它出现在哪一层 format:顶层通常把它解释为模块,模块内部才把它解释为该模块提供的数据。

2.3 模块是两级之间的稳定边界

厘清作用域以后,Starship 的整体模型可以进一步展开:

1
2
3
4
5
6
7
8
9
10
11
12
数据来源
├── Starship 内部采集
├── 环境变量
└── 自定义命令
        ↓
模块实例
├── 固定单例:username、directory、time 等
└── 命名实例:env_var.wsl、custom.vpn 等
        ↓
模块内部 format 编排局部变量
        ↓
顶层 format 编排完整模块

顶层不关心用户名是怎样读取的,也不关心 VPN 状态来自环境变量还是脚本,只关心模块最终输出的一段 prompt。模块因此是数据采集与顶层布局之间的稳定边界。后面的内置模块、env_var.*custom.* 并不是三套互不相干的配置体系,而是三种数据来源进入同一模块模型的方式。

3. Format String 语法

3.1 固定文字、变量和样式组

格式字符串可以包含固定文字、变量和样式组:

1
format = "[$user](bold cyan) [@](white) [$hostname](bold green)"

样式组的语法是:

1
[要渲染的内容](样式)

常见样式包括 bolditalicunderlinedimmed,以及 redgreenyellowbluepurplecyanwhite 等颜色。也可以使用 ANSI 色号、前景色、背景色和十六进制颜色:

1
format = "[warning](bold fg:#ff8800 bg:blue)"

颜色名应优先采用官方文档列出的形式。例如当前版本使用 purple;写成未识别的颜色名时,样式可能被忽略而只保留文字。

3.2 特殊字符与两层转义

$[]() 在 Starship 格式字符串中有特殊含义。要显示这些字符本身,需要在 Starship 语法中使用反斜杠:

1
format = '\[\$\]'

TOML 单引号是字面字符串,反斜杠不再被 TOML 解释。双引号字符串则还要经过 TOML 的一层转义:

1
format = "\\[\\$\\]"

因此下面两者都显示 [$]

1
2
3
4
5
TOML 解析:处理引号和反斜杠
    ↓
Starship 解析:处理变量、样式组和条件组
    ↓
终端渲染:解释 ANSI 颜色控制序列

当前配置中的 (WSL) 正体现了这个边界:

1
format = "... $hostname\\([WSL](bold purple)\\) ..."

\\(\\) 经过 TOML 后变成 Starship 所需的 \(\);中间的 [WSL](bold purple) 是独立样式组。把括号、TOML 转义和样式组混在一个难以识别的嵌套中,会增加排错成本。

3.3 条件格式不是普通括号

Starship 用圆括号表示条件格式:

1
format = "(@$region)"

$region 有值时显示 @ 和 region;为空时整个条件组隐藏。这样不会在数据缺失时留下孤立的 @

只有固定文字、没有变量的条件组永远不显示:

1
format = "(hello)"

因此要显示字面量括号必须写 \(\),不能把它们当普通字符。

4. 模块的三种数据来源

从顶层看,所有模块都以同样的方式参与编排。差异主要在于数据由谁采集。

4.1 内置模块:Starship 采集

usernamehostnamedirectorytimestatus 等模块由 Starship 内部实现。每个模块定义自己的显示条件、配置项和内部变量。

例如:

1
2
3
4
[directory]
truncation_length = 3
truncate_to_repo = false
format = "[$path](bold yellow)"

这里 $path 是目录模块提供的数据;truncation_lengthtruncate_to_repo 控制模块怎样计算最终路径。

4.2 env_var.*:把环境变量适配成模块

环境变量模块读取 Shell 环境中已经存在的数据:

1
2
3
[env_var.wsl]
variable = "WSL_DISTRO_NAME"
format = "[$env_value](bold purple)"

数据路径是:

1
2
3
4
5
环境变量 WSL_DISTRO_NAME
    ↓ env_var.wsl 读取
模块内部变量 $env_value
    ↓ 模块 format
顶层通过 ${env_var.wsl} 引用

如果不写 variable,Starship 会把 env_var. 后面的实例名隐式当作环境变量名:

1
2
[env_var.WSL_DISTRO_NAME]
format = "[$env_value](bold purple)"

$env_value 的具体值因此由模块配置隐式决定,而不是一个全局通用值。

env_var. 前缀不是为了再次声明变量名,而是模块类型和命名空间:

1
2
3
env_var.wsl
│       └─ 实例名:wsl
└───────── 模块类型:从环境变量采集数据

这使同名实例可以由不同实现并存:

1
2
3
4
5
[env_var.wsl]
variable = "WSL_DISTRO_NAME"

[custom.wsl]
command = "detect-wsl"

普通模块通常是固定单例,如 $usernameenv_var.*custom.* 则是可命名的多实例模块。前缀解决类型识别、命名冲突和多实例管理,不只是语法装饰。

环境变量不存在时,模块通常隐藏;配置 default 后可以在变量缺失时显示默认值。已有状态在环境变量中时,应优先使用 env_var.*,因为它不需要为每次 prompt 额外启动命令。

4.3 custom.*:用命令采集数据

没有内置数据和环境变量时,可以执行自定义命令:

1
2
3
4
5
[custom.vpn]
command = "printf VPN"
when = "ip link show tun0 >/dev/null 2>&1"
format = "[$output]($style)"
style = "bold green"

工作流程是:

1
2
3
4
5
6
7
8
9
when 返回 0
    ↓
执行 command
    ↓
标准输出进入 $output
    ↓
模块 format 渲染
    ↓
顶层 ${custom.vpn} 决定位置

when 决定是否显示,command 决定采集什么,format 决定怎样显示。自定义模块本质上仍是普通模块,只是数据采集从 Starship 内部实现换成了外部命令。

提示符会频繁刷新,自定义命令必须足够快。公网请求、复杂扫描和大型解释器启动不应直接放进 prompt;更合适的方式是后台刷新缓存,Starship 只读取缓存文件。

5. detect_* 决定模块是否适用于当前目录

语言和工具链模块不应在所有目录中出现。Starship 可以根据当前目录特征判断它是否像某类项目:

1
2
3
4
[nodejs]
detect_files = ["package.json", "package-lock.json"]
detect_folders = ["node_modules"]
detect_extensions = ["js", "ts"]

三类规则分别匹配文件名、文件夹和扩展名。通常任意正向条件命中即可触发模块。例如目录中存在 package.jsonnode_modules 或 JavaScript/TypeScript 文件时,Node.js 模块可以出现。

! 开头表示排除项:

1
detect_extensions = ["ts", "!video.ts", "!audio.ts"]

这是因为 .ts 既可能表示 TypeScript,也可能是 MPEG Transport Stream 文件。命中排除项时模块不应据此出现。

detect_* 不负责显示文件信息,只负责回答:当前目录是否满足模块的出现条件? 当前提示符不显示语言和 Git 模块,因此无需配置这些检测规则。

6. 当前提示符的设计

当前配置不是从 Starship 默认主题开始,而是先拆解 Oh My Zsh 的 ys 主题。ys 把用户名、主机名、目录、VCS、虚拟环境、时间和退出码组合在一起;实际需要的是它的两行布局、时间和失败退出码,不需要 Git、SVN、Hg 等 VCS 信息。

这一步再次说明“主题”和“插件”不是同一个边界:可以关闭 ys 的显示逻辑,继续保留 Oh My Zsh 的 git 插件作为命令别名来源,再由 Starship 只重建真正需要的模块。

最终目标是保留 ys 的主要视觉结构,删除 VCS 和语言信息,并补充 WSL 环境标签与 Unix 权限提示:

1
2
# yungyu @ Yungyu-PC(WSL) in ~/Project [12:30:20]
$

上一条命令失败时:

1
2
# yungyu @ Yungyu-PC(WSL) in ~/Project [12:30:20] C:127
$

完整配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"$schema" = "https://starship.rs/config-schema.json"

format = """\
[#](bold blue) $username [@](white) $hostname\\([WSL](bold purple)\\) [in](white) $directory $time$status
${custom.prompt_symbol}"""

add_newline = false

[username]
show_always = true
format = "[$user]($style)"
style_user = "bold cyan"
style_root = "bold black bg:yellow"

[hostname]
ssh_only = false
format = "[$hostname](bold green)"

[directory]
truncation_length = 3
truncate_to_repo = false
format = "[$path](bold yellow)"

[time]
disabled = false
time_format = "%H:%M:%S"
format = "[\\[[$time](bold white)\\]](white)"

[status]
disabled = false
format = " [C:$status](bold red)"
map_symbol = false
pipestatus = false

[custom.prompt_symbol]
command = "if [ \"$(id -u)\" -eq 0 ]; then printf '#'; else printf '$'; fi"
when = true
format = "[$output](bold red) "

这里有几个来自实际辨析的取舍:

  • (WSL) 最初可以考虑由 ${env_var.WSL_DISTRO_NAME} 条件显示,但当前配置只服务这台已经确认的 WSL2 主机,而且只想显示固定文字 WSL,所以直接使用环境标签更简单。若配置需要在 WSL 和原生 Linux 之间共享,再把它改为 env_var.* 模块更合理。
  • ys 的退出码表达式只在失败时显示 C:<数字>。Starship 的 status 模块能直接复现这一语义:成功时自动隐藏,失败时保留 1126127130 等数字状态。
  • Unix 惯例是普通用户提示符为 $、root 为 #,但原版 ys 第二行固定使用 $,只在第一行改变 root 用户名样式。当前配置使用一个很小的自定义模块判断有效 UID,从而修正第二行符号。
  • 提示符符号后恰好保留一个空格,让命令不与符号粘连,并从第 3 列开始,与第一行用户名对齐。
  • Git 插件仍可在 Oh My Zsh 中提供别名,但 Git 模块没有进入 Starship 顶层 format,所以界面不显示分支和工作区状态。

7. 排错与验证顺序

7.1 先区分 TOML、Starship 和终端三层

看到字符、颜色或转义异常时,按三层检查:

  1. TOML 是否成功解析字符串?
  2. Starship 是否正确解析模块、变量、条件组和样式组?
  3. 终端主题是否正确显示 ANSI 样式?

不要一开始就把问题归因于终端颜色。当前 (WSL) 无颜色的问题最终来自未采用文档中的颜色名;前面的括号转义调整虽然改变了结构,却没有命中根因。

7.2 直接渲染成功与失败状态

修改配置后可先绕过交互式 Shell,直接渲染:

1
2
starship prompt --status 0 --path "$PWD" --logical-path "$PWD"
starship prompt --status 127 --path "$PWD" --logical-path "$PWD"

再检查 Zsh 配置语法:

1
zsh -n ~/.zshrc

确认模块为什么显示或隐藏:

1
starship explain

当前会话重新加载:

1
exec zsh

source ~/.zshrc 会在当前进程中再次执行所有交互初始化。配置并非完全幂等时,exec zsh 用一个新 Zsh 替换当前进程,通常更干净。

7.3 性能问题

全局选项中与性能直接相关的主要是:

1
2
3
scan_timeout = 30
command_timeout = 500
follow_symlinks = true

目录很大、符号链接指向网络文件系统,或自定义命令较慢时,prompt 可能出现延迟。优先减少模块和外部命令,再考虑调整超时;提高超时只能减少模块超时失败,不能让慢命令变快。

8. 复习索引

  • Starship 只接管 prompt;Zsh 执行命令,Oh My Zsh 管理插件和交互配置。
  • Oh My Zsh 主题和 Starship 都写 prompt,应关闭其中一套主题渲染。
  • 顶层 format 编排模块;模块自己的 format 编排模块内部变量。
  • $username 是顶层模块引用;[username] 内的 $user 是局部数据。
  • 内置模块、env_var.*custom.* 在顶层地位相同,差别在数据来源。
  • env_var. 是模块类型和命名空间;后缀是实例名,variable 才是显式数据源。
  • 未配置 variable 时,env_var. 后缀隐式充当环境变量名,值进入 $env_value
  • detect_filesdetect_foldersdetect_extensions 决定模块是否适用于当前目录,不负责显示文件。
  • $ [ ] ( ) 是格式特殊字符;TOML 双引号与 Starship 各有一层转义。
  • 圆括号表示条件格式;要显示字面量括号必须转义。
  • 自定义命令位于 prompt 热路径,应保持快速、稳定、无网络依赖。

参考资料

本文由作者按照 CC BY 4.0 进行授权