From 121a615e67a4da38b7bdc34df06803f841369a2d Mon Sep 17 00:00:00 2001 From: shuki Date: Fri, 6 Mar 2026 02:15:52 +0200 Subject: [PATCH] Remove legacy gum TUI and clean up install script - Delete all lib/ui_*.sh files (gum-based TUI) - Remove gum download from install script - Remove gum fallback and show_logo from bin/gniza - Update README to reference Textual TUI and web GUI Co-Authored-By: Claude Opus 4.6 --- README.md | 8 +- bin/gniza | 51 +---- lib/ui_backup.sh | 116 ----------- lib/ui_common.sh | 187 ------------------ lib/ui_logs.sh | 96 ---------- lib/ui_main.sh | 35 ---- lib/ui_remotes.sh | 458 -------------------------------------------- lib/ui_restore.sh | 145 -------------- lib/ui_retention.sh | 112 ----------- lib/ui_schedule.sh | 170 ---------------- lib/ui_settings.sh | 123 ------------ lib/ui_snapshots.sh | 111 ----------- lib/ui_targets.sh | 206 -------------------- lib/ui_wizard.sh | 62 ------ scripts/install.sh | 49 ----- 15 files changed, 5 insertions(+), 1924 deletions(-) delete mode 100644 lib/ui_backup.sh delete mode 100644 lib/ui_common.sh delete mode 100644 lib/ui_logs.sh delete mode 100644 lib/ui_main.sh delete mode 100644 lib/ui_remotes.sh delete mode 100644 lib/ui_restore.sh delete mode 100644 lib/ui_retention.sh delete mode 100644 lib/ui_schedule.sh delete mode 100644 lib/ui_settings.sh delete mode 100644 lib/ui_snapshots.sh delete mode 100644 lib/ui_targets.sh delete mode 100644 lib/ui_wizard.sh diff --git a/README.md b/README.md index a0d22ef..add5d99 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gniza - Linux Backup Manager -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. +A generic Linux backup tool with a Python Textual TUI, web GUI, 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. ``` ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ @@ -27,7 +27,8 @@ A generic Linux backup tool with a Gum TUI and CLI interface. Define named backu - **Target-based backups** - Define named profiles with sets of directories to back up - **Multiple remote types** - SSH, local (USB/NFS), S3, Google Drive - **Incremental snapshots** - rsync `--link-dest` for space-efficient deduplication -- **Gum TUI** - Beautiful terminal UI powered by [gum](https://github.com/charmbracelet/gum) +- **Textual TUI** - Beautiful terminal UI powered by [Textual](https://textual.textualize.io/) +- **Web GUI** - Access the TUI from any browser via `gniza web` - **CLI interface** - Scriptable commands for automation and cron - **Atomic snapshots** - `.partial` directory during backup, renamed on success - **Retention policies** - Automatic pruning of old snapshots @@ -66,8 +67,7 @@ Root mode installs to `/usr/local/gniza`. User mode installs to `~/.local/share/ - **Required**: bash 4+, rsync - **Optional**: ssh, curl (SMTP notifications), rclone (S3/GDrive) - -> **Note**: [gum](https://github.com/charmbracelet/gum) (TUI engine) is automatically downloaded during installation. +- **TUI/Web**: python3, textual, textual-serve (installed automatically) ## Quick Start diff --git a/bin/gniza b/bin/gniza index 2f3cd21..7c6eb07 100755 --- a/bin/gniza +++ b/bin/gniza @@ -5,9 +5,6 @@ set -euo pipefail GNIZA_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" export GNIZA_DIR -# Add bundled binaries (gum) to PATH if present -[[ -d "$GNIZA_DIR/bin" ]] && export PATH="$GNIZA_DIR/bin:$PATH" - # ── Source libraries in dependency order ───────────────────── source "$GNIZA_DIR/lib/constants.sh" source "$GNIZA_DIR/lib/utils.sh" @@ -26,45 +23,6 @@ source "$GNIZA_DIR/lib/ssh.sh" source "$GNIZA_DIR/lib/rclone.sh" source "$GNIZA_DIR/lib/snapshot.sh" source "$GNIZA_DIR/lib/transfer.sh" -source "$GNIZA_DIR/lib/ui_common.sh" -source "$GNIZA_DIR/lib/ui_main.sh" -source "$GNIZA_DIR/lib/ui_targets.sh" -source "$GNIZA_DIR/lib/ui_remotes.sh" -source "$GNIZA_DIR/lib/ui_backup.sh" -source "$GNIZA_DIR/lib/ui_restore.sh" -source "$GNIZA_DIR/lib/ui_snapshots.sh" -source "$GNIZA_DIR/lib/ui_retention.sh" -source "$GNIZA_DIR/lib/ui_logs.sh" -source "$GNIZA_DIR/lib/ui_schedule.sh" -source "$GNIZA_DIR/lib/ui_settings.sh" -source "$GNIZA_DIR/lib/ui_wizard.sh" - -# ── ASCII Logo ─────────────────────────────────────────────── -show_logo() { - echo "${C_GREEN}" - cat <<'LOGO' - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓▓▓▓▓ - ▓▓▓▓▓▓ - ▓▓ -LOGO - echo "${C_RESET}" - echo " gniza v${GNIZA4LINUX_VERSION} - Linux Backup Manager" - echo "" -} # ── Help ───────────────────────────────────────────────────── show_help() { @@ -91,7 +49,7 @@ Commands: logs [--last] [--tail=N] version -If no command is given and gum is available, the TUI is launched. +If no command is given, the TUI is launched. EOF } @@ -418,13 +376,6 @@ elif [[ "$FORCE_CLI" == "true" ]]; then elif python3 -c "import textual" 2>/dev/null && [[ -t 1 ]]; then # Python Textual TUI mode PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m tui "$@" -elif command -v gum &>/dev/null && [[ -t 1 ]]; then - # Legacy gum TUI mode - show_logo - if ! has_remotes || ! has_targets; then - ui_first_run_wizard - fi - ui_main_menu else # Fallback to CLI help run_cli diff --git a/lib/ui_backup.sh b/lib/ui_backup.sh deleted file mode 100644 index 49ec23b..0000000 --- a/lib/ui_backup.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_backup.sh — Backup TUI - -[[ -n "${_GNIZA4LINUX_UI_BACKUP_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_BACKUP_LOADED=1 - -ui_backup_menu() { - while true; do - local choice - choice=$(ui_menu "Backup" \ - "SINGLE" "Backup single target" \ - "ALL" "Backup all targets" \ - "BACK" "Return to main menu") || return 0 - - case "$choice" in - SINGLE) ui_backup_wizard ;; - ALL) _ui_backup_all ;; - BACK) return 0 ;; - esac - done -} - -ui_backup_wizard() { - if ! has_targets; then - ui_msgbox "No targets configured. Please add a target first." - return 0 - fi - - local -a items=() - local targets - targets=$(list_targets) - while IFS= read -r t; do - items+=("$t" "Target: $t") - done <<< "$targets" - - local target - target=$(ui_menu "Select Target" "${items[@]}") || return 0 - - local remote="" - if has_remotes; then - local -a ritems=("DEFAULT" "Use default/all remotes") - local remotes - remotes=$(list_remotes) - while IFS= read -r r; do - ritems+=("$r" "Remote: $r") - done <<< "$remotes" - - remote=$(ui_menu "Select Remote" "${ritems[@]}") || return 0 - [[ "$remote" == "DEFAULT" ]] && remote="" - fi - - local confirm_msg="Run backup?\n\nTarget: $target" - [[ -n "$remote" ]] && confirm_msg+="\nRemote: $remote" - confirm_msg+="\n" - - ui_yesno "$confirm_msg" || return 0 - - _ui_run_backup "$target" "$remote" -} - -_ui_backup_all() { - if ! has_targets; then - ui_msgbox "No targets configured." - return 0 - fi - - ui_yesno "Backup ALL targets now?" || return 0 - - local targets - targets=$(list_targets) - local count=0 total=0 - total=$(echo "$targets" | wc -l) - - local output="" - while IFS= read -r t; do - ((count++)) - local pct=$(( count * 100 / total )) - echo "$pct" - local result - if result=$(gniza --cli backup --target="$t" 2>&1); then - output+="$t: OK\n" - else - output+="$t: FAILED\n$result\n" - fi - done <<< "$targets" | ui_gauge "Backing up all targets..." - - ui_msgbox "Backup Results:\n\n$output" -} - -_ui_run_backup() { - local target="$1" - local remote="$2" - - local -a cmd_args=(gniza --cli backup "--target=$target") - [[ -n "$remote" ]] && cmd_args+=("--remote=$remote") - - local tmpfile - tmpfile=$(mktemp /tmp/gniza-backup-XXXXXX.log) - - ( - echo "10" - if "${cmd_args[@]}" > "$tmpfile" 2>&1; then - echo "100" - else - echo "100" - fi - ) | ui_gauge "Backing up target: $target" - - if [[ -s "$tmpfile" ]]; then - ui_textbox "$tmpfile" - else - ui_msgbox "Backup of '$target' completed." - fi - - rm -f "$tmpfile" -} diff --git a/lib/ui_common.sh b/lib/ui_common.sh deleted file mode 100644 index 8420fb8..0000000 --- a/lib/ui_common.sh +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_common.sh — Gum TUI wrappers - -[[ -n "${_GNIZA4LINUX_UI_COMMON_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_COMMON_LOADED=1 - -# Theme -readonly _GUM_ACCENT="212" -readonly _GUM_CURSOR="▸ " - -# ── Internal: tag-description mapping ───────────────────────── -# gum returns the selected display text, not a separate tag. -# To handle duplicate descriptions, we append a zero-width space -# marker per item (U+200B repeated i times) to guarantee uniqueness, -# then strip the markers from the result to find the index. -# -# Alternatively we use a simpler approach: display "tag description" -# and split on the double-space to extract the tag. -# 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 -} - -_gum_extract_tag() { - 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() { - local title="$1"; shift - local -a tags=() descs=() - while [[ $# -ge 2 ]]; do - tags+=("$1") - descs+=("$2") - shift 2 - done - - local result - result=$(_gum_fmt_items tags descs | gum choose \ - --header "$title" \ - --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() { - local title="$1"; shift - local -a tags=() descs=() selected=() - while [[ $# -ge 3 ]]; do - tags+=("$1") - descs+=("$2") - [[ "$3" == "ON" ]] && selected+=("${1} ${2}") - shift 3 - done - - local -a sel_args=() - local s - for s in "${selected[@]}"; do - sel_args+=(--selected "$s") - done - - local result - result=$(_gum_fmt_items tags descs | gum choose --no-limit \ - --header "$title" \ - --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() { - local title="$1"; shift - local -a tags=() descs=() - local preselected="" - while [[ $# -ge 3 ]]; do - tags+=("$1") - descs+=("$2") - [[ "$3" == "ON" ]] && preselected="${1} ${2}" - shift 3 - done - - local -a sel_args=() - [[ -n "$preselected" ]] && sel_args=(--selected "$preselected") - - local result - result=$(_gum_fmt_items tags descs | gum choose \ - --header "$title" \ - --header.foreground "$_GUM_ACCENT" \ - --cursor.foreground "$_GUM_ACCENT" \ - --cursor "$_GUM_CURSOR" \ - "${sel_args[@]}") || return 1 - - _gum_extract_tag "$result" -} - -# ── Input box ───────────────────────────────────────────────── -ui_inputbox() { - local title="$1" - local prompt="$2" - local default="${3:-}" - - gum input \ - --header "$prompt" \ - --header.foreground "$_GUM_ACCENT" \ - --value "$default" \ - --width 60 || return 1 -} - -# ── Yes/No confirmation ────────────────────────────────────── -ui_yesno() { - local prompt="$1" - gum confirm "$(printf '%b' "$prompt")" \ - --affirmative "Yes" \ - --negative "No" -} - -# ── Message box ─────────────────────────────────────────────── -ui_msgbox() { - local msg="$1" - echo "" - printf '%b' "$msg" | gum style \ - --border rounded \ - --border-foreground "$_GUM_ACCENT" \ - --padding "1 2" \ - --margin "0 2" - echo "" - read -rsp " Press any key to continue..." -n1 < /dev/tty - echo "" -} - -# ── Progress gauge (reads percentage lines from stdin) ──────── -ui_gauge() { - local prompt="$1" - while IFS= read -r pct; do - [[ "$pct" =~ ^[0-9]+$ ]] || continue - local filled=$(( pct / 5 )) - local empty=$(( 20 - filled )) - local bar="" - local i - for ((i=0; i/dev/null | head -20) - - if [[ -z "$logs" ]]; then - ui_msgbox "No log files found." - return 0 - fi - - while IFS= read -r f; do - local fname - fname=$(basename "$f") - local fsize - fsize=$(stat -c%s "$f" 2>/dev/null || echo "0") - items+=("$fname" "$(human_size "$fsize")") - done <<< "$logs" - items+=("BACK" "Return") - - local selected - selected=$(ui_menu "Log Files (recent first)" "${items[@]}") || return 0 - - [[ "$selected" == "BACK" ]] && return 0 - - local filepath="$log_dir/$selected" - if [[ -f "$filepath" ]]; then - ui_textbox "$filepath" - else - ui_msgbox "Log file not found: $filepath" - fi -} - -ui_logs_status() { - local log_dir="${LOG_DIR:-/var/log/gniza}" - local status_msg="Backup Status Overview\n" - status_msg+="=====================\n\n" - - # Last backup time - local latest_log - latest_log=$(ls -1t "$log_dir"/gniza-*.log 2>/dev/null | head -1) - if [[ -n "$latest_log" ]]; then - local log_date - log_date=$(stat -c%y "$latest_log" 2>/dev/null | cut -d. -f1) - status_msg+="Last log: $log_date\n" - - # Last result - local last_line - last_line=$(tail -1 "$latest_log" 2>/dev/null) - status_msg+="Last entry: $last_line\n" - else - status_msg+="No backup logs found.\n" - fi - - # Disk usage - if [[ -d "$log_dir" ]]; then - local du_output - du_output=$(du -sh "$log_dir" 2>/dev/null | cut -f1) - status_msg+="\nLog disk usage: ${du_output:-unknown}\n" - fi - - # Log count - local log_count - log_count=$(ls -1 "$log_dir"/gniza-*.log 2>/dev/null | wc -l) - status_msg+="Log files: $log_count\n" - - ui_msgbox "$status_msg" -} diff --git a/lib/ui_main.sh b/lib/ui_main.sh deleted file mode 100644 index a6e2b74..0000000 --- a/lib/ui_main.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_main.sh — Main menu loop - -[[ -n "${_GNIZA4LINUX_UI_MAIN_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_MAIN_LOADED=1 - -ui_main_menu() { - while true; do - local choice - choice=$(ui_menu "Main Menu" \ - "1" "Backup" \ - "2" "Restore" \ - "3" "Targets" \ - "4" "Remotes" \ - "5" "Snapshots" \ - "6" "Retention" \ - "7" "Schedules" \ - "8" "Logs" \ - "9" "Settings" \ - "Q" "Quit") || break - - case "$choice" in - 1) ui_backup_menu ;; - 2) ui_restore_menu ;; - 3) ui_targets_menu ;; - 4) ui_remotes_menu ;; - 5) ui_snapshots_menu ;; - 6) ui_retention_menu ;; - 7) ui_schedule_menu ;; - 8) ui_logs_menu ;; - 9) ui_settings_menu ;; - Q) break ;; - esac - done -} diff --git a/lib/ui_remotes.sh b/lib/ui_remotes.sh deleted file mode 100644 index 5996dfa..0000000 --- a/lib/ui_remotes.sh +++ /dev/null @@ -1,458 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_remotes.sh — Remote management TUI - -[[ -n "${_GNIZA4LINUX_UI_REMOTES_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_REMOTES_LOADED=1 - -ui_remotes_menu() { - while true; do - local -a items=() - local remotes - remotes=$(list_remotes) - if [[ -n "$remotes" ]]; then - while IFS= read -r r; do - items+=("$r" "Remote: $r") - done <<< "$remotes" - fi - items+=("ADD" "Add new remote") - items+=("BACK" "Return to main menu") - - local choice - choice=$(ui_menu "Remotes" "${items[@]}") || return 0 - - case "$choice" in - ADD) ui_remote_add ;; - BACK) return 0 ;; - *) - local action - action=$(ui_menu "Remote: $choice" \ - "EDIT" "Edit remote" \ - "DELETE" "Delete remote" \ - "TEST" "Test connection" \ - "BACK" "Back") || continue - case "$action" in - EDIT) ui_remote_edit "$choice" ;; - DELETE) ui_remote_delete "$choice" ;; - TEST) ui_remote_test "$choice" ;; - BACK) continue ;; - esac - ;; - esac - done -} - -ui_remote_add() { - local name - name=$(ui_inputbox "Add Remote" "Enter remote name (letters, digits, _ -):" "") || return 0 - [[ -z "$name" ]] && return 0 - - if ! validate_target_name "$name" 2>/dev/null; then - ui_msgbox "Invalid remote name. Must start with a letter and contain only letters, digits, underscore, or hyphen (max 32 chars)." - return 0 - fi - - if [[ -f "$CONFIG_DIR/remotes.d/${name}.conf" ]]; then - ui_msgbox "Remote '$name' already exists." - return 0 - fi - - local rtype - rtype=$(ui_radiolist "Remote Type" \ - "ssh" "SSH remote" "ON" \ - "local" "Local directory" "OFF" \ - "s3" "Amazon S3 / compatible" "OFF" \ - "gdrive" "Google Drive" "OFF") || return 0 - - local conf="$CONFIG_DIR/remotes.d/${name}.conf" - - case "$rtype" in - ssh) _ui_remote_add_ssh "$name" "$conf" ;; - local) _ui_remote_add_local "$name" "$conf" ;; - s3) _ui_remote_add_s3 "$name" "$conf" ;; - gdrive) _ui_remote_add_gdrive "$name" "$conf" ;; - esac -} - -_ui_remote_add_ssh() { - local name="$1" conf="$2" - - local host; host=$(ui_inputbox "SSH Remote" "Hostname or IP:" "") || return 0 - [[ -z "$host" ]] && { ui_msgbox "Host is required."; return 0; } - - local port; port=$(ui_inputbox "SSH Remote" "Port:" "$DEFAULT_REMOTE_PORT") || port="$DEFAULT_REMOTE_PORT" - local user; user=$(ui_inputbox "SSH Remote" "Username:" "$DEFAULT_REMOTE_USER") || user="$DEFAULT_REMOTE_USER" - local base; base=$(ui_inputbox "SSH Remote" "Base path on remote:" "$DEFAULT_REMOTE_BASE") || base="$DEFAULT_REMOTE_BASE" - - local auth_method - auth_method=$(ui_radiolist "Authentication" \ - "key" "SSH key" "ON" \ - "password" "Password" "OFF") || auth_method="key" - - local key="" password="" - if [[ "$auth_method" == "key" ]]; then - key=$(ui_inputbox "SSH Remote" "Path to SSH key:" "$HOME/.ssh/id_rsa") || key="" - else - password=$(ui_password "Enter SSH password:") || password="" - fi - - local bwlimit; bwlimit=$(ui_inputbox "SSH Remote" "Bandwidth limit (KB/s, 0=unlimited):" "$DEFAULT_BWLIMIT") || bwlimit="$DEFAULT_BWLIMIT" - local retention; retention=$(ui_inputbox "SSH Remote" "Retention count:" "$DEFAULT_RETENTION_COUNT") || retention="$DEFAULT_RETENTION_COUNT" - - # Test connection before saving - if ui_yesno "Test connection to ${user}@${host}:${port} before saving?"; then - local test_result - local -a ssh_cmd=(ssh -o BatchMode=yes -o ConnectTimeout=10 -p "$port") - if [[ "$auth_method" == "key" && -n "$key" ]]; then - ssh_cmd+=(-i "$key") - fi - ssh_cmd+=("${user}@${host}" "echo OK") - if test_result=$("${ssh_cmd[@]}" 2>&1); then - ui_msgbox "Connection successful!" - else - ui_msgbox "Connection failed:\n\n$test_result" - if ! ui_yesno "Save remote anyway?"; then - return 0 - fi - fi - fi - - cat > "$conf" < "$conf" </dev/null && ui_yesno "Test S3 connection before saving?"; then - # Set globals temporarily for test_rclone_connection - REMOTE_TYPE="s3" S3_BUCKET="$bucket" S3_REGION="$region" \ - S3_ENDPOINT="$endpoint" S3_ACCESS_KEY_ID="$access_key" \ - S3_SECRET_ACCESS_KEY="$secret_key" REMOTE_BASE="$base" - local test_result - if test_result=$(test_rclone_connection 2>&1); then - ui_msgbox "S3 connection successful!" - else - ui_msgbox "S3 connection failed:\n\n$test_result" - if ! ui_yesno "Save remote anyway?"; then - return 0 - fi - fi - fi - - cat > "$conf" </dev/null && ui_yesno "Test Google Drive connection before saving?"; then - REMOTE_TYPE="gdrive" GDRIVE_SERVICE_ACCOUNT_FILE="$sa_file" \ - GDRIVE_ROOT_FOLDER_ID="$folder_id" REMOTE_BASE="$base" - local test_result - if test_result=$(test_rclone_connection 2>&1); then - ui_msgbox "Google Drive connection successful!" - else - ui_msgbox "Google Drive connection failed:\n\n$test_result" - if ! ui_yesno "Save remote anyway?"; then - return 0 - fi - fi - fi - - cat > "$conf" < "$conf" < "$conf" < "$conf" < "$conf" <&1) \ - && ui_msgbox "Connection to '$name' successful.\n\nResponse: $result" \ - || ui_msgbox "Connection to '$name' failed.\n\nError: $result" - ;; - local) - if [[ -d "$REMOTE_BASE" ]]; then - ui_msgbox "Local directory '$REMOTE_BASE' exists and is accessible." - else - ui_msgbox "Local directory '$REMOTE_BASE' does NOT exist." - fi - ;; - s3|gdrive) - if command -v rclone &>/dev/null; then - # Use the proper rclone transport layer (temp config file, not CLI args) - load_remote "$name" || { ui_msgbox "Failed to load remote."; break; } - if result=$(test_rclone_connection 2>&1); then - ui_msgbox "${REMOTE_TYPE} connection to '$name' successful." - else - ui_msgbox "${REMOTE_TYPE} connection to '$name' failed.\n\nError: $result" - fi - else - ui_msgbox "rclone is not installed. Cannot test ${REMOTE_TYPE} connection." - fi - ;; - *) - ui_msgbox "Unknown remote type: ${REMOTE_TYPE}" - ;; - esac -} diff --git a/lib/ui_restore.sh b/lib/ui_restore.sh deleted file mode 100644 index bdb32e1..0000000 --- a/lib/ui_restore.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_restore.sh — Restore TUI - -[[ -n "${_GNIZA4LINUX_UI_RESTORE_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_RESTORE_LOADED=1 - -ui_restore_menu() { - while true; do - local choice - choice=$(ui_menu "Restore" \ - "TARGET" "Restore full target" \ - "FOLDER" "Restore single folder" \ - "BACK" "Return to main menu") || return 0 - - case "$choice" in - TARGET) ui_restore_wizard "full" ;; - FOLDER) ui_restore_wizard "folder" ;; - BACK) return 0 ;; - esac - done -} - -ui_restore_wizard() { - local mode="$1" - - if ! has_targets; then - ui_msgbox "No targets configured." - return 0 - fi - - # Select target - local -a titems=() - local targets - targets=$(list_targets) - while IFS= read -r t; do - titems+=("$t" "Target: $t") - done <<< "$targets" - - local target - target=$(ui_menu "Select Target to Restore" "${titems[@]}") || return 0 - - # Select remote - local remote="" - if has_remotes; then - local -a ritems=() - local remotes - remotes=$(list_remotes) - while IFS= read -r r; do - ritems+=("$r" "Remote: $r") - done <<< "$remotes" - - remote=$(ui_menu "Select Remote" "${ritems[@]}") || return 0 - else - ui_msgbox "No remotes configured." - return 0 - fi - - # Load remote for snapshot listing - load_remote "$remote" || { ui_msgbox "Failed to load remote '$remote'."; return 0; } - - # Select snapshot - local snapshots - snapshots=$(list_remote_snapshots "$target" 2>/dev/null) - if [[ -z "$snapshots" ]]; then - ui_msgbox "No snapshots found for target '$target' on remote '$remote'." - return 0 - fi - - local -a sitems=() - while IFS= read -r s; do - sitems+=("$s" "Snapshot: $s") - done <<< "$snapshots" - - local snapshot - snapshot=$(ui_menu "Select Snapshot" "${sitems[@]}") || return 0 - - # Restore location - local restore_dest="" - local restore_type - restore_type=$(ui_radiolist "Restore Location" \ - "inplace" "Restore in-place (original location)" "ON" \ - "directory" "Restore to a different directory" "OFF") || return 0 - - if [[ "$restore_type" == "directory" ]]; then - restore_dest=$(ui_inputbox "Restore" "Enter destination directory:" "/tmp/restore") || return 0 - [[ -z "$restore_dest" ]] && { ui_msgbox "Destination is required."; return 0; } - fi - - # Folder selection for single-folder mode - local folder_arg="" - if [[ "$mode" == "folder" ]]; then - load_target "$target" || { ui_msgbox "Failed to load target."; return 0; } - local -a fitems=() - local folders - folders=$(get_target_folders) - while IFS= read -r f; do - [[ -z "$f" ]] && continue - fitems+=("$f" "$f") - done <<< "$folders" - - if [[ ${#fitems[@]} -eq 0 ]]; then - ui_msgbox "No folders defined in target '$target'." - return 0 - fi - - folder_arg=$(ui_menu "Select Folder to Restore" "${fitems[@]}") || return 0 - fi - - # Confirm - local confirm_msg="Restore snapshot?\n\nTarget: $target\nRemote: $remote\nSnapshot: $snapshot" - [[ -n "$folder_arg" ]] && confirm_msg+="\nFolder: $folder_arg" - if [[ "$restore_type" == "inplace" ]]; then - confirm_msg+="\nLocation: In-place (original)" - else - confirm_msg+="\nLocation: $restore_dest" - fi - confirm_msg+="\n" - - ui_yesno "$confirm_msg" || return 0 - - # Run restore - local -a cmd_args=(gniza --cli restore "--target=$target" "--remote=$remote" "--snapshot=$snapshot") - [[ -n "$restore_dest" ]] && cmd_args+=("--dest=$restore_dest") - [[ -n "$folder_arg" ]] && cmd_args+=("--folder=$folder_arg") - - local tmpfile - tmpfile=$(mktemp /tmp/gniza-restore-XXXXXX.log) - - ( - echo "10" - if "${cmd_args[@]}" > "$tmpfile" 2>&1; then - echo "100" - else - echo "100" - fi - ) | ui_gauge "Restoring target: $target" - - if [[ -s "$tmpfile" ]]; then - ui_textbox "$tmpfile" - else - ui_msgbox "Restore of '$target' completed." - fi - - rm -f "$tmpfile" -} diff --git a/lib/ui_retention.sh b/lib/ui_retention.sh deleted file mode 100644 index 77b3580..0000000 --- a/lib/ui_retention.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_retention.sh — Retention cleanup TUI - -[[ -n "${_GNIZA4LINUX_UI_RETENTION_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_RETENTION_LOADED=1 - -ui_retention_menu() { - while true; do - local choice - choice=$(ui_menu "Retention" \ - "SINGLE" "Run cleanup for single target" \ - "ALL" "Run cleanup for all targets" \ - "CONFIG" "Configure retention count" \ - "BACK" "Return to main menu") || return 0 - - case "$choice" in - SINGLE) _ui_retention_single ;; - ALL) _ui_retention_all ;; - CONFIG) _ui_retention_config ;; - BACK) return 0 ;; - esac - done -} - -_ui_retention_single() { - if ! has_targets; then - ui_msgbox "No targets configured." - return 0 - fi - - local -a items=() - local targets - targets=$(list_targets) - while IFS= read -r t; do - items+=("$t" "Target: $t") - done <<< "$targets" - - local target - target=$(ui_menu "Select Target for Cleanup" "${items[@]}") || return 0 - - ui_yesno "Run retention cleanup for target '$target'?" || return 0 - - local tmpfile - tmpfile=$(mktemp /tmp/gniza-retention-XXXXXX.log) - - ( - echo "10" - if gniza --cli retention --target="$target" > "$tmpfile" 2>&1; then - echo "100" - else - echo "100" - fi - ) | ui_gauge "Running retention cleanup: $target" - - if [[ -s "$tmpfile" ]]; then - ui_textbox "$tmpfile" - else - ui_msgbox "Retention cleanup for '$target' completed." - fi - - rm -f "$tmpfile" -} - -_ui_retention_all() { - if ! has_targets; then - ui_msgbox "No targets configured." - return 0 - fi - - ui_yesno "Run retention cleanup for ALL targets?" || return 0 - - local targets - targets=$(list_targets) - local count=0 total=0 - total=$(echo "$targets" | wc -l) - - local output="" - while IFS= read -r t; do - ((count++)) - local pct=$(( count * 100 / total )) - echo "$pct" - local result - if result=$(gniza --cli retention --target="$t" 2>&1); then - output+="$t: OK\n" - else - output+="$t: FAILED\n$result\n" - fi - done <<< "$targets" | ui_gauge "Running retention cleanup..." - - ui_msgbox "Retention Results:\n\n$output" -} - -_ui_retention_config() { - local current="${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT}" - local new_count - new_count=$(ui_inputbox "Retention Config" "Number of snapshots to keep (current: $current):" "$current") || return 0 - - if [[ ! "$new_count" =~ ^[0-9]+$ ]] || (( new_count < 1 )); then - ui_msgbox "Retention count must be a positive integer." - return 0 - fi - - local config_file="$CONFIG_DIR/gniza.conf" - if [[ -f "$config_file" ]] && grep -q "^RETENTION_COUNT=" "$config_file"; then - sed -i "s/^RETENTION_COUNT=.*/RETENTION_COUNT=\"$new_count\"/" "$config_file" - else - echo "RETENTION_COUNT=\"$new_count\"" >> "$config_file" - fi - - RETENTION_COUNT="$new_count" - ui_msgbox "Retention count set to $new_count." -} diff --git a/lib/ui_schedule.sh b/lib/ui_schedule.sh deleted file mode 100644 index 975e712..0000000 --- a/lib/ui_schedule.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_schedule.sh — Schedule management TUI - -[[ -n "${_GNIZA4LINUX_UI_SCHEDULE_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_SCHEDULE_LOADED=1 - -ui_schedule_menu() { - while true; do - local choice - choice=$(ui_menu "Schedules" \ - "LIST" "Show current schedules" \ - "ADD" "Add schedule" \ - "DELETE" "Delete schedule" \ - "INSTALL" "Install schedules to crontab" \ - "REMOVE" "Remove schedules from crontab" \ - "SHOW" "Show crontab entries" \ - "BACK" "Return to main menu") || return 0 - - case "$choice" in - LIST) _ui_schedule_list ;; - ADD) _ui_schedule_add ;; - DELETE) _ui_schedule_delete ;; - INSTALL) _ui_schedule_install ;; - REMOVE) _ui_schedule_remove ;; - SHOW) _ui_schedule_show_cron ;; - BACK) return 0 ;; - esac - done -} - -_ui_schedule_list() { - if ! has_schedules; then - ui_msgbox "No schedules configured." - return 0 - fi - - local schedules - schedules=$(list_schedules) - local info="Configured Schedules:\n\n" - - while IFS= read -r sname; do - [[ -z "$sname" ]] && continue - load_schedule "$sname" 2>/dev/null || continue - info+="[$sname]\n" - info+=" Type: ${SCHEDULE:-not set}\n" - info+=" Time: ${SCHEDULE_TIME:-02:00}\n" - [[ -n "${SCHEDULE_DAY:-}" ]] && info+=" Day: $SCHEDULE_DAY\n" - [[ -n "${SCHEDULE_REMOTES:-}" ]] && info+=" Remotes: $SCHEDULE_REMOTES\n" - [[ -n "${SCHEDULE_TARGETS:-}" ]] && info+=" Targets: $SCHEDULE_TARGETS\n" - info+="\n" - done <<< "$schedules" - - ui_msgbox "$info" -} - -_ui_schedule_add() { - local name - name=$(ui_inputbox "Add Schedule" "Schedule name:" "") || return 0 - [[ -z "$name" ]] && return 0 - - if ! validate_target_name "$name" 2>/dev/null; then - ui_msgbox "Invalid name. Must start with a letter, max 32 chars, [a-zA-Z0-9_-]." - return 0 - fi - - local conf="$CONFIG_DIR/schedules.d/${name}.conf" - if [[ -f "$conf" ]]; then - ui_msgbox "Schedule '$name' already exists." - return 0 - fi - - local stype - stype=$(ui_radiolist "Schedule Type" \ - "hourly" "Every hour" "OFF" \ - "daily" "Once a day" "ON" \ - "weekly" "Once a week" "OFF" \ - "monthly" "Once a month" "OFF" \ - "custom" "Custom cron expression" "OFF") || return 0 - - local stime="02:00" - if [[ "$stype" != "hourly" && "$stype" != "custom" ]]; then - stime=$(ui_inputbox "Schedule Time" "Time (HH:MM, 24h format):" "02:00") || return 0 - fi - - local sday="" - if [[ "$stype" == "weekly" ]]; then - sday=$(ui_inputbox "Day of Week" "Day (0=Sun, 1=Mon, ..., 6=Sat):" "0") || return 0 - elif [[ "$stype" == "monthly" ]]; then - sday=$(ui_inputbox "Day of Month" "Day (1-28):" "1") || return 0 - fi - - local scron="" - if [[ "$stype" == "custom" ]]; then - scron=$(ui_inputbox "Custom Cron" "Enter 5-field cron expression:" "0 2 * * *") || return 0 - fi - - local stargets="" - stargets=$(ui_inputbox "Targets" "Target names (comma-separated, empty=all):" "") || return 0 - - local sremotes="" - sremotes=$(ui_inputbox "Remotes" "Remote names (comma-separated, empty=all):" "") || return 0 - - cat > "$conf" <&1); then - ui_msgbox "Schedules installed.\n\n$result" - else - ui_msgbox "Failed to install schedules.\n\n$result" - fi -} - -_ui_schedule_remove() { - ui_yesno "Remove all gniza schedule entries from crontab?" || return 0 - - local result - if result=$(remove_schedules 2>&1); then - ui_msgbox "$result" - else - ui_msgbox "Failed to remove schedules.\n\n$result" - fi -} - -_ui_schedule_show_cron() { - local result - result=$(show_schedules 2>&1) - ui_msgbox "$result" -} diff --git a/lib/ui_settings.sh b/lib/ui_settings.sh deleted file mode 100644 index cd85e23..0000000 --- a/lib/ui_settings.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_settings.sh — Settings editor TUI - -[[ -n "${_GNIZA4LINUX_UI_SETTINGS_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_SETTINGS_LOADED=1 - -ui_settings_menu() { - local config_file="$CONFIG_DIR/gniza.conf" - - while true; do - local choice - choice=$(ui_menu "Settings" \ - "LOGLEVEL" "Log level: ${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" \ - "EMAIL" "Notification email: ${NOTIFY_EMAIL:-none}" \ - "SMTP_HOST" "SMTP host: ${SMTP_HOST:-none}" \ - "SMTP_PORT" "SMTP port: ${SMTP_PORT:-$DEFAULT_SMTP_PORT}" \ - "SMTP_USER" "SMTP user: ${SMTP_USER:-none}" \ - "SMTP_PASS" "SMTP password: ****" \ - "SMTP_FROM" "SMTP from: ${SMTP_FROM:-none}" \ - "SMTP_SEC" "SMTP security: ${SMTP_SECURITY:-$DEFAULT_SMTP_SECURITY}" \ - "RETENTION" "Default retention: ${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT}" \ - "BWLIMIT" "Default BW limit: ${BWLIMIT:-$DEFAULT_BWLIMIT} KB/s" \ - "BACK" "Return to main menu") || return 0 - - case "$choice" in - LOGLEVEL) - local val - val=$(ui_radiolist "Log Level" \ - "debug" "Debug" "$([ "${LOG_LEVEL:-info}" = "debug" ] && echo ON || echo OFF)" \ - "info" "Info" "$([ "${LOG_LEVEL:-info}" = "info" ] && echo ON || echo OFF)" \ - "warn" "Warning" "$([ "${LOG_LEVEL:-info}" = "warn" ] && echo ON || echo OFF)" \ - "error" "Error" "$([ "${LOG_LEVEL:-info}" = "error" ] && echo ON || echo OFF)") || continue - LOG_LEVEL="$val" - _ui_settings_save "LOG_LEVEL" "$val" "$config_file" - ;; - EMAIL) - local val - val=$(ui_inputbox "Settings" "Notification email:" "${NOTIFY_EMAIL:-}") || continue - NOTIFY_EMAIL="$val" - _ui_settings_save "NOTIFY_EMAIL" "$val" "$config_file" - ;; - SMTP_HOST) - local val - val=$(ui_inputbox "Settings" "SMTP host:" "${SMTP_HOST:-}") || continue - SMTP_HOST="$val" - _ui_settings_save "SMTP_HOST" "$val" "$config_file" - ;; - SMTP_PORT) - local val - val=$(ui_inputbox "Settings" "SMTP port:" "${SMTP_PORT:-$DEFAULT_SMTP_PORT}") || continue - SMTP_PORT="$val" - _ui_settings_save "SMTP_PORT" "$val" "$config_file" - ;; - SMTP_USER) - local val - val=$(ui_inputbox "Settings" "SMTP user:" "${SMTP_USER:-}") || continue - SMTP_USER="$val" - _ui_settings_save "SMTP_USER" "$val" "$config_file" - ;; - SMTP_PASS) - local val - val=$(ui_password "SMTP password:") || continue - SMTP_PASSWORD="$val" - _ui_settings_save "SMTP_PASSWORD" "$val" "$config_file" - ;; - SMTP_FROM) - local val - val=$(ui_inputbox "Settings" "SMTP from address:" "${SMTP_FROM:-}") || continue - SMTP_FROM="$val" - _ui_settings_save "SMTP_FROM" "$val" "$config_file" - ;; - SMTP_SEC) - local val - val=$(ui_radiolist "SMTP Security" \ - "tls" "TLS" "$([ "${SMTP_SECURITY:-tls}" = "tls" ] && echo ON || echo OFF)" \ - "ssl" "SSL" "$([ "${SMTP_SECURITY:-tls}" = "ssl" ] && echo ON || echo OFF)" \ - "none" "None" "$([ "${SMTP_SECURITY:-tls}" = "none" ] && echo ON || echo OFF)") || continue - SMTP_SECURITY="$val" - _ui_settings_save "SMTP_SECURITY" "$val" "$config_file" - ;; - RETENTION) - local val - val=$(ui_inputbox "Settings" "Default retention count:" "${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT}") || continue - if [[ ! "$val" =~ ^[0-9]+$ ]] || (( val < 1 )); then - ui_msgbox "Retention count must be a positive integer." - continue - fi - RETENTION_COUNT="$val" - _ui_settings_save "RETENTION_COUNT" "$val" "$config_file" - ;; - BWLIMIT) - local val - val=$(ui_inputbox "Settings" "Default bandwidth limit (KB/s, 0=unlimited):" "${BWLIMIT:-$DEFAULT_BWLIMIT}") || continue - if [[ ! "$val" =~ ^[0-9]+$ ]]; then - ui_msgbox "Bandwidth limit must be a non-negative integer." - continue - fi - BWLIMIT="$val" - _ui_settings_save "BWLIMIT" "$val" "$config_file" - ;; - BACK) return 0 ;; - esac - done -} - -_ui_settings_save() { - local key="$1" - local value="$2" - local config_file="$3" - - # Ensure config file exists - [[ -f "$config_file" ]] || touch "$config_file" - - if grep -q "^${key}=" "$config_file"; then - # Use awk to avoid sed delimiter injection issues - local tmpconf - tmpconf=$(mktemp) - awk -v k="$key" -v v="$value" 'BEGIN{FS=OFS="="} $1==k{print k "=\"" v "\""; next} {print}' "$config_file" > "$tmpconf" - mv "$tmpconf" "$config_file" - else - printf '%s="%s"\n' "$key" "$value" >> "$config_file" - fi -} diff --git a/lib/ui_snapshots.sh b/lib/ui_snapshots.sh deleted file mode 100644 index 3dca049..0000000 --- a/lib/ui_snapshots.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_snapshots.sh — Snapshot browsing TUI - -[[ -n "${_GNIZA4LINUX_UI_SNAPSHOTS_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_SNAPSHOTS_LOADED=1 - -ui_snapshots_menu() { - if ! has_targets; then - ui_msgbox "No targets configured." - return 0 - fi - - # Select target - local -a titems=() - local targets - targets=$(list_targets) - while IFS= read -r t; do - titems+=("$t" "Target: $t") - done <<< "$targets" - - local target - target=$(ui_menu "Select Target" "${titems[@]}") || return 0 - - # Select remote - if ! has_remotes; then - ui_msgbox "No remotes configured." - return 0 - fi - - local -a ritems=() - local remotes - remotes=$(list_remotes) - while IFS= read -r r; do - ritems+=("$r" "Remote: $r") - done <<< "$remotes" - - local remote - remote=$(ui_menu "Select Remote" "${ritems[@]}") || return 0 - - load_remote "$remote" || { ui_msgbox "Failed to load remote '$remote'."; return 0; } - - # List snapshots - while true; do - local snapshots - snapshots=$(list_remote_snapshots "$target" 2>/dev/null) - if [[ -z "$snapshots" ]]; then - ui_msgbox "No snapshots found for target '$target' on remote '$remote'." - return 0 - fi - - local -a sitems=() - while IFS= read -r s; do - sitems+=("$s" "Snapshot: $s") - done <<< "$snapshots" - sitems+=("BACK" "Return") - - local snapshot - snapshot=$(ui_menu "Snapshots: $target @ $remote" "${sitems[@]}") || return 0 - - [[ "$snapshot" == "BACK" ]] && return 0 - - # Snapshot detail menu - while true; do - local snap_dir - snap_dir=$(get_snapshot_dir "$target") - local meta_info="Snapshot: $snapshot\nTarget: $target\nRemote: $remote" - - # Try to read meta.json - local meta_content="" - if [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then - if [[ -f "$snap_dir/$snapshot/meta.json" ]]; then - meta_content=$(cat "$snap_dir/$snapshot/meta.json" 2>/dev/null) - fi - fi - [[ -n "$meta_content" ]] && meta_info+="\n\n$meta_content" - - local action - action=$(ui_menu "Snapshot: $snapshot" \ - "DETAILS" "View details" \ - "DELETE" "Delete snapshot" \ - "BACK" "Back to list") || break - - case "$action" in - DETAILS) - ui_msgbox "$meta_info" - ;; - DELETE) - if ui_yesno "Delete snapshot '$snapshot'?\nThis cannot be undone."; then - local snap_path_del - snap_path_del=$(get_snapshot_dir "$target") - local del_ok=false - if _is_rclone_mode; then - rclone_purge "targets/${target}/snapshots/${snapshot}" 2>/dev/null && del_ok=true - elif [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then - rm -rf "$snap_path_del/$snapshot" 2>/dev/null && del_ok=true - else - remote_exec "rm -rf '$snap_path_del/$snapshot'" 2>/dev/null && del_ok=true - fi - if [[ "$del_ok" == "true" ]]; then - ui_msgbox "Snapshot '$snapshot' deleted." - else - ui_msgbox "Failed to delete snapshot '$snapshot'." - fi - break - fi - ;; - BACK) break ;; - esac - done - done -} diff --git a/lib/ui_targets.sh b/lib/ui_targets.sh deleted file mode 100644 index d610346..0000000 --- a/lib/ui_targets.sh +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_targets.sh — Target management TUI - -[[ -n "${_GNIZA4LINUX_UI_TARGETS_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_TARGETS_LOADED=1 - -ui_targets_menu() { - while true; do - local -a items=() - local targets - targets=$(list_targets) - if [[ -n "$targets" ]]; then - while IFS= read -r t; do - items+=("$t" "Target: $t") - done <<< "$targets" - fi - items+=("ADD" "Add new target") - items+=("BACK" "Return to main menu") - - local choice - choice=$(ui_menu "Targets" "${items[@]}") || return 0 - - case "$choice" in - ADD) ui_target_add ;; - BACK) return 0 ;; - *) - local action - action=$(ui_menu "Target: $choice" \ - "EDIT" "Edit target" \ - "DELETE" "Delete target" \ - "BACK" "Back") || continue - case "$action" in - EDIT) ui_target_edit "$choice" ;; - DELETE) ui_target_delete "$choice" ;; - BACK) continue ;; - esac - ;; - esac - done -} - -ui_target_add() { - local name - name=$(ui_inputbox "Add Target" "Enter target name (letters, digits, _ -). A folder browser will open next:" "") || return 0 - [[ -z "$name" ]] && return 0 - - if ! validate_target_name "$name" 2>/dev/null; then - ui_msgbox "Invalid target name. Must start with a letter and contain only letters, digits, underscore, or hyphen (max 32 chars)." - return 0 - fi - - if [[ -f "$CONFIG_DIR/targets.d/${name}.conf" ]]; then - ui_msgbox "Target '$name' already exists." - return 0 - fi - - local folders - folders=$(ui_target_folder_picker) || return 0 - [[ -z "$folders" ]] && { ui_msgbox "No folders selected. Target not created."; return 0; } - - local exclude - exclude=$(ui_inputbox "Add Target" "Exclude patterns (comma-separated, e.g. *.log,*.tmp):" "") || exclude="" - - local remote - remote=$(ui_inputbox "Add Target" "Remote override (leave empty for default):" "") || remote="" - - create_target "$name" "$folders" "$exclude" "$remote" - ui_msgbox "Target '$name' created successfully." -} - -ui_target_edit() { - local name="$1" - load_target "$name" || { ui_msgbox "Failed to load target '$name'."; return 0; } - - while true; do - local choice - choice=$(ui_menu "Edit Target: $name" \ - "FOLDERS" "Folders: ${TARGET_FOLDERS}" \ - "EXCLUDE" "Exclude: ${TARGET_EXCLUDE:-none}" \ - "REMOTE" "Remote: ${TARGET_REMOTE:-default}" \ - "ENABLED" "Enabled: ${TARGET_ENABLED}" \ - "SAVE" "Save and return" \ - "BACK" "Cancel") || return 0 - - case "$choice" in - FOLDERS) - local folders - folders=$(ui_target_folder_picker "$TARGET_FOLDERS") || continue - [[ -n "$folders" ]] && TARGET_FOLDERS="$folders" - ;; - EXCLUDE) - local exclude - exclude=$(ui_inputbox "Edit Exclude" "Exclude patterns (comma-separated):" "$TARGET_EXCLUDE") || continue - TARGET_EXCLUDE="$exclude" - ;; - REMOTE) - local remote - remote=$(ui_inputbox "Edit Remote" "Remote override (leave empty for default):" "$TARGET_REMOTE") || continue - TARGET_REMOTE="$remote" - ;; - ENABLED) - if ui_yesno "Enable this target?"; then - TARGET_ENABLED="yes" - else - TARGET_ENABLED="no" - fi - ;; - SAVE) - create_target "$name" "$TARGET_FOLDERS" "$TARGET_EXCLUDE" "$TARGET_REMOTE" \ - "$TARGET_RETENTION" "$TARGET_PRE_HOOK" "$TARGET_POST_HOOK" "$TARGET_ENABLED" - ui_msgbox "Target '$name' saved." - return 0 - ;; - BACK) return 0 ;; - esac - done -} - -ui_target_delete() { - local name="$1" - if ui_yesno "Delete target '$name'? This cannot be undone."; then - delete_target "$name" - ui_msgbox "Target '$name' deleted." - fi -} - -ui_target_folder_picker() { - local existing="${1:-}" - local -a folders=() - - if [[ -n "$existing" ]]; then - IFS=',' read -ra folders <<< "$existing" - fi - - # If no existing folders, open file browser immediately - if [[ ${#folders[@]} -eq 0 ]]; then - local path - path=$(gum file --directory --header "Select folder to back up (Esc when done)" \ - --cursor.foreground "$_GUM_ACCENT" --height 15 /) || return 1 - [[ -z "$path" ]] && return 1 - [[ "$path" != /* ]] && path="/$path" - folders+=("$path") - fi - - while true; do - # Show current selection and options - local selected_list="" - local i - for i in "${!folders[@]}"; do - selected_list+=" $(( i + 1 )). ${folders[$i]}\n" - done - - local action - action=$(ui_menu "Selected folders:\n${selected_list}" \ - "ADD" "Add another folder" \ - "REMOVE" "Remove a folder" \ - "DONE" "Done — use these ${#folders[@]} folder(s)") || return 1 - - case "$action" in - ADD) - local path - path=$(gum file --directory --header "Select folder to back up" \ - --cursor.foreground "$_GUM_ACCENT" --height 15 /) || continue - [[ -z "$path" ]] && continue - [[ "$path" != /* ]] && path="/$path" - # Avoid duplicates - local dup=false - for f in "${folders[@]}"; do - [[ "$f" == "$path" ]] && dup=true - done - if $dup; then - ui_msgbox "Folder '$path' is already selected." - else - folders+=("$path") - fi - ;; - REMOVE) - local -a rm_items=() - local j=0 - for f in "${folders[@]}"; do - rm_items+=("$j" "$f") - ((j++)) - done - local idx - idx=$(ui_menu "Remove which folder?" "${rm_items[@]}") || continue - unset 'folders[idx]' - folders=("${folders[@]}") - if [[ ${#folders[@]} -eq 0 ]]; then - ui_msgbox "All folders removed. Please select at least one." - local path - path=$(gum file --directory --header "Select folder to back up" \ - --cursor.foreground "$_GUM_ACCENT" --height 15 /) || return 1 - [[ -z "$path" ]] && return 1 - [[ "$path" != /* ]] && path="/$path" - folders+=("$path") - fi - ;; - DONE) - local result - result=$(IFS=','; echo "${folders[*]}") - echo "$result" - return 0 - ;; - esac - done -} diff --git a/lib/ui_wizard.sh b/lib/ui_wizard.sh deleted file mode 100644 index 617d14e..0000000 --- a/lib/ui_wizard.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# gniza4linux/lib/ui_wizard.sh — First-time setup wizard - -[[ -n "${_GNIZA4LINUX_UI_WIZARD_LOADED:-}" ]] && return 0 -_GNIZA4LINUX_UI_WIZARD_LOADED=1 - -ui_first_run_wizard() { - # Step 1: Welcome - ui_msgbox "Welcome to gniza Backup Manager!\n\nThis wizard will help you set up your first backup:\n\n 1. Configure a backup destination (remote)\n 2. Define what to back up (target)\n 3. Optionally run your first backup\n\nPress OK to start, or Cancel to skip." \ - || return 0 - - # Step 2: Create first remote (skip if one already exists) - if ! has_remotes; then - ui_msgbox "Step 1: Configure Backup Destination\n\nChoose where your backups will be stored:\n - SSH server\n - Local directory (USB/NFS)\n - Amazon S3\n - Google Drive" - - local remote_created=false - while ! $remote_created; do - ui_remote_add - if has_remotes; then - remote_created=true - else - if ! ui_yesno "No remote was created.\n\nWould you like to try again?"; then - ui_msgbox "You can configure remotes later from the main menu.\n\nSetup wizard exiting." - return 0 - fi - fi - done - fi - - # Step 3: Create first target (skip if one already exists) - if ! has_targets; then - ui_msgbox "Step 2: Define Backup Target\n\nChoose a name for your backup profile and select the folders you want to back up." - - local target_created=false - while ! $target_created; do - ui_target_add - if has_targets; then - target_created=true - else - if ! ui_yesno "No target was created.\n\nWould you like to try again?"; then - ui_msgbox "You can configure targets later from the main menu.\n\nSetup wizard exiting." - return 0 - fi - fi - done - fi - - # Step 4: Optionally run first backup (only if both exist) - if has_remotes && has_targets; then - local target - target=$(list_targets | head -1) - local remote - remote=$(list_remotes | head -1) - - if ui_yesno "Run First Backup?\n\nTarget: $target\nRemote: $remote\n\nRun your first backup now?"; then - _ui_run_backup "$target" "$remote" - fi - fi - - # Done - ui_msgbox "Setup complete!\n\nYou can manage your backups from the main menu:\n - Add more targets and remotes\n - Schedule automatic backups\n - Browse and restore snapshots" -} diff --git a/scripts/install.sh b/scripts/install.sh index 52092c5..f0cc386 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -74,55 +74,6 @@ for cmd in ssh curl; do fi done -# ── Install bundled gum binary ─────────────────────────────── -GUM_VERSION="0.17.0" - -_detect_arch() { - local arch; arch=$(uname -m) - case "$arch" in - x86_64) echo "x86_64" ;; - aarch64) echo "arm64" ;; - armv7*) echo "armv7" ;; - i386|i686) echo "i386" ;; - *) echo "" ;; - esac -} - -if command -v gum &>/dev/null; then - info "gum already installed: $(command -v gum)" -else - GUM_ARCH=$(_detect_arch) - if [[ -n "$GUM_ARCH" ]] && command -v curl &>/dev/null; then - GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_${GUM_ARCH}.tar.gz" - info "Downloading gum v${GUM_VERSION} (${GUM_ARCH})..." - GUM_TMP=$(mktemp -d) - if curl -fsSL "$GUM_URL" | tar xz -C "$GUM_TMP" 2>/dev/null; then - mkdir -p "$INSTALL_DIR/bin" - # The tarball extracts to a subdirectory - if [[ -f "$GUM_TMP/gum_${GUM_VERSION}_Linux_${GUM_ARCH}/gum" ]]; then - cp "$GUM_TMP/gum_${GUM_VERSION}_Linux_${GUM_ARCH}/gum" "$INSTALL_DIR/bin/gum" - elif [[ -f "$GUM_TMP/gum" ]]; then - cp "$GUM_TMP/gum" "$INSTALL_DIR/bin/gum" - fi - if [[ -f "$INSTALL_DIR/bin/gum" ]]; then - chmod +x "$INSTALL_DIR/bin/gum" - info "Installed gum to $INSTALL_DIR/bin/gum" - else - warn "Failed to locate gum binary in archive" - fi - else - warn "Failed to download gum (TUI will not be available)" - fi - rm -rf "$GUM_TMP" - else - if [[ -z "${GUM_ARCH:-}" ]]; then - warn "Unsupported architecture for gum: $(uname -m)" - else - warn "curl not found, cannot download gum (TUI will not be available)" - fi - fi -fi - # ── Install files ──────────────────────────────────────────── info "Installing to $INSTALL_DIR..." mkdir -p "$INSTALL_DIR"