A visual gallery of every built-in widget: what it looks like, the code that produces it, and what you get back.
Ask a yes/no question with optional detail lines summarising what will happen.
+--------------------------------------------+
| |
| api-server restart |
| cache flush |
| workers stop |
| |
| Deploy to production? |
| |
| [ Yes ] [ No ] |
| |
+--------------------------------------------+
←/→ select y/n quick Enter confirm
source shellframe.sh
shellframe_confirm "Deploy to production?" \
" api-server restart" \
" cache flush" \
" workers stop"
(( $? == 0 )) && deploy || echo "Cancelled."Returns: 0 = Yes, 1 = No / Esc / q
Show a non-blocking message the user must dismiss — handy after a long background operation finishes.
+--------------------------------------------+
| |
| api-server v2.4.1 done |
| cache cleared |
| workers 3 restarted |
| |
| Deployment complete. |
| |
| [ OK — any key ] |
| |
+--------------------------------------------+
any key to dismiss
source shellframe.sh
shellframe_alert "Deployment complete." \
" api-server v2.4.1 done" \
" cache cleared" \
" workers 3 restarted"Returns: always 0 after the user dismisses.
Each item has a set of named actions; the user cycles through them with
Space / → and confirms the whole list with Enter. Great for "what do you
want to do with each of these?" workflows.
Deploy to Production
────────────────────────────────────────────────────────
api-server [ restart ]
database [ -------- ]
> cache [ flush ]
workers [ restart ]
cdn [ -------- ]
────────────────────────────────────────────────────────
↑/↓ move Space/→ cycle action Enter confirm q quit
source shellframe.sh
SHELLFRAME_AL_LABELS=("api-server" "database" "cache" "workers" "cdn")
SHELLFRAME_AL_ACTIONS=(
"nothing restart"
"nothing"
"nothing flush"
"nothing restart"
"nothing"
)
SHELLFRAME_AL_IDX=(0 0 0 0 0)
SHELLFRAME_AL_META=("" "" "" "" "")
_draw_row() {
local i="$1" label="$2" acts_str="$3" aidx="$4" meta="$5"
local cursor=" "
(( i == SHELLFRAME_AL_SELECTED )) && cursor="> "
local -a acts; IFS=' ' read -r -a acts <<< "$acts_str"
local action="${acts[$aidx]}"
printf "%b%-14s [ %-8s]\n" "$cursor" "$label" "$action"
}
shellframe_action_list "_draw_row" "" \
"↑/↓ move Space/→ cycle Enter confirm q quit"
if (( $? == 0 )); then
for i in "${!SHELLFRAME_AL_LABELS[@]}"; do
IFS=' ' read -r -a acts <<< "${SHELLFRAME_AL_ACTIONS[$i]}"
action="${acts[${SHELLFRAME_AL_IDX[$i]}]}"
[[ "$action" != "nothing" ]] && \
printf "%s → %s\n" "${SHELLFRAME_AL_LABELS[$i]}" "$action"
done
fiReturns: 0 = confirmed, 1 = quit / Esc. Per-row action indices are in
SHELLFRAME_AL_IDX[@].
A cursor-driven list that fits any region. Cursor highlights with reverse-video; scroll state is maintained automatically.
┌─────────────────────────────┐
│ bash-completion │
│ curl │
│ git ← here │
│ jq │
│ ripgrep │
│ tmux │
└─────────────────────────────┘
↑/↓ move Enter select q quit
source shellframe.sh
SHELLFRAME_LIST_ITEMS=("bash-completion" "curl" "git" "jq" "ripgrep" "tmux")
SHELLFRAME_LIST_CTX="pkgs"
shellframe_list_init "pkgs" 10 # ctx, visible-row count
shellframe_screen_enter
shellframe_raw_enter
shellframe_cursor_hide
trap 'shellframe_raw_exit; shellframe_cursor_show; shellframe_screen_exit' EXIT
while true; do
shellframe_list_render 1 1 32 8
shellframe_read_key key
shellframe_list_on_key "$key"
rc=$?
(( rc == 2 )) && break # Enter
[[ "$key" == q ]] && { rc=1; break; }
done
if (( rc == 2 )); then
idx=$(shellframe_sel_cursor "pkgs")
printf "Selected: %s\n" "${SHELLFRAME_LIST_ITEMS[$idx]}"
fiReturns: cursor position via shellframe_sel_cursor; exit code 0 =
confirmed, 1 = cancelled.
Same widget, SHELLFRAME_LIST_MULTISELECT=1 turns Space into a toggle.
Selected rows get a [✓] prefix by convention (rendered by your draw logic
or the default renderer).
┌─────────────────────────────┐
│ [✓] bash-completion │
│ [ ] curl │
│ [✓] git │
│ [ ] jq │
│ [✓] ripgrep ← here │
│ [ ] tmux │
└─────────────────────────────┘
↑/↓ move Space toggle Enter confirm
source shellframe.sh
SHELLFRAME_LIST_ITEMS=("bash-completion" "curl" "git" "jq" "ripgrep" "tmux")
SHELLFRAME_LIST_CTX="pkgs"
SHELLFRAME_LIST_MULTISELECT=1
shellframe_list_init "pkgs" 10 # ctx, visible-row count
# ... same render loop as single-select above ...
selected=$(shellframe_sel_selected "pkgs") # space-separated indices
for i in $selected; do
printf "Install: %s\n" "${SHELLFRAME_LIST_ITEMS[$i]}"
doneReturns: selected indices via shellframe_sel_selected "ctx" (space-separated).
A centered overlay with a message, an embedded text field, and labelled buttons. Good for rename, add-item, and any short-answer prompts.
┌── Rename ───────────────────────────────────┐
│ │
│ New name for "report.csv": │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ Q4_report_final▌ │ │
│ └───────────────────────────────────────┘ │
│ │
│ [ OK ] [ Cancel ] │
└─────────────────────────────────────────────┘
source shellframe.sh
SHELLFRAME_MODAL_TITLE="Rename"
SHELLFRAME_MODAL_MESSAGE='New name for "report.csv":'
SHELLFRAME_MODAL_BUTTONS=("OK" "Cancel")
SHELLFRAME_MODAL_INPUT=1
SHELLFRAME_MODAL_FOCUSED=1
shellframe_modal_init
shellframe_screen_enter
shellframe_raw_enter
shellframe_cursor_hide
trap 'shellframe_raw_exit; shellframe_cursor_show; shellframe_screen_exit' EXIT
cols=$(tput cols); rows=$(tput lines)
while true; do
shellframe_modal_render 1 1 "$cols" "$rows"
shellframe_read_key key
shellframe_modal_on_key "$key"
(( $? == 2 )) && break
done
if (( SHELLFRAME_MODAL_RESULT == 0 )); then
name=$(shellframe_cur_text "${SHELLFRAME_MODAL_INPUT_CTX}")
printf "Rename to: %s\n" "$name"
fiReturns: SHELLFRAME_MODAL_RESULT — button index on Enter (0 = first
button), -1 on Esc / cancel. Input text via shellframe_cur_text.
A full-region text editor with soft word-wrap and standard editing keys. Useful for commit messages, notes, or any multi-line free-text field.
┌─ Notes ──────────────────────────────────────────────────┐
│ Deploy the api-server first, then flush the cache. │
│ Workers can be restarted in parallel. │
│ │
│ Do NOT restart the database during business hours.▌ │
│ │
│ │
└──────────────────────────────────────────────────────────┘
Ctrl-D submit Ctrl-C cancel Ctrl-K kill line
source shellframe.sh
SHELLFRAME_EDITOR_CTX="notes"
SHELLFRAME_EDITOR_WRAP=1 # soft word-wrap (0 = horizontal scroll)
shellframe_editor_init "notes"
shellframe_screen_enter
shellframe_raw_enter
shellframe_cursor_show
trap 'shellframe_raw_exit; shellframe_cursor_show; shellframe_screen_exit' EXIT
cols=$(tput cols); rows=$(tput lines)
while true; do
shellframe_editor_render 2 2 $(( cols - 2 )) $(( rows - 3 ))
shellframe_read_key key
shellframe_editor_on_key "$key"
rc=$?
(( rc == 2 )) && break # Ctrl-D
[[ "$key" == $'\003' ]] && { rc=1; break; } # Ctrl-C
done
(( rc == 0 )) && printf "Text:\n%s\n" "$(shellframe_editor_get_text "notes")"Returns: full text via shellframe_editor_get_text "ctx".
For flows with multiple screens, declare each screen as a function triple
and let shellframe_app drive the loop. No render loop or key dispatch to
write yourself.
Screen 1 — confirm Screen 2 — alert (after Yes)
+----------------------------------+ +----------------------------------+
| | | |
| Flush the Redis cache? | | Cache flushed. |
| | | |
| [ Yes ] [ No ] | | [ OK — any key ] |
| | | |
+----------------------------------+ +----------------------------------+
source shellframe.sh
# Screen 1: confirm
_app_CONFIRM_type() { printf 'confirm'; }
_app_CONFIRM_render() {
_SHELLFRAME_APP_QUESTION="Flush the Redis cache?"
}
_app_CONFIRM_yes() { _SHELLFRAME_APP_NEXT="DONE"; }
_app_CONFIRM_no() { _SHELLFRAME_APP_NEXT="__QUIT__"; }
# Screen 2: alert
_app_DONE_type() { printf 'alert'; }
_app_DONE_render() {
_SHELLFRAME_APP_TITLE="Cache flushed."
flush_cache # run your real work here
}
_app_DONE_dismiss() { _SHELLFRAME_APP_NEXT="__QUIT__"; }
shellframe_app "_app" "CONFIRM"How it works: shellframe_app calls _app_SCREEN_render() to populate
globals, renders the widget declared by _app_SCREEN_type(), maps the
keypress to an event name, and calls _app_SCREEN_<event>() to set
_SHELLFRAME_APP_NEXT. Screens transition without any loop boilerplate.
A horizontal row of labelled tabs. Left/Right arrow keys cycle focus; the active tab is rendered in reverse-video.
┌─────────────────────────────────────────────────────────┐
│ Overview │ Deployments │ Logs │ Settings │
├─────────────────────────────────────────────────────────┤
│ │
│ (content area for the active tab) │
│ │
└─────────────────────────────────────────────────────────┘
source shellframe.sh
SHELLFRAME_TABBAR_TABS=("Overview" "Deployments" "Logs" "Settings")
SHELLFRAME_TABBAR_ACTIVE=0
SHELLFRAME_TABBAR_FOCUSED=1
# In your render loop:
shellframe_tabbar_render 1 1 "$cols" 1
# In your key handler:
shellframe_tabbar_on_key "$key"
active=$SHELLFRAME_TABBAR_ACTIVEReturns: active tab index in SHELLFRAME_TABBAR_ACTIVE (0-based).
The v2 runtime (shellframe_shell) lets you declare named regions, assign
widgets to them, and handle focus traversal with Tab. Each region gets its
own render and key-handler callbacks.
┌─ shellframe demo ──────────────────────────────────────────┐
│ Overview │ Deployments │ Logs │ Settings │
├────────────────────────────────────────────────────────────┤
│ ┌─ Services ──────────────┐ ┌─ Details ────────────────┐ │
│ │ api-server │ │ Service: api-server │ │
│ │ > worker ← │ │ Version: v2.4.0 │ │
│ │ cache │ │ Status: live │ │
│ │ cdn │ │ Deployed: 1 day ago │ │
│ └─────────────────────────┘ └──────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Tab next pane ↑/↓ move Enter select q quit
source shellframe.sh
# Declare regions: name top left width height [nofocus]
shellframe_shell_region "tabs" 1 1 "$cols" 1 nofocus
shellframe_shell_region "list" 3 1 30 20
shellframe_shell_region "detail" 3 32 0 20 # 0 = fill remaining
# Render callbacks — called on every draw cycle
_demo_ROOT_tabs_render() { shellframe_tabbar_render "$@"; }
_demo_ROOT_list_render() { shellframe_list_render "$@"; }
_demo_ROOT_detail_render() { shellframe_panel_render "$@"; }
# Key callbacks — return 0 (handled), 1 (pass on), 2 (submit/quit)
_demo_ROOT_list_on_key() {
shellframe_list_on_key "$1"
local rc=$?
(( rc == 2 )) && _SHELLFRAME_APP_NEXT="DETAIL"
return $rc
}
shellframe_shell "_demo" "ROOT"See docs/skeletons.md for copy-paste starting points for
each of the patterns above.
Horizontal menu bar with dropdowns and one level of submenu nesting.
File Edit View
╔══════════════════╗
║ Open ║
║ Save ║
║ ════════════════ ║
║ Recent Files ▶║╔══════════════╗
║ ════════════════ ║║ demo.db ║
║ Quit ║║ work.db ║
╚══════════════════╝║ archive.db ║
╚══════════════╝
source shellframe.sh
SHELLFRAME_MENU_NAMES=("File" "Edit" "View")
SHELLFRAME_MENU_FILE=("Open" "Save" "---" "@RECENT:Recent Files" "---" "Quit")
SHELLFRAME_MENU_RECENT=("demo.db" "work.db" "archive.db")
SHELLFRAME_MENUBAR_CTX="demo"
shellframe_menubar_init "demo"
shellframe_menubar_on_focus 1
exec 3>/dev/tty
while true; do
shellframe_menubar_render 1 1 "$cols" "$rows"
shellframe_read_key key
shellframe_menubar_on_key "$key"
(( $? == 2 )) && break
done
printf 'Selected: %s\n' "$SHELLFRAME_MENUBAR_RESULT"Data model: SHELLFRAME_MENU_<NAME> arrays hold items. --- = separator.
@VARNAME:Label declares a submenu backed by SHELLFRAME_MENU_VARNAME.
Result path in SHELLFRAME_MENUBAR_RESULT (e.g. File|Recent Files|demo.db).
Empty result = dismissed with Esc.
Layers a filtered suggestion popup on any input field or editor. Consumer provides a callback; shellframe handles the UI.
┌──────────────────────────────────────┐
│ Table name: us█ │
│ ┌──────────────┐ │
│ │ users │ │
│ │ user_roles │ │
│ └──────────────┘ │
│ │
│ Tab: complete Esc: dismiss │
└──────────────────────────────────────┘
# Provider: return matches for prefix
_my_provider() {
local _prefix="$1" _out="$2"
local _items=("users" "user_roles" "products")
local _matches=()
local _i
for _i in "${_items[@]}"; do
case "$_i" in "${_prefix}"*) _matches+=("$_i") ;; esac
done
eval "$_out=(\"\${_matches[@]+\"\${_matches[@]}\"}\")"
}
# Attach to an input field
shellframe_field_init "myfield"
SHELLFRAME_AC_PROVIDER="_my_provider"
SHELLFRAME_AC_TRIGGER="tab"
shellframe_ac_attach "myfield" "field"
# In your on_key handler:
shellframe_ac_on_key "$key" && return 0
# ... field processes key ...
shellframe_ac_on_key_after # re-filter in auto modeA sheet is a partial overlay that sits above the current shellframe_shell screen. It:
- Shows 1 frozen, dimmed row of the underlying screen at the top (the "back strip")
- Renders its own content from row 2 downward, with configurable height
- Supports internal screen transitions (wizard pattern) via
_SHELLFRAME_SHEET_NEXT - Dismisses on Esc, Up from topmost focusable region, or
shellframe_sheet_pop
source shellframe.sh
# Push a sheet from any shellframe_shell event handler:
_myapp_ROOT_open_action() {
shellframe_sheet_push "_myapp" "OPEN_DB"
}
# Sheet screen hooks — identical convention to shellframe_shell screens.
# Row 1 = first content row (screen row 2, below the back strip).
# Use $SHELLFRAME_SHEET_WIDTH for the width argument.
_myapp_OPEN_DB_render() {
SHELLFRAME_SHEET_HEIGHT=7
shellframe_shell_region body 1 1 "$SHELLFRAME_SHEET_WIDTH" 6
shellframe_shell_region footer 7 1 "$SHELLFRAME_SHEET_WIDTH" 1 nofocus
}
_myapp_OPEN_DB_body_render() { shellframe_form_render "db" "$@"; }
_myapp_OPEN_DB_body_on_key() { shellframe_form_on_key "db" "$1"; }
_myapp_OPEN_DB_quit() { shellframe_sheet_pop; }
# Wizard transition (set _SHELLFRAME_SHEET_NEXT in any action handler):
_myapp_OPEN_DB_body_action() { _SHELLFRAME_SHEET_NEXT="CONFIRM"; }- Back-strip dimming is best-effort. Frozen rows are wrapped in
\033[2m...\033[22m. Rows containing\033[0m(full reset) mid-string will have dim cancelled at that point. Partial dimming is visually acceptable; full ANSI stripping is deferred to a future release. - Stacking not supported. Calling
shellframe_sheet_pushwhile a sheet is already active returns 1 and prints a warning to stderr. One sheet at a time.