Files
docker-podman-installation-…/scripts/lib/common.sh
T
duffyduckandClaude Opus 5 7fb0c6ea9e "manifest unknown" erklaeren - und eine Pipe-Falle beseitigen, die das
Erkennen verhindert hat

Der Pull meldete "manifest unknown". Das heisst, anders als zuvor: der
Repository-Pfad registry.checkmk.com/enterprise/check-mk-enterprise ist
richtig und die Anmeldung auch - nur das Tag 2.4.0p9 gibt es dort nicht.

Neu "image why [REF]" unterscheidet die drei Faelle anhand der Registry
statt anhand der Fehlermeldung:
    401/403  Anmeldung oder Berechtigung
    404      Repository gibt es nicht        -> image repos
    Tag      Repository ja, Tag nein         -> vorhandene Tags des
             Zweigs werden aufgelistet
"up" ruft das von selbst auf, wenn das Starten scheitert und das Image
nicht lokal vorliegt; "image pull" ebenfalls.

Beim Bauen stellte sich heraus, dass die Tag-Pruefung immer "nicht
vorhanden" meldete - auch fuer Tags, die es gibt. Ursache ist eine
Falle, die das ganze Projekt betraf:

    printf ... | grep -q muster

"grep -q" endet beim ersten Treffer. Der Erzeuger bekommt dann SIGPIPE,
und unter "set -o pipefail" gilt die ganze Pipe als fehlgeschlagen -
aber nur, wenn er noch am Schreiben war. Bei drei Zeilen faellt das nie
auf, bei 2393 Tags immer. Nachgestellt:

    set -euo pipefail; seq 1 500000 | grep -qx 3   -> schlaegt fehl

Betroffen waren zwoelf Stellen, darunter zwei, die ernster sind als die
Tag-Liste: bundle_read prueft mit "tar -tzf ... | grep -qx", ob ein
Backup ein Bundle ist. Bei einem grossen Archiv haette ein gueltiges
Bundle als "kein Bundle" gegolten - also ein stiller Fehler bei Restore
und Import genau dann, wenn das Backup gross ist. Alle zwoelf laufen
jetzt in einer Subshell ohne pipefail.

Der Selbsttest hat dafuer eine eigene Pruefung bekommen, damit die
Schreibweise nicht zurueckkehrt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 18:44:08 +02:00

510 lines
19 KiB
Bash

