﻿---
title: Use gum in Nushell Script for better UIUX
date: 2026-03-10
excerpt: 使用 Gum 为Shell脚本添加交互式 TUI 组件，以及在NuShell使用它的注意事项
tags:
  - Shell
  - Nushell
  - TUI
  - UIUX
updated: 2026-07-05 22:26:03
i18n:
  en: /en/nushell_gum
translation: 2
---

<script type="module" src="/js/components/tab.js"></script>

Charm 团队推出了一系列 CLI/TUI 工具，其中就包括 [[ai_skills|OpenCode]] 的前身[crush: Glamourous agentic coding for all 💘](https://github.com/charmbracelet/crush)

本文要介绍的 [Gum](https://github.com/charmbracelet/gum) 是 Charmbracelet 推出的另一个 CLI 工具，让你在 Shell 脚本中轻松添加交互式组件——选择菜单、文本输入、确认弹窗、加载动画等，不用手写 TUI 逻辑。

在 Bash/Zsh 等 UNIX-Compatible Shell 里用 gum 很简单，官方仓库也提供了示例。Nushell 也可以很顺手地使用 gum，只是因为 Nu 有结构化数据和不同的错误/退出码模型，少数写法需要换一种思路。

本文先给一组可以直接复制的 Nushell 写法，再集中解释容易踩坑的地方。

## Install

<x-tabs>

<x-tab title="macOS" active>

```bash
brew install gum
```

</x-tab>

<x-tab title="Windows">

```bash
scoop install charm-gum
```

</x-tab>

<x-tab title="Linux">

```bash
# Arch
pacman -S gum
```

</x-tab>

</x-tabs>

## Cheatsheet

下面的示例都在 Nushell 中运行。建议用 `^gum` 明确调用外部命令，避免将来与 Nu 内置命令、alias 或自定义函数重名。

### 交互输入

<x-tabs>

<x-tab title="choose" active>

```nu
let choice = (^gum choose "feat" "fix" "docs" "chore" --header "Choose a commit type")
print $choice
```

`choose` 适合固定选项，例如选择 commit type、环境、任务名称等。

</x-tab>

<x-tab title="filter">

```nu
let items = ["apple", "banana", "cherry"]
let pick = ($items | str join "\n" | ^gum filter --placeholder "Search fruit")
print $pick
```

`filter` 适合列表较长、需要搜索的场景。Nu 的 list 需要先转成换行分隔的文本，后面的 Gotchas 会详细解释。

</x-tab>

<x-tab title="input/write">

```nu
# 单行输入
let name = (^gum input --placeholder "Project name")

# 多行输入
let notes = (^gum write --header "Release notes" --show-line-numbers)
```

`input` 用来收集短文本，`write` 用来写多行内容，比如 release notes、commit body、说明文字等。

</x-tab>

<x-tab title="confirm">

```nu
if (^gum confirm "Delete file?"; $env.LAST_EXIT_CODE == 0) {
    print "Deleting..."
}
```

`confirm` 的核心结果在退出码里：选择 Yes 时退出码是 `0`，选择 No 或取消时为非 `0`。

</x-tab>

</x-tabs>

### 终端输出美化

<x-tabs>

<x-tab title="style" active>

```nu
^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`）

</x-tab>

<x-tab title="log">

```nu
# 带时间戳的结构化日志
^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` 等预设格式。

</x-tab>

<x-tab title="format">

```nu
# 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 emoji
```

`format` 默认按 Markdown 渲染；如果内容可能被识别为 flag，记得在正文前加 `--`。

</x-tab>

<x-tab title="join">

```nu
# 水平拼接
^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 $LOVE
```

</x-tab>

</x-tabs>

### Spin

```nu
# 基本用法
^gum spin --spinner dot --title "正在安装依赖..." -- bun install

# 显示命令输出
^gum spin --spinner dot --title "正在安装依赖..." --show-output -- bun install
bun 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](https://github.com/holo96/catppuccin-gum)：

<x-tabs>

<x-tab title="Bash/Zsh" active>

```sh
wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.sh -O ~/gum-catppuccin.sh
source ~/gum-catppuccin.sh mocha lavender
```

可以传入 `[latte|frappe|macchiato|mocha] [accent] [highlight]`，省略参数时默认使用 `mocha` + `lavender`，并自动选择互补 highlight。

</x-tab>

<x-tab title="Nushell">

```nu
wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.nu -O ~/gum-catppuccin.nu

use ~/gum-catppuccin.nu apply_gum_theme
apply_gum_theme                                      # defaults: mocha + lavender
apply_gum_theme --flavour latte --accent peach       # auto-pick complementary highlight
apply_gum_theme --accent red --highlight maroon      # manually specify highlight
```

Nushell 不能直接 `source` Bash/Zsh 的 `.sh` 主题脚本，需要使用对应的 `.nu` 模块；参数都是可选的，并支持 shell auto-completion。

</x-tab>

</x-tabs>

## Common GOTCHAs

上面的示例已经能覆盖大多数脚本需求。真正需要注意的是：gum 仍然是传统 CLI，输入输出都是文本；而 Nushell 的管道默认传递结构化数据。这也是大部分坑的来源。

### Text 📝

Nushell 中外部命令的输出可以直接赋值给变量，单选和输入类命令通常不会踩坑：

```nu
let choice = (^gum choose "feat" "fix" "docs" "chore" --header "Choose a commit type")
print $choice
```

> [!NOTE]
> 当外部命令与 Nushell built-in command/alias 重名时可以用 `^` 来明确调用外部程序。
> Gum 本身不一定会重名，但脚本里建议统一写 `^gum`。

#### 关于多选

需要注意的是多选。gum 的多选结果是多行文本，Nushell 不会自动把它转成 list，需要自己处理类型：

```nu
# 多选（--no-limit 或 --limit N）
let langs = (^gum choose --no-limit "Rust" "Go" "TypeScript" "Python")
let lang_list = ($langs | lines)
```

反过来，Nushell 的 list 也不能直接 pipe 给 gum。需要先转成多行文本：

```nu
# ✅ 正确：先 join 成换行分隔的文本
let items = ["apple", "banana", "cherry"]
let pick = ($items | str join "\n" | ^gum filter)
```

不然 gum 收到的是 Nu 渲染出来的表格文本，而不是三个独立选项：

```nu
$ let pick = ($items | ^gum filter)
> Filter...
• ╭───┬────────╮
  │ 0 │ apple  │
  │ 1 │ banana │
  │ 2 │ cherry │
  ╰───┴────────╯
```

如果数据来自 `ls`、`glob` 或 record/table，需要先取出字段或显式转成 string：

```nu
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

下面是对应的写法

<x-tabs>

<x-tab title="AND" active>

```bash
# 先执行 command1
#         ↓
# 若退出码为 0（成功）
#         ↓
# 则继续执行 command2
command1 && command2

# 等价的 if 写法
command1
if [ $? -eq 0 ]; then
  command2
fi
```

</x-tab>

<x-tab title="OR">

```bash
# 先执行 command1
#         ↓
# 若退出码 ≠ 0（失败）
#         ↓
# 则继续执行 command2
command1 || command2

# 等价的 if 写法
command1
if [ $? -ne 0 ]; then
  command2
fi
```

</x-tab>

</x-tabs>

而在 Nushell 中，`&&` / `||` 这种 Bash 的「基于退出码的短路执行」并不存在。但 Nushell 同样提供了退出码，你可以用普通的 `if` 判断命令是否成功。

<x-tabs>

<x-tab title="写法 1" active>

最直接的写法是读取 `{nu} $env.LAST_EXIT_CODE`：

```nu
^gum confirm "Delete file?"
if $env.LAST_EXIT_CODE == 0 {
    print "Deleting..."
}
```

</x-tab>

<x-tab title="写法 2">

你也可以使用 `()` 操作符，Nushell 会依次执行 `()` 内的语句并返回最终的结果。

因此可以把 `{nu} ^gum confirm` 与 `{nu} $env.LAST_EXIT_CODE == 0` 包裹成一个 expression，让代码变得更紧凑：

```nu
if (^gum confirm "Delete file?"; $env.LAST_EXIT_CODE == 0) {
    print "Deleting..."
}
```

</x-tab>

<x-tab title="写法 3">

也可为其封装一个函数：

```nu
def confirm [msg: string] {
    ^gum confirm $msg
    $env.LAST_EXIT_CODE == 0
}

if confirm "Delete file?" {
    print "Deleting..."
}
```

</x-tab>

</x-tabs>

同理，`choose`、`filter`、`input`、`write`、`file`、`table`、`spin` 等命令如果被用户取消或超时，也应该检查 `$env.LAST_EXIT_CODE`，否则脚本可能继续拿空字符串往下跑。

## 写在最后

一开始写这篇 post 的时候，感觉~~不如直接用 Bash 得了~~

但相信 Nushell 确实是更好的选择，跨平台 matters
