Use Gum in Nushell Scripts for Better UIUX

Adding interactive TUI components to shell scripts with Gum, and caveats when using it in Nushell.

The Charm team has released a suite of CLI/TUI tools, including crush: Glamourous agentic coding for all 💘 — the predecessor to OpenCode.

This post introduces 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

brew install gum
scoop install charm-gum
# Archpacman -S gum

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:

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:

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

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

Otherwise you’ll get output like this:

$ 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:

# Run command1 first#         # If exit code is 0 (success)#         # Then run command2command1 && command2# Equivalent if-statementcommand1if [ $? -eq 0 ]; then  command2fi
# Run command1 first#         # If exit code  0 (failure)#         # Then run command2command1 || command2# Equivalent if-statementcommand1if [ $? -ne 0 ]; then  command2fi

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.

The most direct approach is reading $env.LAST_EXIT_CODE:

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

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

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

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

Or wrap it in a helper function:

def confirm [msg: string] {    ^gum confirm $msg    $env.LAST_EXIT_CODE == 0}if confirm "Delete file?" {    print "Deleting..."}

Cheatsheet

Terminal Output Styling

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)
# Structured logging with timestampgum 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 levelsgum log --level error "Something went wrong"gum log --level info "Deployment complete"

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

# Markdown renderinggum format -- "# Gum Formats" "- Markdown" "- Code" "- Template" "- Emoji"Gum Formats Markdown Code Template Emoji# Syntax highlightingcat main.go | gum format -t code# Emoji parsingecho 'I :heart: Bubble Gum :candy:' | gum format -t emoji
# Horizontal joingum join "A" "B" "C"# Vertical joingum join --vertical "A" "B" "C"# Combine with style for complex layoutslet 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

Spin

# Basic usagegum spin --spinner dot --title "Installing dependencies..." -- bun install# Show command outputgum spin --spinner dot --title "Installing dependencies..." --show-output -- bun installbun 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
    wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.sh -O ~/.config/gum/theme.sh
  2. In your script, source theme.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.

CompactRelaxed
Normal1.70