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.
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 gumscoop install charm-gum# Archpacman -S gumCheatsheet
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 $choicechoose 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 $pickfilter 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 emojiformat 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 $LOVESpin
# 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 lavenderYou 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 highlightNushell 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
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 command2fiIn 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.