Use Gum in Nushell Scripts for Better UIUX
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 gumscoop install charm-gum# Archpacman -S gumCommon 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
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 command2fiIn 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 $LOVESpin
# 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:
- Run
wget https://raw.githubusercontent.com/holo96/catppuccin-gum/refs/heads/main/gum-catppuccin.sh -O ~/.config/gum/theme.sh - 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.