﻿---
title: Use Gum in Nushell Scripts for Better UIUX
date: 2026-03-10
excerpt: Adding interactive TUI components to shell scripts with Gum, and caveats when using it in Nushell.
tags:
  - Shell
  - Nushell
  - TUI
  - UIUX
updated: 2026-05-23 15:36:36
lang: en
i18n:
  cn: /nushell_gum
  translation: 2
---

<script data-swup-reload-script type="module" src="/js/components/tab.js"></script>

The Charm team has released a suite of CLI/TUI tools, including [crush: Glamourous agentic coding for all 💘](https://github.com/charmbracelet/crush) — the predecessor to [[ai_skills|OpenCode]].

This post introduces [Gum](https://github.com/charmbracelet/gum), another CLI tool from Charmbracelet that lets you add interactive components to shell scripts — selection menus, text input, confirmation dialogs, spinners, and more — without writing TUI logic from scratch.

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

Using Gum in Bash/Zsh and other UNIX-compatible shells is straightforward — the official repo has examples, so I won't repeat them here.

Nushell defines its own syntax, so there are some differences when using Gum compared to Bash.

### Text 📝

In Nushell, output from external commands can be directly assigned to a variable — no real gotchas here:

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

> [!NOTE]
> When an external command shares a name with a Nushell built-in command or alias, use `^` to explicitly call the external program.
> It's optional but recommended.

#### Multi-Select

The thing to ==watch out for== is multi-select. The output is multiline text, and Gum's multiline output isn't automatically converted to a list — you need to handle the type conversion yourself:

```nu
# Multi-select (--no-limit or --limit N)
let langs = (gum choose --no-limit "Rust" "Go" "TypeScript" "Python")
let lang_list = ($langs | lines)
```

Similarly, you can't pipe a Nushell list directly to Gum — you need to convert it to multiline text first:

```nu
# ✅ Correct: join into newline-separated text first
let items = ["apple", "banana", "cherry"]
let pick = ($items | str join "\n" | gum filter)
```

Otherwise you'll get output like this:

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

### Exit Code ☑️

In shell scripting, an exit code of `0` means success and any non-`0` value means failure. Gum's `confirm` subcommand uses exit codes to reflect the user's choice.

Based on exit codes, Bash supports two typical patterns:

- When Command 1 **succeeds** → run Command 2
- When Command 1 **fails** → run Command 2

Here are the corresponding patterns:

<x-tabs>

<x-tab title="AND" active>

```bash
# Run command1 first
#         ↓
# If exit code is 0 (success)
#         ↓
# Then run command2
command1 && command2

# Equivalent if-statement
command1
if [ $? -eq 0 ]; then
  command2
fi
```

</x-tab>

<x-tab title="OR">

```bash
# Run command1 first
#         ↓
# If exit code ≠ 0 (failure)
#         ↓
# Then run command2
command1 || command2

# Equivalent if-statement
command1
if [ $? -ne 0 ]; then
  command2
fi
```

</x-tab>

</x-tabs>

In Nushell, Bash-style `&&` / `||` short-circuit execution based on exit codes doesn't exist. But Nushell still provides exit codes — you can use a regular `if` to check whether a command succeeded.

<x-tabs>

<x-tab title="Approach 1" active>

The most direct approach is reading `{nu} $env.LAST_EXIT_CODE`:

```nu
gum confirm;
if $env.LAST_EXIT_CODE == 0 {
    print "Deleting..."
}
```

</x-tab>

<x-tab title="Approach 2">

You can also use the `()` operator. Nushell executes the statements inside `()` sequentially and returns the final result.

So you can wrap `{nu} gum confirm` and `{nu} $env.LAST_EXIT_CODE == 0` in a single block for more compact code:

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

</x-tab>

<x-tab title="Approach 3">

Or wrap it in a helper function:

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

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

</x-tab>

</x-tabs>

## Cheatsheet

### Terminal Output Styling

<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  ║
║                      ║
╚══════════════════════╝
```

Common parameters:

- `--foreground` / `--background`: text/background color (number or hex)
- `--border` / `--border-foreground`: border style (`single`, `double`, `rounded`, etc.) and color
- `--padding` / `--margin`: inner/outer spacing (format: `"top/bottom left/right"` or `"top left/right bottom"`)
- `--align`: text alignment (`left`, `center`, `right`)

</x-tab>

<x-tab title="log">

```nu
# Structured logging with timestamp
gum log --time rfc822 --structured --level debug "Creating file..." name file.txt
# 10 Mar 26 22:55 CST DEBUG Creating file... name=file.txt

# Different log levels
gum log --level error "Something went wrong"
gum log --level info "Deployment complete"
```

Supported `--time` formats: `rfc822`, `unix`, `date`, `date-time`, `datetime`, etc.

</x-tab>

<x-tab title="format">

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

Gum Formats

• Markdown
• Code
• Template
• Emoji

# Syntax highlighting
cat main.go | gum format -t code

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

</x-tab>

<x-tab title="join">

```nu
# Horizontal join
gum join "A" "B" "C"

# Vertical join
gum join --vertical "A" "B" "C"

# Combine with style for complex layouts
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
# Basic usage
gum spin --spinner dot --title "Installing dependencies..." -- bun install

# Show command output
gum spin --spinner dot --title "Installing dependencies..." --show-output -- bun install
bun install v1.3.10 (30e609e0)
Checked 274 installs across 321 packages (no changes) [14.00ms]
```

Available spinners: `line`, `dot`, `minidot`, `jump`, `pulse`, `points`, `globe`, `moon`, `monkey`, `meter`, `hamburger`

## Gum Theming

Gum subcommands support setting default styles via environment variables, so you don't have to pass a pile of flags every time. If you like Catppuccin, follow these steps:

1. Run
   `{sh} wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.sh -O ~/.config/gum/theme.sh`
2. In your script, `{sh} source theme.sh`

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

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

# ...
```

## Closing Thoughts

When I first started writing this post, I felt like ~~why not just use Bash~~

But I genuinely believe Nushell is the better choice — cross-platform matters.