Replace whiptail TUI with gum (charmbracelet/gum)
- Rewrite ui_common.sh wrappers to use gum choose, gum input, gum confirm, gum style, gum pager - Menu items display as "TAG Description" for reliable tag extraction (handles duplicate descriptions safely) - Replace whiptail --gauge with text-based progress bar - Use printf %b instead of echo -e to avoid escape injection - Read msgbox keypress from /dev/tty to work in piped contexts - Update bin/gniza, install.sh, README.md references Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# gniza - Linux Backup Manager
|
# gniza - Linux Backup Manager
|
||||||
|
|
||||||
A generic Linux backup tool with a Whiptail TUI and CLI interface. Define named backup targets (sets of directories), configure remote destinations (SSH, local, S3, Google Drive), and run incremental backups with rsync `--link-dest` deduplication.
|
A generic Linux backup tool with a Gum TUI and CLI interface. Define named backup targets (sets of directories), configure remote destinations (SSH, local, S3, Google Drive), and run incremental backups with rsync `--link-dest` deduplication.
|
||||||
|
|
||||||
```
|
```
|
||||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
@@ -30,7 +30,7 @@ A generic Linux backup tool with a Whiptail TUI and CLI interface. Define named
|
|||||||
- **Target-based backups** - Define named profiles with sets of directories to back up
|
- **Target-based backups** - Define named profiles with sets of directories to back up
|
||||||
- **Multiple remote types** - SSH, local (USB/NFS), S3, Google Drive
|
- **Multiple remote types** - SSH, local (USB/NFS), S3, Google Drive
|
||||||
- **Incremental snapshots** - rsync `--link-dest` for space-efficient deduplication
|
- **Incremental snapshots** - rsync `--link-dest` for space-efficient deduplication
|
||||||
- **Whiptail TUI** - Full terminal UI for interactive management
|
- **Gum TUI** - Beautiful terminal UI powered by [gum](https://github.com/charmbracelet/gum)
|
||||||
- **CLI interface** - Scriptable commands for automation and cron
|
- **CLI interface** - Scriptable commands for automation and cron
|
||||||
- **Atomic snapshots** - `.partial` directory during backup, renamed on success
|
- **Atomic snapshots** - `.partial` directory during backup, renamed on success
|
||||||
- **Retention policies** - Automatic pruning of old snapshots
|
- **Retention policies** - Automatic pruning of old snapshots
|
||||||
@@ -68,7 +68,7 @@ Root mode installs to `/usr/local/gniza`. User mode installs to `~/.local/share/
|
|||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- **Required**: bash 4+, rsync
|
- **Required**: bash 4+, rsync
|
||||||
- **Optional**: ssh, whiptail (TUI), curl (SMTP notifications), rclone (S3/GDrive)
|
- **Optional**: ssh, [gum](https://github.com/charmbracelet/gum) (TUI), curl (SMTP notifications), rclone (S3/GDrive)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ Commands:
|
|||||||
logs [--last] [--tail=N]
|
logs [--last] [--tail=N]
|
||||||
version
|
version
|
||||||
|
|
||||||
If no command is given and whiptail is available, the TUI is launched.
|
If no command is given and gum is available, the TUI is launched.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +436,7 @@ if [[ -n "$SUBCOMMAND" ]]; then
|
|||||||
run_cli
|
run_cli
|
||||||
elif [[ "$FORCE_CLI" == "true" ]]; then
|
elif [[ "$FORCE_CLI" == "true" ]]; then
|
||||||
run_cli
|
run_cli
|
||||||
elif command -v whiptail &>/dev/null && [[ -t 1 ]]; then
|
elif command -v gum &>/dev/null && [[ -t 1 ]]; then
|
||||||
# TUI mode
|
# TUI mode
|
||||||
show_logo
|
show_logo
|
||||||
if ! has_remotes && ! has_targets; then
|
if ! has_remotes && ! has_targets; then
|
||||||
|
|||||||
212
lib/ui_common.sh
212
lib/ui_common.sh
@@ -1,133 +1,187 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# gniza4linux/lib/ui_common.sh — Whiptail TUI wrappers with consistent sizing
|
# gniza4linux/lib/ui_common.sh — Gum TUI wrappers
|
||||||
|
|
||||||
[[ -n "${_GNIZA4LINUX_UI_COMMON_LOADED:-}" ]] && return 0
|
[[ -n "${_GNIZA4LINUX_UI_COMMON_LOADED:-}" ]] && return 0
|
||||||
_GNIZA4LINUX_UI_COMMON_LOADED=1
|
_GNIZA4LINUX_UI_COMMON_LOADED=1
|
||||||
|
|
||||||
readonly WHIPTAIL_TITLE="gniza Backup Manager"
|
# Theme
|
||||||
|
readonly _GUM_ACCENT="212"
|
||||||
|
readonly _GUM_CURSOR="▸ "
|
||||||
|
|
||||||
ui_calc_size() {
|
# ── Internal: tag-description mapping ─────────────────────────
|
||||||
local term_h="${LINES:-24}"
|
# gum returns the selected display text, not a separate tag.
|
||||||
local term_w="${COLUMNS:-80}"
|
# To handle duplicate descriptions, we append a zero-width space
|
||||||
local h w
|
# marker per item (U+200B repeated i times) to guarantee uniqueness,
|
||||||
h=$(( term_h - 4 ))
|
# then strip the markers from the result to find the index.
|
||||||
w=$(( term_w - 4 ))
|
#
|
||||||
(( h > 20 )) && h=20
|
# Alternatively we use a simpler approach: display "tag description"
|
||||||
(( w > 76 )) && w=76
|
# and split on the double-space to extract the tag.
|
||||||
echo "$h" "$w"
|
# Tags in gniza are always short identifiers without spaces.
|
||||||
|
|
||||||
|
_gum_fmt_items() {
|
||||||
|
local -n _tags=$1 _descs=$2
|
||||||
|
local i
|
||||||
|
for i in "${!_tags[@]}"; do
|
||||||
|
printf '%s\n' "${_tags[$i]} ${_descs[$i]}"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
_ui_backtitle() {
|
_gum_extract_tag() {
|
||||||
echo "gniza v${GNIZA4LINUX_VERSION}"
|
local line="$1"
|
||||||
|
echo "${line%% *}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Menu (tag/description pairs) ─────────────────────────────
|
||||||
|
# Usage: ui_menu "Title" "TAG1" "Desc 1" "TAG2" "Desc 2" ...
|
||||||
|
# Returns the selected TAG.
|
||||||
ui_menu() {
|
ui_menu() {
|
||||||
local title="$1"; shift
|
local title="$1"; shift
|
||||||
local -a items=("$@")
|
local -a tags=() descs=()
|
||||||
local size; size=$(ui_calc_size)
|
while [[ $# -ge 2 ]]; do
|
||||||
local h w
|
tags+=("$1")
|
||||||
read -r h w <<< "$size"
|
descs+=("$2")
|
||||||
local menu_h=$(( h - 7 ))
|
shift 2
|
||||||
(( menu_h < 3 )) && menu_h=3
|
done
|
||||||
|
|
||||||
local result
|
local result
|
||||||
result=$(whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
result=$(_gum_fmt_items tags descs | gum choose \
|
||||||
--menu "$title" "$h" "$w" "$menu_h" "${items[@]}" 3>&1 1>&2 2>&3) || return 1
|
--header "$title" \
|
||||||
echo "$result"
|
--header.foreground "$_GUM_ACCENT" \
|
||||||
|
--cursor.foreground "$_GUM_ACCENT" \
|
||||||
|
--cursor "$_GUM_CURSOR") || return 1
|
||||||
|
|
||||||
|
_gum_extract_tag "$result"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Checklist (tag/description/status triplets, multi-select) ─
|
||||||
|
# Usage: ui_checklist "Title" "TAG1" "Desc 1" "ON" "TAG2" "Desc 2" "OFF" ...
|
||||||
|
# Returns space-separated quoted tags: "TAG1" "TAG2"
|
||||||
ui_checklist() {
|
ui_checklist() {
|
||||||
local title="$1"; shift
|
local title="$1"; shift
|
||||||
local -a items=("$@")
|
local -a tags=() descs=() selected=()
|
||||||
local size; size=$(ui_calc_size)
|
while [[ $# -ge 3 ]]; do
|
||||||
local h w
|
tags+=("$1")
|
||||||
read -r h w <<< "$size"
|
descs+=("$2")
|
||||||
local list_h=$(( h - 7 ))
|
[[ "$3" == "ON" ]] && selected+=("${1} ${2}")
|
||||||
(( list_h < 3 )) && list_h=3
|
shift 3
|
||||||
|
done
|
||||||
|
|
||||||
|
local -a sel_args=()
|
||||||
|
local s
|
||||||
|
for s in "${selected[@]}"; do
|
||||||
|
sel_args+=(--selected "$s")
|
||||||
|
done
|
||||||
|
|
||||||
local result
|
local result
|
||||||
result=$(whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
result=$(_gum_fmt_items tags descs | gum choose --no-limit \
|
||||||
--checklist "$title" "$h" "$w" "$list_h" "${items[@]}" 3>&1 1>&2 2>&3) || return 1
|
--header "$title" \
|
||||||
echo "$result"
|
--header.foreground "$_GUM_ACCENT" \
|
||||||
|
--cursor.foreground "$_GUM_ACCENT" \
|
||||||
|
--cursor "$_GUM_CURSOR" \
|
||||||
|
"${sel_args[@]}") || return 1
|
||||||
|
|
||||||
|
local output=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
local tag
|
||||||
|
tag=$(_gum_extract_tag "$line")
|
||||||
|
[[ -n "$output" ]] && output+=" "
|
||||||
|
output+="\"$tag\""
|
||||||
|
done <<< "$result"
|
||||||
|
echo "$output"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Radiolist (tag/description/status triplets, single select) ─
|
||||||
|
# Usage: ui_radiolist "Title" "TAG1" "Desc 1" "ON" "TAG2" "Desc 2" "OFF" ...
|
||||||
|
# Returns the selected TAG.
|
||||||
ui_radiolist() {
|
ui_radiolist() {
|
||||||
local title="$1"; shift
|
local title="$1"; shift
|
||||||
local -a items=("$@")
|
local -a tags=() descs=()
|
||||||
local size; size=$(ui_calc_size)
|
local preselected=""
|
||||||
local h w
|
while [[ $# -ge 3 ]]; do
|
||||||
read -r h w <<< "$size"
|
tags+=("$1")
|
||||||
local list_h=$(( h - 7 ))
|
descs+=("$2")
|
||||||
(( list_h < 3 )) && list_h=3
|
[[ "$3" == "ON" ]] && preselected="${1} ${2}"
|
||||||
|
shift 3
|
||||||
|
done
|
||||||
|
|
||||||
|
local -a sel_args=()
|
||||||
|
[[ -n "$preselected" ]] && sel_args=(--selected "$preselected")
|
||||||
|
|
||||||
local result
|
local result
|
||||||
result=$(whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
result=$(_gum_fmt_items tags descs | gum choose \
|
||||||
--radiolist "$title" "$h" "$w" "$list_h" "${items[@]}" 3>&1 1>&2 2>&3) || return 1
|
--header "$title" \
|
||||||
echo "$result"
|
--header.foreground "$_GUM_ACCENT" \
|
||||||
|
--cursor.foreground "$_GUM_ACCENT" \
|
||||||
|
--cursor "$_GUM_CURSOR" \
|
||||||
|
"${sel_args[@]}") || return 1
|
||||||
|
|
||||||
|
_gum_extract_tag "$result"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Input box ─────────────────────────────────────────────────
|
||||||
ui_inputbox() {
|
ui_inputbox() {
|
||||||
local title="$1"
|
local title="$1"
|
||||||
local prompt="$2"
|
local prompt="$2"
|
||||||
local default="${3:-}"
|
local default="${3:-}"
|
||||||
local size; size=$(ui_calc_size)
|
|
||||||
local h w
|
|
||||||
read -r h w <<< "$size"
|
|
||||||
|
|
||||||
local result
|
gum input \
|
||||||
result=$(whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
--header "$prompt" \
|
||||||
--inputbox "$prompt" "$h" "$w" "$default" 3>&1 1>&2 2>&3) || return 1
|
--header.foreground "$_GUM_ACCENT" \
|
||||||
echo "$result"
|
--value "$default" \
|
||||||
|
--width 60 || return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Yes/No confirmation ──────────────────────────────────────
|
||||||
ui_yesno() {
|
ui_yesno() {
|
||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
local size; size=$(ui_calc_size)
|
gum confirm "$(printf '%b' "$prompt")" \
|
||||||
local h w
|
--affirmative "Yes" \
|
||||||
read -r h w <<< "$size"
|
--negative "No"
|
||||||
|
|
||||||
whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
|
||||||
--yesno "$prompt" "$h" "$w" 3>&1 1>&2 2>&3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Message box ───────────────────────────────────────────────
|
||||||
ui_msgbox() {
|
ui_msgbox() {
|
||||||
local msg="$1"
|
local msg="$1"
|
||||||
local size; size=$(ui_calc_size)
|
echo ""
|
||||||
local h w
|
printf '%b' "$msg" | gum style \
|
||||||
read -r h w <<< "$size"
|
--border rounded \
|
||||||
|
--border-foreground "$_GUM_ACCENT" \
|
||||||
whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
--padding "1 2" \
|
||||||
--msgbox "$msg" "$h" "$w" 3>&1 1>&2 2>&3
|
--margin "0 2"
|
||||||
|
echo ""
|
||||||
|
read -rsp " Press any key to continue..." -n1 < /dev/tty
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Progress gauge (reads percentage lines from stdin) ────────
|
||||||
ui_gauge() {
|
ui_gauge() {
|
||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
local size; size=$(ui_calc_size)
|
while IFS= read -r pct; do
|
||||||
local h w
|
[[ "$pct" =~ ^[0-9]+$ ]] || continue
|
||||||
read -r h w <<< "$size"
|
local filled=$(( pct / 5 ))
|
||||||
|
local empty=$(( 20 - filled ))
|
||||||
whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
local bar=""
|
||||||
--gauge "$prompt" "$h" "$w" 0 3>&1 1>&2 2>&3
|
local i
|
||||||
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
||||||
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
||||||
|
printf "\r %s [%s] %s%%" "$prompt" "$bar" "$pct"
|
||||||
|
done
|
||||||
|
printf "\033[2K\r %s [████████████████████] done\n" "$prompt"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Text file viewer ─────────────────────────────────────────
|
||||||
ui_textbox() {
|
ui_textbox() {
|
||||||
local filepath="$1"
|
local filepath="$1"
|
||||||
local size; size=$(ui_calc_size)
|
gum pager < "$filepath"
|
||||||
local h w
|
|
||||||
read -r h w <<< "$size"
|
|
||||||
|
|
||||||
whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
|
||||||
--textbox "$filepath" "$h" "$w" 3>&1 1>&2 2>&3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Password input ────────────────────────────────────────────
|
||||||
ui_password() {
|
ui_password() {
|
||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
local size; size=$(ui_calc_size)
|
gum input \
|
||||||
local h w
|
--password \
|
||||||
read -r h w <<< "$size"
|
--header "$prompt" \
|
||||||
|
--header.foreground "$_GUM_ACCENT" \
|
||||||
local result
|
--width 60 || return 1
|
||||||
result=$(whiptail --title "$WHIPTAIL_TITLE" --backtitle "$(_ui_backtitle)" \
|
|
||||||
--passwordbox "$prompt" "$h" "$w" 3>&1 1>&2 2>&3) || return 1
|
|
||||||
echo "$result"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ for cmd in bash rsync; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
for cmd in ssh whiptail curl; do
|
for cmd in ssh gum curl; do
|
||||||
if ! command -v "$cmd" &>/dev/null; then
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
warn "Optional dependency not found: $cmd"
|
warn "Optional dependency not found: $cmd"
|
||||||
fi
|
fi
|
||||||
@@ -140,5 +140,5 @@ echo " Logs: $LOG_DIR"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Get started:"
|
echo "Get started:"
|
||||||
echo " gniza --help Show CLI help"
|
echo " gniza --help Show CLI help"
|
||||||
echo " gniza Launch TUI (requires whiptail)"
|
echo " gniza Launch TUI (requires gum)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
Reference in New Issue
Block a user