executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
bash-tui-menuImplements robust interactive Bash TUI menus with dialog widgets, safe selection handling, cancel paths, and non-interactive fallbacks.
Independent GitHub snapshot · Automated classification · Original documentation governs
name: bash-tui-menu description: Implements robust interactive Bash TUI menus with dialog widgets, safe selection handling, cancel paths, and non-interactive fallbacks. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding role: implementation scope: implementation output-format: code content-types:
Implements production-ready interactive Bash terminal interfaces using the dialog command and the concrete Bash-Dialog example patterns. This skill makes the model design menus that are safe under set -euo pipefail, preserve cancel/ESC semantics, clean up the terminal, and remain scriptable through non-interactive fallbacks.
dialog and an attached TTY before rendering; provide a CLI/env fallback when either is missing.set -e, capture widget output inside an if choice=$(dialog --stdout ...); then status=0; else status=$?; fi block so Cancel, ESC, Extra, and errors cannot abort before status handling.0 as OK/Yes, 1 as Cancel/No, 3 as Extra, and 255 as ESC unless the script intentionally remaps DIALOG_* variables."${options[@]}"; never concatenate untrusted labels into one command string.trap for INT, TERM, and EXIT cleanup so temp files and alternate-screen artifacts do not survive aborts.case statement or allowlist before running commands; menu text is not authorization.Use this skill when:
read prompt into a richer terminal UI with --menu, --yesno, --inputbox, --passwordbox, --checklist, or --radiolist.<More Info> or third action button with --extra-button and handling exit code 3.Avoid this skill for:
sh; dialog workflows here rely on Bash arrays, [[ ]], and Bash file-descriptor patterns.dialog cannot be installed and no fallback UX is acceptable.Prove the Runtime Can Render a TUI — Check that dialog exists, stdin/stdout connect to a terminal, and the script is not running in CI/non-interactive mode. Checkpoint: If any check fails, the script must use a deterministic fallback or exit with a clear setup message.
Model Choices as Data — Store tags, labels, and defaults in arrays before calling dialog. For checklist/radiolist widgets, include the required status field (on/off). Checkpoint: The command line must pass array elements with "${items[@]}" so spaces and punctuation in labels remain intact.
Render One Widget and Capture Both Result Channels — Use --stdout for simple capture, or 3>&1 1>&2 2>&3 when stdout must remain available for status text. In strict-mode scripts, put the capture assignment in an if condition and set status=0 or status=$? inside the corresponding branch. Checkpoint: Cancel, ESC, Extra, and error statuses are captured before any clear, echo, function call, or side effect runs.
Route by Exit Code Before Acting on Data — First branch on OK/Cancel/Extra/ESC/error, then branch on the selected tag. Checkpoint: Cancel and ESC must be intentional paths, not accidental fall-through to the default action.
Execute Side Effects Outside the UI Layer — TUI functions return tags or normalized values; worker functions perform installation, file changes, or command execution. Checkpoint: Worker functions can be tested without dialog by passing arguments directly.
Clean Up and Restore the Terminal — Use mktemp, trap, clear, and local variables. Checkpoint: Temporary files are deleted on normal exit, Cancel, ESC, Ctrl+C, and command failure.
Use this pattern for scripts that should be pleasant for humans but still safe in CI, cron, SSH commands without TTY allocation, or automation. It uses --stdout, as shown in Bash-Dialog's advanced example, because that is easier to read than saving output to a fixed file.
#!/usr/bin/env bash
set -euo pipefail
readonly APP_TITLE="Server Maintenance"
has_interactive_tty() {
[[ -t 0 && -t 1 ]]
}
require_dialog_or_fallback() {
if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then
return 1
fi
if ! has_interactive_tty; then
return 1
fi
command -v dialog >/dev/null 2>&1
}
choose_maintenance_action() {
local default_action="${MAINTENANCE_ACTION:-status}"
if ! require_dialog_or_fallback; then
printf '%s\n' "$default_action"
return 0
fi
local options=(
status "Show service status"
logs "Open recent logs"
restart "Restart service"
exit "Exit without changes"
)
local choice
local status
if choice=$(dialog --clear \
--backtitle "$APP_TITLE" \
--title "Main Menu" \
--cancel-label "Exit" \
--stdout \
--menu "Choose a maintenance action:" 0 0 4 \
"${options[@]}"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$choice" ;;
1|255) printf '%s\n' "exit" ;;
*) printf 'ERROR: dialog failed with exit code %s\n' "$status" >&2; return 1 ;;
esac
}
run_maintenance_action() {
local action="$1"
case "$action" in
status) systemctl status my-app.service --no-pager ;;
logs) journalctl -u my-app.service -n 100 --no-pager ;;
restart) systemctl restart my-app.service ;;
exit) return 0 ;;
*) printf 'ERROR: unsupported action: %s\n' "$action" >&2; return 64 ;;
esac
}
main() {
local action
action=$(choose_maintenance_action)
run_maintenance_action "$action"
}
main "$@"
Why this works: UI capability is parsed at the boundary. Internal logic receives one trusted action string and can fail fast if an impossible tag appears.
The Bash-Dialog examples show two capture styles: 2>&1 >/dev/tty and --stdout. Both are valid, but they become unsafe when the script ignores exit codes, writes to fixed temp files, or runs selected text as code.
#!/usr/bin/env bash
set -euo pipefail
# ❌ BAD — fixed temp file, ignores cancel, unquoted read, and fall-through action.
dialog --menu "Choose:" 15 50 4 \
1 "Restart service" \
2 "Delete cache" \
2>/tmp/menu-choice
choice=$(</tmp/menu-choice)
if [[ $choice == 1 ]]; then
systemctl restart my-app.service
else
rm -rf /var/cache/my-app/*
fi
# ✅ GOOD — result and status captured atomically, all branches explicit.
choose_action() {
local choice
local status
if choice=$(dialog --stdout --menu "Choose:" 15 50 4 \
restart "Restart service" \
purge-cache "Delete cache" \
back "Return to previous menu"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$choice" ;;
1) printf '%s\n' "back" ;;
255) printf '%s\n' "back" ;;
*) printf 'ERROR: dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
}
dispatch_action() {
local choice="$1"
case "$choice" in
restart) systemctl restart my-app.service ;;
purge-cache) find /var/cache/my-app -mindepth 1 -maxdepth 1 -delete ;;
back) return 0 ;;
*) printf 'ERROR: invalid menu choice: %s\n' "$choice" >&2; return 64 ;;
esac
}
dispatch_action "$(choose_action)"
Use input widgets for data collection, then parse and validate before executing. Bash-Dialog's user-creation example captures --form output with 3>&1 1>&2 2>&3 3>&- and uses --passwordbox --insecure; production code should avoid echoing secrets and should validate every field before continuing.
#!/usr/bin/env bash
set -euo pipefail
prompt_required_text() {
local title="$1"
local prompt="$2"
local value
local status
if value=$(dialog --clear --stdout \
--title "$title" \
--inputbox "$prompt" 10 60 ""); then
status=0
else
status=$?
fi
case "$status" in
0) [[ -n "$value" ]] || { printf 'ERROR: value cannot be empty\n' >&2; return 64; }
printf '%s\n' "$value" ;;
1|255) return 130 ;;
*) printf 'ERROR: input dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
}
prompt_secret() {
local secret
local status
if secret=$(dialog --clear --stdout \
--title "Credentials" \
--passwordbox "Enter the API token:" 10 60); then
status=0
else
status=$?
fi
case "$status" in
0) [[ -n "$secret" ]] || { printf 'ERROR: token cannot be empty\n' >&2; return 64; }
printf '%s\n' "$secret" ;;
1|255) return 130 ;;
*) printf 'ERROR: password dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
}
confirm_write() {
local username="$1"
dialog --clear \
--title "Confirm" \
--yes-label "Write" \
--no-label "Cancel" \
--yesno "Write configuration for ${username}?" 8 60
}
main() {
local username token
username=$(prompt_required_text "Profile" "Enter the username:") || exit $?
token=$(prompt_secret) || exit $?
if confirm_write "$username"; then
install -m 600 /dev/null "$HOME/.my-app-token"
printf '%s\n' "$token" > "$HOME/.my-app-token"
dialog --msgbox "Configuration saved for ${username}." 7 50
else
dialog --msgbox "No files were changed." 7 40
fi
}
main "$@"
dialog --checklist and dialog --radiolist require triples: tag, item text, and on/off status. Keep those triples in arrays. Checklists return selected tags, commonly quoted and separated by spaces, so only use stable tags without spaces or normalize with --separate-output when processing multiple selections.
#!/usr/bin/env bash
set -euo pipefail
select_components() {
local items=(
nginx "Install nginx reverse proxy" on
certbot "Install certbot TLS tooling" off
logrotate "Install log rotation config" on
firewall "Enable firewall profile" off
)
local selected
local status
if selected=$(dialog --clear --stdout \
--separate-output \
--title "Components" \
--checklist "Select components to install:" 18 72 8 \
"${items[@]}"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$selected" ;;
1|255) return 130 ;;
*) printf 'ERROR: checklist failed: %s\n' "$status" >&2; return 1 ;;
esac
}
select_environment() {
local choices=(
dev "Developer workstation" on
staging "Shared staging server" off
prod "Production server" off
)
local environment
local status
if environment=$(dialog --clear --stdout \
--title "Environment" \
--radiolist "Choose exactly one target:" 15 64 4 \
"${choices[@]}"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$environment" ;;
1|255) return 130 ;;
*) printf 'ERROR: radiolist failed: %s\n' "$status" >&2; return 1 ;;
esac
}
main() {
local environment
environment=$(select_environment) || exit $?
printf 'Environment: %s\n' "$environment"
local components_output
local components_status
if components_output=$(select_components); then
components_status=0
else
components_status=$?
fi
case "$components_status" in
0)
while IFS= read -r component; do
case "$component" in
nginx|certbot|logrotate|firewall) printf 'Selected component: %s\n' "$component" ;;
"") ;;
*) printf 'ERROR: unknown component: %s\n' "$component" >&2; exit 64 ;;
esac
done <<< "$components_output"
;;
130) exit 130 ;;
*) printf 'ERROR: checklist failed: %s\n' "$components_status" >&2; exit 1 ;;
esac
}
main "$@"
Bash-Dialog's Extra Button examples verify that --extra-button --extra-label returns exit code 3. Use that code for contextual help or details; do not overload Cancel or force users to select an item just to learn what it does.
#!/usr/bin/env bash
set -euo pipefail
readonly TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_DIR"
if [[ -t 1 ]]; then
clear
fi
}
trap cleanup EXIT INT TERM
show_plan_help() {
local selected="${1:-}"
case "$selected" in
quick) dialog --msgbox "Quick runs only cheap checks." 7 50 ;;
full) dialog --msgbox "Full runs every diagnostic and may take several minutes." 8 60 ;;
*) dialog --msgbox "Highlight a plan, then choose More Info." 7 55 ;;
esac
}
choose_plan() {
local choice status
while true; do
if choice=$(dialog --clear \
--backtitle "Diagnostics" \
--title "Run Plan" \
--extra-button --extra-label "More Info" \
--cancel-label "Exit" \
--stdout \
--menu "Choose a diagnostic plan:" 14 68 4 \
quick "Fast status checks" \
full "Complete diagnostics" \
exit "Leave without running checks"); then
status=0
else
status=$?
fi
case "$status" in
0) printf '%s\n' "$choice"; return 0 ;;
3) show_plan_help "$choice" ;;
1|255) printf '%s\n' "exit"; return 0 ;;
*) printf 'ERROR: dialog failed: %s\n' "$status" >&2; return 1 ;;
esac
done
}
run_plan() {
local plan="$1"
case "$plan" in
quick) printf 'Running quick diagnostics\n' ;;
full) printf 'Running full diagnostics\n' ;;
exit) return 0 ;;
*) printf 'ERROR: invalid plan: %s\n' "$plan" >&2; return 64 ;;
esac
}
run_plan "$(choose_plan)"
#!/usr/bin/env bash and set -euo pipefail unless integrating into an existing script with a different established shell policy.command -v dialog before rendering and provide install guidance or a non-interactive fallback.restart, logs, prod) rather than user-facing labels as the values executed by case statements.Cancel, ESC, and Extra intentionally; every exit code path must be explicit.mktemp for any file capture and remove temp files with trap.echo, debug output, process arguments, and dialog summary boxes.dialog binary./tmp/drinkChoice in production scripts.eval, unquoted command substitution, or shell interpolation.dialog; Cancel must never trigger the first or default destructive action.while true with clear exit paths.When generating or reviewing a Bash TUI menu script, return:
dialog dependency, TTY/non-interactive behavior.Repository-standard links checked against the local
SKILL_FORMAT_SPEC.md; these links preserve the Bash-Dialog research trail without treating Bash-Dialog as a sourceable library.
https://github.com/RileyMeta/Bash-Dialogdialog command; the cloned tree contains README.md and example scripts under Examples/, not a sourceable Bash library module.pacman), Debian/Ubuntu (apt), and Fedora (dnf), then demonstrates the minimal widget dialog --msgbox "Hello World" 0 0.--backtitle, --title, --colors, --clear, --cr-wrap, --msgbox, --infobox, --menu, --programbox, --pause, --yesno, --inputbox, --passwordbox, --help-button, and --extra-button.\Zb/\ZB for bold and \Z0-\Z7 for ANSI colors when --colors is enabled.0, Cancel 1, Help 2, Extra 3, ESC 255, and Error -1 through DIALOG_* environment variables.Examples/All_Menus/All_Menus.sh demonstrates --menu, --msgbox, --yesno, --inputbox, --passwordbox, --infobox, --textbox, --checklist, --radiolist, --gauge, --calendar, --timebox, --fselect, --dselect, and --form.Examples/Advanced_Examples/Advanced_Example.sh demonstrates a while true main menu, --stdout, --cancel-label, trap 'aborted' INT, and routing selected tags through nested case statements.Examples/Extra_Button/Advanced_Extra_Button.sh and Examples/Extra_Button/Dynamic_Extra.sh demonstrate --extra-button --extra-label and route exit code 3 separately from OK and Cancel.Examples/Advanced_Examples/User_Creation.sh demonstrates --form capture with file descriptor swapping and --passwordbox for hidden input.--stdout for simple capture because it is readable and avoids fixed temporary files.--separate-output for checklists so each selected tag can be processed line-by-line.dialog blocks or fails in automation contexts.| Skill | Purpose |
|---|---|
shell-parameter-expansion | Use safe defaults and fail-fast required variables in Bash scripts that feed menu labels and fallback values. |
shell-command-chaining | Build reliable validation gates, fallback chains, and explicit error paths around dialog-driven actions. |
shell-process-management | Add traps, cleanup, background process handling, and signal-aware termination to long-running TUI scripts. |
output-formatting | Shape generated script output and user-facing summaries consistently after interactive choices are made. |