Quiet dnf: spinner + package counts, full log kept for failures #7

Merged
yuri merged 4 commits from feat/quiet-dnf into feat/gum-tui 2026-10-27 19:49:14 +01:00
3 changed files with 104 additions and 21 deletions
+81 -6
View File
@@ -71,16 +71,91 @@ pause_if_debug() {
#------------------------------------------
# Package helpers (dnf5)
#------------------------------------------
# True when gum is installed AND we are interactive (stdin tty + /dev/tty
# writable). stdout must NOT be checked: it is a pipe inside $(...).
# Unattended runs never get the TUI (see ui_quiet_run).
have_gum() {
command -v gum >/dev/null 2>&1 || return 1
[[ "${ASSUME_YES:-false}" == "true" ]] && return 1
[ -t 0 ] || return 1
[ -w /dev/tty ] || return 1
return 0
}
# Quiet dnf transaction: spinner while it runs, one ok-line after, full output
# appended to $SETUP_DNF_LOG and printed (tail) only on failure. Unattended or
# no-gum runs execute plainly with normal output (logs matter there).
ui_quiet_run() {
local msg="$1"
shift
local log="${SETUP_DNF_LOG:-/tmp/fedora-setup-dnf.log}"
local before n
# Live-quiet mode needs only an interactive terminal (stdin tty + writable
# /dev/tty), NOT gum — the progress line is pure bash. Unattended runs and
# pipes keep plain full output (logs/cron contract).
if [[ "${ASSUME_YES:-false}" == "true" ]] || [ ! -t 0 ] || [ ! -w /dev/tty ]; then
"$@"
return $?
fi
before=$(wc -l < "$log" 2>/dev/null || echo 0)
# Child writes to the log; we draw a LIVE line: spinner frame + the package
# dnf is currently working on. gum spin was tried here first but rendered
# as a frozen block on the real VM ("looks like it hangs") — this variant
# animates independently of gum and shows real progress, not just motion.
"$@" >>"$log" 2>&1 &
local pid=$!
# ASCII frames ONLY: braille/em-dash are multibyte UTF-8 and rendered as
# garbage letters when the session locale is not UTF-8 (VM-proven: the
# spinner appeared as changing random chars, not as a spinner).
local frames=( '|' '/' '-' '\\' )
local f=0 cur
while kill -0 "$pid" 2>/dev/null; do
cur=$(sed -n "$((before+1)),\$p" "$log" 2>/dev/null \
| grep -E '^(Installing|Upgrading|Downgrading|Removing|Verifying|Preparing|Cleanup):' \
| tail -n 1 || true)
if [[ -n "$cur" ]]; then
printf '\r %s %s: %s' "${frames[$f]}" "$msg" "$cur" >&2
else
printf '\r %s %s' "${frames[$f]}" "$msg" >&2
fi
f=$(( (f + 1) % ${#frames[@]} ))
sleep 0.15
done
local rc=0
wait "$pid" || rc=$?
# wipe the progress line before printing the result
printf '\r%*s\r' 100 '' >&2
if (( rc == 0 )); then
n=$(sed -n "$((before+1)),\$p" "$log" 2>/dev/null | grep -c '^Installing:' || true)
local plural="s"
(( n == 1 )) && plural=""
ok "$msg (${n} package${plural})"
return 0
else
color_red " Failed: $msg"
color_red " Last lines from $log:"
sed -n "$((before+1)),\$p" "$log" 2>/dev/null | tail -n 20 | sed 's/^/ /' >&2
return "$rc"
fi
}
install_packages() {
# -y : no prompts
# weak deps on : keep default behaviour, stated explicitly so it is greppable
sudo dnf install -y --setopt=install_weak_deps=True "$@"
ui_quiet_run "Installing: $*" \
sudo dnf install -y --setopt=install_weak_deps=True "$@"
}
# Same, but a missing/renamed package warns instead of aborting the whole run.
# Use for anything that is a nice-to-have rather than load-bearing.
install_optional() {
sudo dnf install -y --setopt=install_weak_deps=True "$@" \
ui_quiet_run "Installing (optional): $*" \
sudo dnf install -y --setopt=install_weak_deps=True "$@" \
|| warn "Optional packages not installed: $*"
}
@@ -114,8 +189,8 @@ remove_packages() {
done
if [ ${#packages_to_remove[@]} -gt 0 ]; then
info "Removing packages: ${packages_to_remove[*]}"
sudo dnf remove -y "${packages_to_remove[@]}"
ui_quiet_run "Removing: ${packages_to_remove[*]}" \
sudo dnf remove -y "${packages_to_remove[@]}"
fi
}
@@ -129,7 +204,7 @@ install_group() {
dnf group list --hidden 2>/dev/null | sed 's/^/ /' >&2 || true
return 1
fi
sudo dnf group install -y "$group"
ui_quiet_run "Installing group: @$group" sudo dnf group install -y "$group"
}
# Try each candidate group/environment in turn; first one that exists wins.
@@ -138,7 +213,7 @@ install_group_any() {
for g in "$@"; do
if dnf group info "$g" >/dev/null 2>&1; then
info "Using dnf group: @$g"
sudo dnf group install -y "$g"
ui_quiet_run "Installing group: @$g" sudo dnf group install -y "$g"
return 0
fi
done
+7 -11
View File
@@ -16,16 +16,7 @@ if [[ -n "${_SB_UI_LOADED:-}" ]]; then
fi
_SB_UI_LOADED=1
have_gum() {
command -v gum >/dev/null 2>&1 || return 1
# stdin must be interactive — it is inherited by $(...) subshells, so
# menus still get the real terminal. stdout must NOT be checked: it is
# a pipe by definition inside $(ui_choose ...). gum draws its UI on
# /dev/tty and prints only the answer to stdout (which we capture).
[ -t 0 ] || return 1
[ -w /dev/tty ] || return 1
return 0
}
# have_gum lives in lib/common.sh (install_packages needs it too).
# Single choice from a list.
# ui_choose "<header>" "<prompt>" "<default>" item1 item2 ...
@@ -62,7 +53,8 @@ ui_choose() {
local n=${#items[@]} answer
while true; do
if ! read -t 90 -rp "$prompt [1-$n] (default: shown above): " answer; then
printf "\n"; answer=""
printf "\n" >&2
answer=""
fi
# empty/timeout -> the default item (its position in the list)
if [[ -z "$answer" ]]; then
@@ -74,6 +66,10 @@ ui_choose() {
echo "${items[-1]}"
return 0
fi
if [[ "${answer,,}" == "x" ]]; then
echo "$default"
return 0
fi
if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= n )); then
echo "${items[$((answer-1))]}"
return 0
+16 -4
View File
@@ -13,6 +13,10 @@ source "$SCRIPT_DIR/lib/session.sh"
# shellcheck source=lib/ui.sh
source "$SCRIPT_DIR/lib/ui.sh"
# Full dnf output from quiet transactions lands here (shown on failure).
export SETUP_DNF_LOG="${SETUP_DNF_LOG:-/tmp/fedora-setup-dnf-$(id -u).log}"
: >> "$SETUP_DNF_LOG"
#------------------------------------------
# Defaults + argument parsing
# DEBUG_MODE must be initialised before anything reads it under set -u.
@@ -99,6 +103,14 @@ if [[ ${EUID} -eq 0 ]]; then
exit 1
fi
# Ask for the sudo password ONCE, up front, on a clean screen. Otherwise the
# first sudo call mid-run prints its prompt straight into the live progress
# line ("Installing (optional): gum[sudo] password for yuri:" — VM-proven).
# The timestamp refreshes on every subsequent sudo use, so one prompt covers
# the whole run.
info "Requesting sudo credentials (asked once for the whole run)..."
sudo -v
if [[ "$ASSUME_YES" == "true" && ( -z "$DE" || -z "$TWM" ) ]]; then
err "--yes requires explicit --de and --twm, so nothing is guessed on an"
err "unattended run. Example:"
@@ -185,8 +197,8 @@ fi
info "Ensuring Flathub is available (system-wide)..."
sudo flatpak remote-add --if-not-exists --system flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# Update the system (spinner when gum is present; output shown on failure)
ui_run "Updating the system" sudo dnf update -y
# Update the system (spinner + one-line summary; full output in $SETUP_DNF_LOG)
ui_quiet_run "Updating the system" sudo dnf update -y
pause_if_debug
#------------------------------------------
@@ -208,7 +220,7 @@ if [[ -z "$SELINUX_OPT" ]]; then
[[ "$choice" == "yes" ]] && SELINUX_DISABLED=true
printf "\n"
if findmnt -n -o FSTYPE / 2>/dev/null | grep -q btrfs; then
ok "Root filesystem is btrfs — snapshots + zstd compression will be set up."
ok "Root filesystem is btrfs: snapshots + zstd compression will be set up."
BTRFS_SNAPPER=true
else
info "Root filesystem is not btrfs; skipping snapshot setup."
@@ -247,7 +259,7 @@ pause_if_debug
if [[ -z "$DE" ]]; then
step "4 Desktop environment"
rule
color_gray " Bare Fedora install — picking a DE is what gives you a graphical login."
color_gray "Bare Fedora install: picking a DE is what gives you a graphical login."
choice=$(ui_choose "Desktop environment" "Select desktop environment:" \
"None (TWM only, SDDM via TWM module)" \
"GNOME (gdm)" "Plasma (sddm)" "XFCE (sddm)" "None (TWM only, SDDM via TWM module)")