Use Gum in Nushell Script for Better UIUX
Charm 团队推出了一系列 CLI/TUI 工具,其中就包括 OpenCode 的前身crush: Glamourous agentic coding for all 💘
本文要介绍的 Gum 是 Charmbracelet 推出的另一个 CLI 工具,让你在 Shell 脚本中轻松添加交互式组件——选择菜单、文本输入、确认弹窗、加载动画等,不用手写 TUI 逻辑。
在 Bash/Zsh 等 UNIX-Compatible Shell 里用 gum 很简单,官方仓库也提供了示例。Nushell 也可以很顺手地使用 gum,只是因为 Nu 有结构化数据和不同的错误/退出码模型,少数写法需要换一种思路。
本文先给一组可以直接复制的 Nushell 写法,再集中解释容易踩坑的地方。
Install
brew install gumscoop install charm-gum# Archpacman -S gumCheatsheet
下面的示例都在 Nushell 中运行。建议用 ^gum 明确调用外部命令,避免将来与 Nu 内置命令、alias 或自定义函数重名。
交互输入
let choice = (^gum choose "feat" "fix" "docs" "chore" --header "Choose a commit type")print $choicechoose 适合固定选项,例如选择 commit type、环境、任务名称等。
let items = ["apple", "banana", "cherry"]let pick = ($items | str join "\n" | ^gum filter --placeholder "Search fruit")print $pickfilter 适合列表较长、需要搜索的场景。Nu 的 list 需要先转成换行分隔的文本,后面的 Gotchas 会详细解释。
# 单行输入let name = (^gum input --placeholder "Project name")# 多行输入let notes = (^gum write --header "Release notes" --show-line-numbers)input 用来收集短文本,write 用来写多行内容,比如 release notes、commit body、说明文字等。
if (^gum confirm "Delete file?"; $env.LAST_EXIT_CODE == 0) { print "Deleting..."}confirm 的核心结果在退出码里:选择 Yes 时退出码是 0,选择 No 或取消时为非 0。
终端输出美化
^gum style --foreground 212 --border-foreground 212 --border double --padding "1 2" --margin "1" "Hello from Nushell"╔══════════════════════╗║ ║║ Hello from Nushell ║║ ║╚══════════════════════╝常用参数:
--foreground/--background: 文字/背景颜色(数字或十六进制)--border/--border-foreground: 边框样式(single,double,rounded等)和颜色--padding/--margin: 内边距/外边距(格式:"上下 左右"或"上 左右 下")--align: 文本对齐(left,center,right)
# 带时间戳的结构化日志^gum log --time rfc822 --structured --level debug "Creating file..." name file.txt# 10 Mar 26 22:55 CST DEBUG Creating file... name=file.txt# 不同日志级别^gum log --level error "Something went wrong"^gum log --level info "Deployment complete"--time 支持 Go 风格时间布局,也支持 kitchen, ansic, rfc822, rfc1123, datetime 等预设格式。
# Markdown 渲染^gum format -- "# Gum Formats" "- Markdown" "- Code" "- Template" "- Emoji"Gum Formats• Markdown• Code• Template• Emoji# 语法高亮open main.go --raw | ^gum format -t code -l go# Emoji 解析'I :heart: Bubble Gum :candy:' | ^gum format -t emojiformat 默认按 Markdown 渲染;如果内容可能被识别为 flag,记得在正文前加 --。
# 水平拼接^gum join --horizontal "A" "B" "C"# 垂直拼接^gum join --vertical "A" "B" "C"# 配合 style 构建复杂布局let I = (^gum style --padding "1 5" --border double --border-foreground 212 "I")let LOVE = (^gum style --padding "1 4" --border double --border-foreground 57 "LOVE")^gum join --vertical $I $LOVESpin
# 基本用法^gum spin --spinner dot --title "正在安装依赖..." -- bun install# 显示命令输出^gum spin --spinner dot --title "正在安装依赖..." --show-output -- bun installbun install v1.3.10 (30e609e0)Checked 274 installs across 321 packages (no changes) [14.00ms]可用 spinner:line, dot, minidot, jump, pulse, points, globe, moon, monkey, meter, hamburger
为Gum应用Catppuccin主题 🌿
gum 的各子命令支持通过环境变量设置默认样式,避免每次调用都写一堆参数。喜欢 Catppuccin 的可以使用 catppuccin-gum:
wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.sh -O ~/gum-catppuccin.shsource ~/gum-catppuccin.sh mocha lavender可以传入 [latte|frappe|macchiato|mocha] [accent] [highlight],省略参数时默认使用 mocha + lavender,并自动选择互补 highlight。
wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.nu -O ~/gum-catppuccin.nuuse ~/gum-catppuccin.nu apply_gum_themeapply_gum_theme # defaults: mocha + lavenderapply_gum_theme --flavour latte --accent peach # auto-pick complementary highlightapply_gum_theme --accent red --highlight maroon # manually specify highlightNushell 不能直接 source Bash/Zsh 的 .sh 主题脚本,需要使用对应的 .nu 模块;参数都是可选的,并支持 shell auto-completion。
Common GOTCHAs
上面的示例已经能覆盖大多数脚本需求。真正需要注意的是:gum 仍然是传统 CLI,输入输出都是文本;而 Nushell 的管道默认传递结构化数据。这也是大部分坑的来源。
Text 📝
Nushell 中外部命令的输出可以直接赋值给变量,单选和输入类命令通常不会踩坑:let choice = (^gum choose "feat" "fix" "docs" "chore" --header "Choose a commit type")print $choice
当外部命令与 Nushell built-in command/alias 重名时可以用 ^ 来明确调用外部程序。
Gum 本身不一定会重名,但脚本里建议统一写 ^gum。
关于多选
需要注意的是多选。gum 的多选结果是多行文本,Nushell 不会自动把它转成 list,需要自己处理类型:# 多选(--no-limit 或 --limit N)let langs = (^gum choose --no-limit "Rust" "Go" "TypeScript" "Python")let lang_list = ($langs | lines)
反过来,Nushell 的 list 也不能直接 pipe 给 gum。需要先转成多行文本:# ✅ 正确:先 join 成换行分隔的文本let items = ["apple", "banana", "cherry"]let pick = ($items | str join "\n" | ^gum filter)
不然 gum 收到的是 Nu 渲染出来的表格文本,而不是三个独立选项:$ let pick = ($items | ^gum filter)> Filter...• ╭───┬────────╮ │ 0 │ apple │ │ 1 │ banana │ │ 2 │ cherry │ ╰───┴────────╯
如果数据来自 ls、glob 或 record/table,需要先取出字段或显式转成 string:let file = ( glob "**/*.nu" | each { |path| $path | into string } | str join "\n" | ^gum filter --placeholder "Search Nu files")
Exit Code ☑️
在 Shell 中,命令的退出码 0 表示成功,非 0 表示失败;gum 的 confirm 子命令便是利用退出码反映用户的选择结果。
基于退出码,Bash 可以实现两种典型逻辑:
- 当 Command 1 成功 → 执行 Command 2
- 当 Command 1 失败 → 执行 Command 2
下面是对应的写法
# 先执行 command1# ↓# 若退出码为 0(成功)# ↓# 则继续执行 command2command1 && command2# 等价的 if 写法command1if [ $? -eq 0 ]; then command2fi# 先执行 command1# ↓# 若退出码 ≠ 0(失败)# ↓# 则继续执行 command2command1 || command2# 等价的 if 写法command1if [ $? -ne 0 ]; then command2fi而在 Nushell 中,&& / || 这种 Bash 的「基于退出码的短路执行」并不存在。但 Nushell 同样提供了退出码,你可以用普通的 if 判断命令是否成功。
最直接的写法是读取 $env.LAST_EXIT_CODE:
^gum confirm "Delete file?"if $env.LAST_EXIT_CODE == 0 { print "Deleting..."}你也可以使用 () 操作符,Nushell 会依次执行 () 内的语句并返回最终的结果。
因此可以把 ^gum confirm 与 $env.LAST_EXIT_CODE == 0 包裹成一个 expression,让代码变得更紧凑:
if (^gum confirm "Delete file?"; $env.LAST_EXIT_CODE == 0) { print "Deleting..."}也可为其封装一个函数:
def confirm [msg: string] { ^gum confirm $msg $env.LAST_EXIT_CODE == 0}if confirm "Delete file?" { print "Deleting..."}同理,choose、filter、input、write、file、table、spin 等命令如果被用户取消或超时,也应该检查 $env.LAST_EXIT_CODE,否则脚本可能继续拿空字符串往下跑。
写在最后
一开始写这篇 post 的时候,感觉不如直接用 Bash 得了
但相信 Nushell 确实是更好的选择,跨平台 matters