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

<script data-swup-reload-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 逻辑。

### 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>

## Common GOTCHAs

在 Bash/Zsh 等 UNIX-Compatible Shell 里用 gum 很简单，官方仓库也提供了示例，本文不再赘述。

Nushell 定义了一套不同的语法，在使用 gum 时与 Bash 有些许不同。

### Text 📝

Nushell 中外部命令的输出可以直接赋值给变量，基本不会踩坑：

```nu
let choice = ^gum choose "feat" "fix" "docs" "chore" --header "Choose a language"
print $choice
```

>  [!NOTE]
>  当外部命令与 Nushell built-in command/alias重名时可以用 `^` 来明确调用外部程序。
>  加不加都行，但建议加上

#### 关于多选

需要==注意==的就是多选。输出结果是多行文本，gum 的多行文本并不会被自动转成 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)
```

不然就会得到如下输出：

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

### 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;
if $env.LAST_EXIT_CODE == 0 { 
    print "Deleting..."
}
```

</x-tab>

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

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

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

```nu
if (^gum confirm; $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>

## Cheatsheet

### 终端输出美化

<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` 支持的格式：`rfc822`, `unix`, `date`, `date-time`, `datetime` 等。

</x-tab>

<x-tab title="format">

```nu
# Markdown 渲染
gum format -- "# Gum Formats" "- Markdown" "- Code" "- Template" "- Emoji"

Gum Formats

• Markdown
• Code
• Template
• Emoji

# 语法高亮
cat main.go | gum format -t code

# Emoji 解析
echo 'I :heart: Bubble Gum :candy:' | gum format -t emoji
```

</x-tab>

<x-tab title="join">

```nu
# 水平拼接
gum join "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 美化

gum 的各子命令支持通过环境变量设置默认样式，避免每次调用都写一堆参数。喜欢 Catppuccin 的可以参考下面的步骤：

1. 执行
   `{sh} wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.sh -O ~/.config/gum/theme.sh`
2. 在脚本中 `{sh} source theme.sh`

```sh
#!/usr/bin/env bash

[ -f ~/.config/gum/theme.sh ] && source ~/.config/gum/theme.sh

# ...
```

## 写在最后

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

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