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.

Gum is straightforward in Bash/Zsh and other UNIX-compatible shells, and the official repository already provides examples. It also works nicely in Nushell, but Nu’s structured data pipeline and different error/exit-code model mean a few patterns need to be written differently.

This post starts with copyable Nushell examples, then covers the common gotchas.

Install

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

Cheatsheet

All examples below run in Nushell. I recommend using ^gum to explicitly call the external command, so the script will not be affected by future Nu built-ins, aliases, or custom functions with the same name.

Interactive Input

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

choose is useful for fixed options such as commit types, environments, or task names.

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

filter is better for longer lists that need search. A Nu list must be converted to newline-separated text first; the Gotchas section explains why.

# Single-line inputlet name = (^gum input --placeholder "Project name")# Multi-line inputlet notes = (^gum write --header "Release notes" --show-line-numbers)

Use input for short text and write for multiline content such as release notes, commit bodies, or descriptions.

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

The meaningful result of confirm is its exit code: Yes exits with 0; No or cancel exits with a non-0 code.

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, either number or hex
  • --border / --border-foreground: border style (single, double, rounded, etc.) and color
  • --padding / --margin: inner/outer spacing, such as "top/bottom left/right" or "top left/right bottom"
  • --align: text alignment (left, center, right)
# Structured logging with a 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"

--time supports Go-style time layouts and preset formats such as kitchen, ansic, rfc822, rfc1123, and datetime.

# Markdown rendering^gum format -- "# Gum Formats" "- Markdown" "- Code" "- Template" "- Emoji"Gum Formats Markdown Code Template Emoji# Syntax highlightingopen main.go --raw | ^gum format -t code -l go# Emoji parsing'I :heart: Bubble Gum :candy:' | ^gum format -t emoji

format renders Markdown by default. If the content may be parsed as flags, put -- before the text.

# Horizontal join^gum join --horizontal "A" "B" "C"# Vertical join^gum join --vertical "A" "B" "C"# Combine with style for more 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 usage^gum spin --spinner dot --title "Installing dependencies..." -- bun install# Show command output^gum 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

Apply Catppuccin Theme to Gum 🌿

Gum subcommands support default styles through environment variables, so you do not have to pass a pile of flags every time. If you like Catppuccin, use 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

You can pass [latte|frappe|macchiato|mocha] [accent] [highlight]. If omitted, it defaults to mocha + lavender and automatically picks a complementary highlight color.

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 highlight

Nushell cannot directly source the Bash/Zsh .sh theme script. Use the .nu module instead; all parameters are optional and support shell autocompletion.

Common GOTCHAs

The examples above cover most scripting needs. The real thing to remember is that Gum is still a traditional CLI: its input and output are text. Nushell pipelines, on the other hand, carry structured data by default. Most gotchas come from that boundary.

Text 📝

In Nushell, output from external commands can be assigned directly to variables. Single-choice and input-style commands are usually straightforward:

let choice = (^gum choose "feat" "fix" "docs" "chore" --header "Choose a commit type")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.
Gum itself may not conflict today, but reusable scripts should consistently use ^gum.

Multi-Select

Multi-select needs special care. Gum returns multiline text, and Nushell does not automatically convert that output to a list:

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

The reverse is also true: do not pipe a Nushell list directly into Gum. 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, Gum receives Nushell’s rendered table text instead of three independent options:

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

If the data comes from ls, glob, or a record/table, extract a field or explicitly convert values to strings first:

let file = (    glob "**/*.nu"    | each { |path| $path | into string }    | str join "\n"    | ^gum filter --placeholder "Search Nu files")

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 represent 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 Bash 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 does not exist. But Nushell still provides the exit code, so you can use a regular if.

The most direct approach is reading $env.LAST_EXIT_CODE:

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

You can also use (). 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 expression:

if (^gum confirm "Delete file?"; $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..."}

Similarly, if commands such as choose, filter, input, write, file, table, or spin can be cancelled or timed out, check $env.LAST_EXIT_CODE; otherwise the script may continue with an empty string.

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.