【笔记】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 的边界
三者处在不同层次:
| 组件 | 职责 |
|---|---|
| Zsh | Shell 本身,解析并执行命令,提供 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 主题不会移除 git、z、sudo、自动建议和语法高亮等插件。失去的只是 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 和 $style 是 username 模块的内部变量,不能直接放进顶层代替 $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
[要渲染的内容](样式)
常见样式包括 bold、italic、underline、dimmed,以及 red、green、yellow、blue、purple、cyan、white 等颜色。也可以使用 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 采集
username、hostname、directory、time 和 status 等模块由 Starship 内部实现。每个模块定义自己的显示条件、配置项和内部变量。
例如:
1
2
3
4
[directory]
truncation_length = 3
truncate_to_repo = false
format = "[$path](bold yellow)"
这里 $path 是目录模块提供的数据;truncation_length 和 truncate_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"
普通模块通常是固定单例,如 $username;env_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.json、node_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模块能直接复现这一语义:成功时自动隐藏,失败时保留1、126、127、130等数字状态。- Unix 惯例是普通用户提示符为
$、root 为#,但原版ys第二行固定使用$,只在第一行改变 root 用户名样式。当前配置使用一个很小的自定义模块判断有效 UID,从而修正第二行符号。 - 提示符符号后恰好保留一个空格,让命令不与符号粘连,并从第 3 列开始,与第一行用户名对齐。
- Git 插件仍可在 Oh My Zsh 中提供别名,但 Git 模块没有进入 Starship 顶层
format,所以界面不显示分支和工作区状态。
7. 排错与验证顺序
7.1 先区分 TOML、Starship 和终端三层
看到字符、颜色或转义异常时,按三层检查:
- TOML 是否成功解析字符串?
- Starship 是否正确解析模块、变量、条件组和样式组?
- 终端主题是否正确显示 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_files、detect_folders和detect_extensions决定模块是否适用于当前目录,不负责显示文件。$ [ ] ( )是格式特殊字符;TOML 双引号与 Starship 各有一层转义。- 圆括号表示条件格式;要显示字面量括号必须转义。
- 自定义命令位于 prompt 热路径,应保持快速、稳定、无网络依赖。