#!/usr/bin/env bash
# =====================================================================
# common.sh - gemeinsame Funktionen fuer cmk-manage.sh
# Wird per "source" eingebunden, nicht direkt ausgefuehrt.
# =====================================================================
# ---------------------------------------------------------------------
# Ausgabe / Logging
# ---------------------------------------------------------------------
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'
C_BLU=$'\033[34m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m'
else
C_RED=; C_GRN=; C_YEL=; C_BLU=; C_BLD=; C_RST=
fi
log() { printf '%s[ %s ]%s %s\n' "$C_BLU" "INFO" "$C_RST" "$*"; }
ok() { printf '%s[ OK ]%s %s\n' "$C_GRN" "$C_RST" "$*"; }
warn() { printf '%s[ WARN ]%s %s\n' "$C_YEL" "$C_RST" "$*" >&2; }
err() { printf '%s[FEHLER]%s %s\n' "$C_RED" "$C_RST" "$*" >&2; }
die() { err "$*"; exit 1; }
debug() { [ -n "${VERBOSE:-}" ] && printf ' > %s\n' "$*" >&2 || true; }
# ---------------------------------------------------------------------
# .env laden
# ---------------------------------------------------------------------
load_env() {
local envfile="${1:-$PROJECT_DIR/.env}"
if [ -f "$envfile" ]; then
set -a
# shellcheck disable=SC1090
. "$envfile"
set +a
ENV_FILE="$envfile"
else
warn "Keine .env gefunden ($envfile) - es gelten die Standardwerte."
ENV_FILE=""
fi
# Rueckfallwerte, falls ein Schluessel in der .env fehlt.
# Die .env bleibt die massgebliche Konfiguration - hier steht nur,
# was gilt, wenn dort nichts eingetragen ist.
CMK_CONTAINER_NAME="${CMK_CONTAINER_NAME:-checkmk}"
NPM_CONTAINER_NAME="${NPM_CONTAINER_NAME:-nginx-proxy-manager}"
CMK_SITE_ID="${CMK_SITE_ID:-cmk}"
CMK_IMAGE="${CMK_IMAGE:-docker.io/checkmk/check-mk-raw:2.3.0-latest}"
NPM_ENABLED="${NPM_ENABLED:-on}"
BACKUP_AUTO="${BACKUP_AUTO:-off}"
PROXY_FORWARD_HOST="${PROXY_FORWARD_HOST:-127.0.0.1}"
PROXY_FORWARD_PORT="${PROXY_FORWARD_PORT:-5000}"
NPM_ADMIN_PORT="${NPM_ADMIN_PORT:-81}"
# Leer = automatisch aus dem Admin-Port ableiten
NPM_ADMIN_URL="${NPM_ADMIN_URL:-http://127.0.0.1:$NPM_ADMIN_PORT}"
BACKUP_DIR="$(abs_path "${BACKUP_DIR:-$PROJECT_DIR/backups}")"
IMAGE_DIR="$(abs_path "${IMAGE_DIR:-$PROJECT_DIR/images}")"
}
# relativen Pfad (./foo) am Projektverzeichnis ausrichten
abs_path() {
case "$1" in
/*) printf '%s\n' "$1" ;;
*) printf '%s\n' "$PROJECT_DIR/${1#./}" ;;
esac
}
# .env-Wert dauerhaft setzen
# env_set KEY WERT [allow_empty]
env_set() {
local key="$1" val="$2" allow_empty="${3:-}" f="${ENV_FILE:-$PROJECT_DIR/.env}"
[ -f "$f" ] || die "Keine .env vorhanden - kann '$key' nicht setzen."
# mehrzeilige Werte wuerden die .env zerstoeren
case "$val" in
*$'\n'*) die "Ungueltiger Wert fuer '$key' (mehrzeilig)." ;;
esac
[ -n "$val" ] || [ -n "$allow_empty" ] || die "Leerer Wert fuer '$key' - abgebrochen."
# Werte mit Leerzeichen in Anfuehrungszeichen setzen
case "$val" in
*" "*) case "$val" in \"*\") : ;; *) val="\"$val\"" ;; esac ;;
esac
if grep -qE "^[#[:space:]]*${key}=" "$f"; then
sed -i -E "s|^[#[:space:]]*${key}=.*|${key}=${val}|" "$f"
else
printf '%s=%s\n' "$key" "$val" >>"$f"
fi
export "$key=$val"
}
# ---------------------------------------------------------------------
# Container-Runtime erkennen (docker / podman)
# ---------------------------------------------------------------------
detect_runtime() {
if [ -n "${CMK_RUNTIME:-}" ] && command -v "$CMK_RUNTIME" >/dev/null 2>&1; then
RUNTIME="$CMK_RUNTIME"
elif [ -n "${CMK_RUNTIME:-}" ]; then
# Falsch gesetzter Wert darf das Werkzeug nicht unbenutzbar machen
warn "CMK_RUNTIME='$CMK_RUNTIME' ist nicht installiert - suche selbst.
Korrigieren mit: cmk-manage.sh config set CMK_RUNTIME auto"
CMK_RUNTIME=""
detect_runtime
return
elif command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then
RUNTIME=podman
elif command -v docker >/dev/null 2>&1; then
RUNTIME=docker # vorhanden, aber Daemon evtl. gestoppt
else
die "Weder docker noch podman gefunden. Bitte zuerst ./install.sh ausfuehren."
fi
debug "Runtime: $RUNTIME"
}
# Compose-Kommando ermitteln (als Array COMPOSE_CMD)
detect_compose() {
COMPOSE_CMD=()
if [ "$RUNTIME" = docker ]; then
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
fi
else
if command -v podman-compose >/dev/null 2>&1; then
COMPOSE_CMD=(podman-compose)
elif podman compose version >/dev/null 2>&1; then
COMPOSE_CMD=(podman compose)
fi
fi
[ ${#COMPOSE_CMD[@]} -gt 0 ] || die "Kein Compose-Kommando gefunden (docker compose / podman-compose)."
debug "Compose: ${COMPOSE_CMD[*]}"
}
compose() {
detect_compose
( cd "$PROJECT_DIR" && "${COMPOSE_CMD[@]}" "$@" )
}
# ---------------------------------------------------------------------
# Container-Helfer
# ---------------------------------------------------------------------
# Ist der NGINX Proxy Manager eingeschaltet? (on/true/1/yes = ja)
npm_enabled() {
case "$(printf '%s' "${NPM_ENABLED:-on}" | tr 'A-Z' 'a-z')" in
on|true|1|yes|ja|y) return 0 ;;
*) return 1 ;;
esac
}
container_exists() { $RUNTIME container inspect "$1" >/dev/null 2>&1; }
container_running() { [ "$($RUNTIME container inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" = "true" ]; }
require_cmk_running() {
container_exists "$CMK_CONTAINER_NAME" \
|| die "Container '$CMK_CONTAINER_NAME' existiert nicht. Zuerst: $0 up"
container_running "$CMK_CONTAINER_NAME" \
|| die "Container '$CMK_CONTAINER_NAME' laeuft nicht. Zuerst: $0 up"
}
# Kommando im Checkmk-Container als root ausfuehren
ct() { $RUNTIME exec "$CMK_CONTAINER_NAME" "$@"; }
# ... mit stdin
cti() { $RUNTIME exec -i "$CMK_CONTAINER_NAME" "$@"; }
# ---------------------------------------------------------------------
# OMD-Helfer (im Container)
# ---------------------------------------------------------------------
cmk_sites() {
ct sh -c 'ls -1 /omd/sites 2>/dev/null' | sed '/^$/d' || true
}
cmk_site_exists() {
( set +o pipefail; cmk_sites | grep -qxF "$1" )
}
cmk_site_version() {
ct omd version -b "$1" 2>/dev/null | tr -d '\r' || true
}
# 0 = laeuft komplett, 1 = teilweise, 2 = gestoppt
cmk_site_state() {
local rc=0
ct omd status --bare "$1" >/dev/null 2>&1 || rc=$?
echo "$rc"
}
cmk_site_state_text() {
case "$(cmk_site_state "$1")" in
0) echo "laeuft" ;;
1) echo "teilweise" ;;
*) echo "gestoppt" ;;
esac
}
cmk_versions_installed() {
ct omd versions -b 2>/dev/null | tr -d '\r' || ct sh -c 'ls -1 /omd/versions | grep -v "^default$"' 2>/dev/null || true
}
# ---------------------------------------------------------------------
# Diverses
# ---------------------------------------------------------------------
human_size() {
local b="${1:-0}"
if [ "$b" -ge 1073741824 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f GiB", b/1073741824}'
elif [ "$b" -ge 1048576 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f MiB", b/1048576}'
elif [ "$b" -ge 1024 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f KiB", b/1024}'
else echo "${b} B"; fi
}
file_size() { stat -c %s "$1" 2>/dev/null || echo 0; }
confirm() {
[ -n "${ASSUME_YES:-}" ] && return 0
local prompt="${1:-Fortfahren?} [j/N] " a
read -r -p "$prompt" a </dev/tty || return 1
case "$a" in [jJyY]*) return 0 ;; *) return 1 ;; esac
}
primary_ip() {
ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1);exit}}' \
|| hostname -I 2>/dev/null | awk '{print $1}'
}
# =====================================================================
# TUI-Helfer (dialog bevorzugt, sonst whiptail) - ncurses wie im mc
# =====================================================================
UI_BACKTITLE="Checkmk Stack Manager - Container-Runtime: ${RUNTIME:-?}"
# Groesse des Terminals ermitteln (mit Rueckfallwerten)
ui_term_size() {
UI_ROWS="$(tput lines 2>/dev/null || true)"
UI_COLS="$(tput cols 2>/dev/null || true)"
case "$UI_ROWS" in ''|*[!0-9]*) UI_ROWS=24 ;; esac
case "$UI_COLS" in ''|*[!0-9]*) UI_COLS=80 ;; esac
if [ "$UI_ROWS" -lt 10 ]; then UI_ROWS=24; fi
if [ "$UI_COLS" -lt 40 ]; then UI_COLS=80; fi
return 0
}
# Fenstergroesse aus Textlaenge, Anzahl der Eintraege und Terminalgroesse
# berechnen. Vorher waren die Werte fest - bei langem Text blieb dann nur
# eine Zeile fuer die Liste uebrig.
#
# ACHTUNG: hier bewusst mit if-Bloecken statt "[ ... ] && VAR=...".
# Letzteres liefert 1, wenn die Bedingung nicht zutrifft, und beendet
# unter "set -e" die Funktion bzw. das ganze Skript.
#
# ui_calc TEXT FELDER_JE_EINTRAG EINTRAEGE...
ui_calc() {
local text="$1" per="$2"; shift 2
ui_term_size
local tl maxw
tl="$(printf '%s\n' "$text" | wc -l)"
maxw="$(printf '%s\n' "$text" | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')"
# breiteste Zeile der Liste (Kennung und Beschriftung stehen nebeneinander)
local -a args=("$@")
local i=0 w itemw=0 n=0
while [ "$i" -lt "${#args[@]}" ]; do
w=$(( ${#args[$i]} + 4 ))
if [ $((i+1)) -lt "${#args[@]}" ]; then
w=$(( w + ${#args[$((i+1))]} ))
fi
if [ "$w" -gt "$itemw" ]; then itemw="$w"; fi
i=$(( i + per )); n=$(( n + 1 ))
done
# Breite: der breitere von Text und Liste, plus Rahmen
if [ "$maxw" -gt "$itemw" ]; then UI_W=$(( maxw + 10 )); else UI_W=$(( itemw + 10 )); fi
if [ "$UI_W" -lt 60 ]; then UI_W=60; fi
if [ "$UI_W" -gt $(( UI_COLS - 4 )) ]; then UI_W=$(( UI_COLS - 4 )); fi
# Listenhoehe: moeglichst alle Eintraege zeigen, ohne aus dem Bild zu laufen
local avail=$(( UI_ROWS - tl - 9 ))
if [ "$avail" -lt 3 ]; then avail=3; fi
if [ "$n" -lt "$avail" ]; then UI_MH="$n"; else UI_MH="$avail"; fi
if [ "$UI_MH" -lt 1 ]; then UI_MH=1; fi
UI_H=$(( tl + UI_MH + 8 ))
if [ "$UI_H" -gt $(( UI_ROWS - 2 )) ]; then UI_H=$(( UI_ROWS - 2 )); fi
if [ "$UI_H" -lt 10 ]; then UI_H=10; fi
return 0
}
# Hoehe fuer Fenster ohne Liste (Meldung, Abfrage, Eingabe)
ui_calc_text() { # text zusatzzeilen
local text="$1" extra="${2:-7}"
ui_term_size
local tl maxw
tl="$(printf '%s\n' "$text" | wc -l)"
maxw="$(printf '%s\n' "$text" | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')"
UI_W=$(( maxw + 10 ))
if [ "$UI_W" -lt 60 ]; then UI_W=60; fi
if [ "$UI_W" -gt $(( UI_COLS - 4 )) ]; then UI_W=$(( UI_COLS - 4 )); fi
UI_H=$(( tl + extra ))
if [ "$UI_H" -gt $(( UI_ROWS - 2 )) ]; then UI_H=$(( UI_ROWS - 2 )); fi
if [ "$UI_H" -lt 8 ]; then UI_H=8; fi
return 0
}
# ---------------------------------------------------------------------
# Rahmenzeichen in PuTTY & Co.
#
# Ohne UTF-8-Locale zeichnen dialog/whiptail die Rahmen ueber die alte
# VT100-Umschaltung "ESC(0". Terminals, die im UTF-8-Modus laufen -
# PuTTY in der Vorgabe -, ignorieren diese Umschaltung und zeigen
# stattdessen die rohen Buchstaben q, x, l, m.
#
# Nachgemessen: bei LC_ALL=C schickt whiptail eine ESC(0-Umschaltung und
# 91 rohe "q"; mit UTF-8-Locale keine einzige. Es genuegt, LC_CTYPE zu
# setzen - Sortierung und Zahlenformat bleiben damit unberuehrt.
# Zwingt ncurses zu echten Unicode-Rahmenzeichen.
#
# Auch mit UTF-8-Locale benutzt ncurses die VT100-Umschaltung ESC(0,
# solange das terminfo sie anbietet (bei TERM=xterm der Fall). Terminals
# im UTF-8-Modus - PuTTY - machen die nicht mit und zeigen q, x, l, m.
# Gemessen mit dialog unter LANG=C.UTF-8, TERM=xterm:
# ohne Schalter : 38 ESC(0, 0 Rahmenbytes, 182 rohe q
# mit Schalter : 0 ESC(0, 220 Rahmenbytes, 0 rohe q
# whiptail (slang) braucht ihn nicht, es zeichnet bei UTF-8-Locale schon
# von sich aus Unicode.
ui_use_unicode_acs() {
# CMK_TUI_ACS=1 erzwingt umgekehrt das alte Verhalten - fuer
# Terminals, die ESC(0 koennen, aber kein UTF-8.
if [ -n "${CMK_TUI_ACS:-}" ]; then
unset NCURSES_NO_UTF8_ACS 2>/dev/null || true
return 0
fi
export NCURSES_NO_UTF8_ACS=1
return 0
}
ui_fix_locale() {
[ -n "${UI_LOCALE_DONE:-}" ] && return 0
UI_LOCALE_DONE=1
UI_LOCALE_NOTE=""
case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
*UTF-8*|*utf-8*|*UTF8*|*utf8*) ui_use_unicode_acs; return 0 ;;
esac
local l
for l in C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8 de_DE.UTF-8 de_DE.utf8; do
if ( set +o pipefail; locale -a 2>/dev/null | grep -qix "$l" ); then
# LC_ALL sticht jede einzelne Kategorie aus - solange es auf
# einem Nicht-UTF-8-Wert steht, bliebe LC_CTYPE wirkungslos.
if [ -n "${LC_ALL:-}" ]; then unset LC_ALL; fi
export LC_CTYPE="$l"
UI_LOCALE_NOTE="LC_CTYPE auf $l gesetzt (war nicht UTF-8)"
ui_use_unicode_acs
return 0
fi
done
# Keine UTF-8-Locale vorhanden - dann bleibt nur ASCII
UI_ASCII=1
UI_LOCALE_NOTE="keine UTF-8-Locale gefunden - einfache Rahmen"
return 1
}
# Zusaetzliche Argumente, wenn nur ASCII-Rahmen moeglich sind
ui_ascii_args() {
UI_EXTRA=()
case "${CMK_TUI_ASCII:-}${UI_ASCII:-}" in
'') return 0 ;;
esac
if [ "${UI_BIN:-}" = dialog ] && dialog --ascii-lines --print-version >/dev/null 2>&1; then
UI_EXTRA=(--ascii-lines)
elif [ -z "${UI_ASCII_WARNED:-}" ]; then
UI_ASCII_WARNED=1
warn "Einfache Rahmen kann nur 'dialog' zeichnen - 'whiptail' kennt
die Option nicht. Installieren mit:
dnf install -y dialog (RHEL/Rocky/Alma)
apt-get install -y dialog (Debian/Ubuntu)"
fi
return 0
}
ui_init() {
ui_fix_locale || true
# Geometrie IMMER setzen - sonst laeuft das Skript unter "set -u" auf
# eine unbound variable, wenn UI_BIN von aussen vorgegeben wurde.
UI_H="${UI_H:-22}"; UI_W="${UI_W:-78}"; UI_MH="${UI_MH:-13}"
if [ -z "${UI_BIN:-}" ]; then
if command -v dialog >/dev/null 2>&1; then UI_BIN=dialog
elif command -v whiptail >/dev/null 2>&1; then UI_BIN=whiptail
else UI_BIN=""; fi
export DIALOGRC="${DIALOGRC:-}"
fi
# Einfache Rahmen koennen nur mit dialog gezeichnet werden. Ist das
# gewuenscht und dialog vorhanden, wird es dafuer vorgezogen.
if [ -n "${CMK_TUI_ASCII:-}${UI_ASCII:-}" ] && command -v dialog >/dev/null 2>&1; then
UI_BIN=dialog
fi
ui_ascii_args
}
ui_available() { ui_init; [ -n "$UI_BIN" ]; }
ui_require() {
ui_available || die "Fuer die TUI wird 'dialog' oder 'whiptail' benoetigt.
Debian/Ubuntu : apt-get install -y dialog
RHEL/Rocky : dnf install -y dialog"
UI_BACKTITLE="Checkmk Stack Manager - Runtime: $RUNTIME"
}
# =====================================================================
# Alle Dialoge laufen ueber genau drei Engstellen.
#
# Damit stimmt die Fenstergroesse automatisch - auch fuer Dialoge, die
# spaeter dazukommen. NEUE FENSTER BITTE IMMER UEBER DIESE DREI BAUEN
# und niemals "$UI_BIN" direkt aufrufen; "cmk-manage.sh selftest"
# prueft das nach.
#
# _ui_list Listenfenster (menu, checklist, radiolist) -> ui_calc
# _ui_box Textfenster (msgbox, yesno, inputbox, ...) -> ui_calc_text
# _ui_file Dateianzeige (textbox) -> volles Bild
# =====================================================================
# _ui_list WIDGET TITEL TEXT FELDER_JE_EINTRAG EINTRAEGE...
_ui_list() {
ui_init
local widget="$1" title="$2" text="$3" per="$4"; shift 4
ui_calc "$text" "$per" "$@"
"$UI_BIN" ${UI_EXTRA[@]+"${UI_EXTRA[@]}"} --backtitle "$UI_BACKTITLE" --title "$title" \
"$widget" "$text" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3
}
# _ui_box WIDGET TITEL TEXT ZUSATZZEILEN [weitere Argumente]
# Widgets ohne Rueckgabe (msgbox, yesno, infobox) liefern nur den Status.
_ui_box() {
ui_init
local widget="$1" title="$2" text="$3" extra="$4"; shift 4
ui_calc_text "$text" "$extra"
case "$widget" in
--inputbox|--passwordbox)
"$UI_BIN" ${UI_EXTRA[@]+"${UI_EXTRA[@]}"} --backtitle "$UI_BACKTITLE" --title "$title" \
"$widget" "$text" "$UI_H" "$UI_W" "$@" 3>&1 1>&2 2>&3 ;;
*)
"$UI_BIN" ${UI_EXTRA[@]+"${UI_EXTRA[@]}"} --backtitle "$UI_BACKTITLE" --title "$title" \
"$widget" "$text" "$UI_H" "$UI_W" "$@" ;;
esac
}
# _ui_file TITEL DATEI - nutzt das Terminal so weit wie moeglich aus
_ui_file() {
ui_init
ui_term_size
"$UI_BIN" ${UI_EXTRA[@]+"${UI_EXTRA[@]}"} --backtitle "$UI_BACKTITLE" --title "$1" \
--textbox "$2" $(( UI_ROWS - 2 )) $(( UI_COLS - 4 ))
}
# ---------------------------------------------------------------------
# Die eigentlichen Dialoge - alle nur noch duenne Huellen
# ---------------------------------------------------------------------
ui_menu() { local t="$1" x="$2"; shift 2; _ui_list --menu "$t" "$x" 2 "$@"; }
ui_checklist() { local t="$1" x="$2"; shift 2; _ui_list --checklist "$t" "$x" 3 "$@"; }
ui_radiolist() { local t="$1" x="$2"; shift 2; _ui_list --radiolist "$t" "$x" 3 "$@"; }
ui_input() { _ui_box --inputbox "$1" "$2" 10 "${3:-}"; }
ui_password() { _ui_box --passwordbox "$1" "$2" 10; }
ui_msg() { _ui_box --msgbox "$1" "$2" 7; }
ui_yesno() { _ui_box --yesno "$1" "$2" 8; }
ui_info() { _ui_box --infobox "$1" "$2" 5; }
ui_text() { _ui_file "$1" "$2"; }
# Kommando ausfuehren, Ausgabe live in Datei, danach in Textbox anzeigen
ui_run() { # title cmd...
ui_init
local title="$1"; shift
local logf; logf="$(mktemp)"
ui_info "$title" "Bitte warten ...
$*"
if "$@" >"$logf" 2>&1; then
ui_text "$title - erfolgreich" "$logf"
rm -f "$logf"; return 0
else
ui_text "$title - FEHLGESCHLAGEN" "$logf"
rm -f "$logf"; return 1
fi
}
# ---------------------------------------------------------------------
# Dateiauswahl im Midnight-Commander-Stil (funktioniert auch mit whiptail)
# ---------------------------------------------------------------------
ui_pick_file() { # startdir [glob] -> Dateipfad auf stdout
ui_init
local dir="${1:-$PWD}" glob="${2:-*}" sel
dir="$(cd "$dir" 2>/dev/null && pwd || echo "$PWD")"
while :; do
local -a items=()
items+=(".." "<Verzeichnis hoeher>")
local d f
while IFS= read -r d; do
[ -n "$d" ] && items+=("$d/" "<DIR>")
done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null | sort)
while IFS= read -r f; do
[ -n "$f" ] || continue
items+=("$f" "$(human_size "$(file_size "$dir/$f")")")
done < <(find "$dir" -mindepth 1 -maxdepth 1 -type f -name "$glob" -printf '%f\n' 2>/dev/null | sort)
sel="$(ui_menu "Datei waehlen" "Verzeichnis: $dir\nFilter: $glob" "${items[@]}")" || return 1
case "$sel" in
"..") dir="$(dirname "$dir")" ;;
*/) dir="$dir/${sel%/}" ;;
*) printf '%s\n' "$dir/$sel"; return 0 ;;
esac
done
}