# Bash Tui Menu

> Implements robust interactive Bash TUI menus with dialog widgets, safe selection handling, cancel paths, and non-interactive fallbacks.

- Skill: `paulpas/bash-tui-menu` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/bash-tui-menu`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/bash-tui-menu/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/bash-tui-menu

---





# Bash TUI Menu Builder

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.

## TL;DR for Code Generation

- [ ] Detect `dialog` and an attached TTY before rendering; provide a CLI/env fallback when either is missing.
- [ ] Under `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.
- [ ] Treat exit code `0` as OK/Yes, `1` as Cancel/No, `3` as Extra, and `255` as ESC unless the script intentionally remaps `DIALOG_*` variables.
- [ ] Build menu/checklist/radiolist options as Bash arrays and pass them as `"${options[@]}"`; never concatenate untrusted labels into one command string.
- [ ] Use `trap` for `INT`, `TERM`, and `EXIT` cleanup so temp files and alternate-screen artifacts do not survive aborts.
- [ ] Validate selected tags with a `case` statement or allowlist before running commands; menu text is not authorization.
- [ ] Keep UI functions thin: collect choices in TUI functions, execute side effects in testable worker functions.

---

## When to Use

Use this skill when:

- Building an interactive Bash script that guides users through setup, diagnostics, deployment, backups, or local administration tasks.
- Converting a numbered `read` prompt into a richer terminal UI with `--menu`, `--yesno`, `--inputbox`, `--passwordbox`, `--checklist`, or `--radiolist`.
- Adding a `<More Info>` or third action button with `--extra-button` and handling exit code `3`.
- Capturing multi-field input from forms and password prompts while keeping passwords out of logs.
- Supporting both humans at a terminal and automation in CI through flags, environment variables, or defaults.

## When NOT to Use

Avoid this skill for:

- Scripts that must be strictly POSIX `sh`; `dialog` workflows here rely on Bash arrays, `[[ ]]`, and Bash file-descriptor patterns.
- Non-terminal automation where prompts would hang CI; expose flags or environment variables instead of forcing a TUI.
- Full-screen applications needing long-lived state, mouse-heavy interaction, or custom rendering; use a real TUI framework in Python, Go, Rust, or Node.
- Security-sensitive account creation where root privileges, password hashing, or policy enforcement are not already designed and reviewed.
- Systems where `dialog` cannot be installed and no fallback UX is acceptable.

---

## Core Workflow

1. **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.

2. **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.

3. **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.

4. **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.

5. **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.

6. **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.

---

## Implementation Patterns

### Pattern 1: Dependency Detection, Safe Capture, and Non-Interactive Fallback

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.

```bash
#!/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.

### Pattern 2: BAD vs GOOD Selection Handling

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.

```bash
#!/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)"
```

### Pattern 3: Prompt, Password, and Confirmation Flow

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.

```bash
#!/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 "$@"
```

### Pattern 4: Checklist and Radiolist Builders

`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.

```bash
#!/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 "$@"
```

### Pattern 5: Extra Button, Help Path, Cleanup, and Testability

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.

```bash
#!/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)"
```

---

## Constraints

### MUST DO

- Use `#!/usr/bin/env bash` and `set -euo pipefail` unless integrating into an existing script with a different established shell policy.
- Check `command -v dialog` before rendering and provide install guidance or a non-interactive fallback.
- Capture dialog output and status separately; branch on status first, selected value second.
- Quote every variable expansion and pass menu data through arrays.
- Use stable machine tags (`restart`, `logs`, `prod`) rather than user-facing labels as the values executed by `case` statements.
- Handle `Cancel`, `ESC`, and `Extra` intentionally; every exit code path must be explicit.
- Use `mktemp` for any file capture and remove temp files with `trap`.
- Keep secrets out of `echo`, debug output, process arguments, and dialog summary boxes.

### MUST NOT DO

- Do not source a remote Bash-Dialog file or assume Bash-Dialog provides a library API; the verified repository is a guide and examples for the `dialog` binary.
- Do not save selections to fixed paths such as `/tmp/drinkChoice` in production scripts.
- Do not run menu labels, user input, or selected text with `eval`, unquoted command substitution, or shell interpolation.
- Do not ignore the exit status of `dialog`; Cancel must never trigger the first or default destructive action.
- Do not force interactive dialogs in CI, cron, systemd units, or scripts called with redirected stdin/stdout.
- Do not put more than roughly 7-10 choices in one menu without grouping or adding search/filter behavior.
- Do not use recursive menu calls for long-running loops; prefer `while true` with clear exit paths.

---

## Output Template

When generating or reviewing a Bash TUI menu script, return:

1. **Runtime assumptions** — Bash version needs, `dialog` dependency, TTY/non-interactive behavior.
2. **Menu map** — Each widget, its stable tags, and the action for OK/Cancel/ESC/Extra.
3. **Implementation** — Bash code with strict mode, dependency detection, arrays, traps, and worker functions.
4. **Fallback behavior** — Flags or environment variables that bypass the TUI for automation.
5. **Verification steps** — Commands to run with and without a TTY, plus a ShellCheck command when available.

---

## Live References

> 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.

- [Bash-Dialog repository](https://github.com/RileyMeta/Bash-Dialog)
- [Bash-Dialog README](https://github.com/RileyMeta/Bash-Dialog/blob/main/README.md)
- [All Menus example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/All_Menus/All_Menus.sh)
- [Advanced menu example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Advanced_Examples/Advanced_Example.sh)
- [Advanced Extra Button example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Extra_Button/Advanced_Extra_Button.sh)
- [Dynamic Extra Button example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Extra_Button/Dynamic_Extra.sh)
- [User Creation form example](https://github.com/RileyMeta/Bash-Dialog/blob/main/Examples/Advanced_Examples/User_Creation.sh)

---

## Reference Notes

### Verified from `https://github.com/RileyMeta/Bash-Dialog`

- The repository is a tutorial/example collection for Bash scripts using the `dialog` command; the cloned tree contains `README.md` and example scripts under `Examples/`, not a sourceable Bash library module.
- The main README documents installation commands for Arch (`pacman`), Debian/Ubuntu (`apt`), and Fedora (`dnf`), then demonstrates the minimal widget `dialog --msgbox "Hello World" 0 0`.
- Documented common options include `--backtitle`, `--title`, `--colors`, `--clear`, `--cr-wrap`, `--msgbox`, `--infobox`, `--menu`, `--programbox`, `--pause`, `--yesno`, `--inputbox`, `--passwordbox`, `--help-button`, and `--extra-button`.
- The README describes advanced styling sequences such as `\Zb`/`\ZB` for bold and `\Z0`-`\Z7` for ANSI colors when `--colors` is enabled.
- The README records default dialog-related exit-code variables: OK `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.

### Generic terminal-dialog patterns applied here

- Prefer `--stdout` for simple capture because it is readable and avoids fixed temporary files.
- Prefer `--separate-output` for checklists so each selected tag can be processed line-by-line.
- Prefer dependency and TTY detection at startup because `dialog` blocks or fails in automation contexts.
- Prefer allowlisted tags and worker functions because terminal menus are user input, not trusted execution plans.

---

## Related Skills

| 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. |

