Files
docker-podman-installation-…/scripts/lib/common.sh
T
duffyduckandClaude Opus 5 861a9bd71c tui-check: Ursache der Rahmendarstellung am Terminal ablesen
Nach der Locale-Korrektur meldet der Selbsttest UTF-8, die Anzeige blieb
aber unveraendert. Damit liegt es nicht mehr an der Serverseite, und
Weiterraten hat keinen Sinn.

"cmk-manage.sh tui-check" gibt jetzt zwei Testzeilen aus:
  Zeile A  echte UTF-8-Rahmenzeichen (U+2500 ff.)
  Zeile B  die alte VT100-Umschaltung ESC(0 mit den Buchstaben lqk
An deren Aussehen laesst sich die Ursache direkt ablesen - Terminal
ignoriert die Umschaltung (PuTTY-Fall), Terminal nicht im UTF-8-Modus,
oder Schrift ohne Rahmenzeichen. Zu jeder Beobachtung nennt die Ausgabe
die passende Abhilfe samt PuTTY-Einstellungen.

Dazu der ASCII-Schalter, der bisher ins Leere lief: einfache Rahmen kann
nur dialog zeichnen. Ist CMK_TUI_ASCII gesetzt und dialog vorhanden,
wird dialog jetzt dafuer vorgezogen; fehlt es, sagt das Skript das
ausdruecklich statt wirkungslos weiterzumachen.

dialog ist auf dieser Seite ohnehin der wirksamere Griff - es geht mit
dem Zeichensatz zuverlaessiger um als whiptail. Entsprechend in beiden
Sprachfassungen hervorgehoben.

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

488 lines
18 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() {
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.
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*) 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 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)"
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
}