Compare commits

..
7 Commits
Author SHA1 Message Date
yuri 85ba9ddb51 Merge pull request 'Breathing-block status indicator (blue/green/red)' (#10) from feat/breathing-block into main 2026-10-27 22:50:24 +01:00
Pathfinder 0630acf020 Console pulse: one-third speed (150ms frames), 256-ramp untouched
YuKu: 'slow the flicker a bit (half or one-third speed)'. The 16-colour
blue<->grey pulse now ticks at 150ms (600ms full cycle, calm); the
256-colour terminal breathe keeps its 50ms deep ramp. Per-depth tick
variable; same in the vendored helper.

Rate proven by pty trace: 8 frames in a 1.2s transaction = 150ms
exactly. (Frame-counting via grep -c undercounts — \r overwrites
collapse frames onto one line; count matches, not lines.)
2026-10-27 22:48:50 +01:00
Pathfinder 4a53155c7e TTY pulse: constant layout, blue <-> dark grey (no more block-off)
The on/off pulse shifted the whole line by one character during the
dark frames (off-frame printed one space fewer than the block frame).
Per YuKu: keep the block ALWAYS present and alternate its colour
instead — blue (44) <-> dark grey (100, bright black). Layout is
constant, pulse is colour-based, nothing moves but the block itself.

Also: disable_selinux now mkdir -p's /etc/selinux before writing a
fresh config (harness caught a die on a missing parent dir).
2026-10-27 22:45:43 +01:00
Pathfinder 71780291a3 TTY: on/off block pulse on the Linux console (shade fusion defeated)
YuKu tests on the bare Linux VT (not a terminal emulator): my 16-colour
fallback alternated blue (44) / bright blue (104), which render nearly
identically on VGA consoles -> fused into 'solid blue'. If TERM
misreports 256, 48;5;N codes are ignored outright.

Fixes:
- TERM linux*/console* forces the 16-colour path regardless of tput.
- 16-colour mode now PULSES THE BLOCK ON/OFF (2 frames blue block,
  2 frames no block, ~2.5Hz) instead of shade alternation — visible
  on any console hardware, no colour fidelity required.
- 256-colour terminals keep the deep 17->45 breathe ramp.

Verified: TERM=linux pty shows block/no-block alternation with zero
48;5 codes; TERM=xterm-256color keeps the 8-shade ramp; unattended +
piped regressions green.
2026-10-27 22:41:48 +01:00
Pathfinder 3d21a1c427 Breathing: deep amplitude so the pulse survives flicker fusion
At 20fps the previous narrow ramp (shades 17-33) flicker-fused into an
apparent SOLID block on the VM. Amplitude now spans dark navy (17) to
bright blue (45) in 12 steps — every frame is visibly different, cycle
is 0.6s. Speed unchanged.
2026-10-27 22:38:02 +01:00
Pathfinder 35906d4df0 Breathing pulse: 3x faster (0.15s -> 0.05s per frame)
YuKu: 'the breathing is super slow'. Frame interval down from 150ms
to 50ms (~7fps -> ~20fps pulse). Same change in the vendored
lib/ui-block.sh helper for consistency.
2026-10-27 22:35:35 +01:00
Pathfinder 9c0b716758 Status blocks replace the spinner: blue breathing / green / red
ui_quiet_run now paints a TTY-safe status BLOCK: a background-coloured
SPACE (never a glyph, no font/locale dependency — the braille lesson).

  busy : blue block breathing through a 256-colour ramp (48;5;17..33)
         with the live dnf action after it
  ok   : solid green block + package count
  fail : solid RED block (spec: YuKu changed the red X to a red block)
         followed by the log tail

Colour-depth aware: 256-colour ramp -> 16-colour blue/bright-blue ->
plain ASCII spinner when the terminal reports no colour. LANG=C pty
run shows zero non-ASCII bytes.

Also vendors lib/ui-block.sh (the standalone helper from the Telegram
session prototype; fail changed to red block) and installs the
ui_block_close EXIT trap — with a return 0, because an EXIT trap's
status becomes the script's exit code (harness caught every-successful-
run exiting 1 through that path).
2026-10-27 22:32:58 +01:00
3 changed files with 189 additions and 16 deletions
+67 -16
View File
@@ -101,43 +101,91 @@ ui_quiet_run() {
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.
# ---- status BLOCK (TTY-safe: a background-coloured SPACE, never a glyph) --
# busy: blue block that breathes (bg-colour ramp) + current dnf action
# ok : solid green block + package count
# fail: solid RED block (spec change from the X, per YuKu)
# Colour-depth aware: 256 -> ramp, 16 -> blue/bright-blue, none -> ASCII.
local depth=0 nc
nc=$(tput colors 2>/dev/null || echo 0)
case "$nc" in ''|*[!0-9]*) nc=0 ;; esac
if (( nc >= 256 )); then depth=256
elif (( nc >= 8 )); then depth=16
fi
# The bare Linux VT is 16-color VGA: 48;5;N codes are ignored there, and
# its blue/bright-blue backgrounds render nearly identically (VM-proven:
# "solid blue, no breathing" on the console). Cap to the 16-color path.
case "${TERM:-}" in linux*|console*) depth=16 ;; esac
# Per-depth frame interval: the console colour-pulse runs slower (a fast
# blue/grey flip reads as flicker — YuKu asked for half-to-third speed);
# the 256-colour breathe ramp keeps its 50ms.
local tick=0.05
(( depth == 16 )) && tick=0.15
local -a bg=()
case "$depth" in
# Deep-amplitude breathe: dark navy (17) to BRIGHT blue (45). Adjacent
# shades must differ enough to survive flicker fusion — a subtle ramp
# at 20fps reads as a solid block (VM-proven).
256) bg=( "48;5;17" "48;5;19" "48;5;21" "48;5;27" "48;5;33" "48;5;39" "48;5;45" "48;5;39" "48;5;33" "48;5;27" "48;5;21" "48;5;19" ) ;;
# Colour pulse with CONSTANT layout: blue <-> dark grey (bright black).
# Never remove the block on a frame — a missing block shifted the
# whole line by one char on the console (VM-proven).
16) bg=( "44" "44" "100" "100" ) ;;
esac
local -a ch=( "|" "/" "-" "\\" )
"$@" >>"$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
if (( depth >= 16 )); then
local code="${bg[$(( f % ${#bg[@]} ))]}"
if [[ -n "$cur" ]]; then
printf '\r\033[%sm \033[0m %s: %s' "$code" "$msg" "$cur" >&2
else
printf '\r\033[%sm \033[0m %s' "$code" "$msg" >&2
fi
else
printf '\r %s %s' "${frames[$f]}" "$msg" >&2
if [[ -n "$cur" ]]; then
printf '\r %s %s: %s' "${ch[$(( f % ${#ch[@]} ))]}" "$msg" "$cur" >&2
else
printf '\r %s %s' "${ch[$(( f % ${#ch[@]} ))]}" "$msg" >&2
fi
fi
f=$(( (f + 1) % ${#frames[@]} ))
sleep 0.15
f=$(( f + 1 ))
sleep "$tick"
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})"
if (( depth == 256 )); then
printf '\033[48;5;34m \033[0m %s (%s package%s)\n' "$msg" "$n" "$plural" >&2
elif (( depth == 16 )); then
printf '\033[42m \033[0m %s (%s package%s)\n' "$msg" "$n" "$plural" >&2
else
ok "$msg (${n} package${plural})"
fi
return 0
else
color_red " Failed: $msg"
if (( depth == 256 )); then
printf '\033[48;5;196m \033[0m %s\n' "$msg" >&2
elif (( depth == 16 )); then
printf '\033[41m \033[0m %s\n' "$msg" >&2
else
color_red " Failed: $msg"
fi
color_red " (dnf reported a transaction failure)"
color_red " Last lines from $log:"
sed -n "$((before+1)),\$p" "$log" 2>/dev/null | tail -n 20 | sed 's/^/ /' >&2
return "$rc"
@@ -307,6 +355,9 @@ disable_selinux() {
sudo cp "$cfg" "${cfg}.bkp.$(date +%F)"
sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' "$cfg"
else
# belt-and-suspenders: on real Fedora /etc/selinux always exists, but
# do not die if it somehow does not
sudo mkdir -p "$(dirname "$cfg")"
printf 'SELINUX=disabled\nSELINUXTYPE=targeted\n' | sudo tee "$cfg" >/dev/null
fi
ok "SELINUX=disabled written to $cfg (backup saved). Reboot to apply."
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# ui-block.sh - TTY-safe status "block" indicator (no special fonts)
#
# busy : blue block that "breathes" (background colour pulses)
# ok : solid green block
# fail : solid red block (changed from red X per YuKu's spec)
#
# Why this works on a bare TTY: the "block" is a BACKGROUND-COLOURED SPACE,
# not a U+2588 glyph. No fonts, no UTF-8, no locale dependency.
#
# Draws on /dev/tty, never stdout: ui_choose/ui_confirm capture stdout with $(),
# so a status line on stdout would corrupt every captured answer.
_UB_FD=""
_UB_PID=""
_UB_MSG=""
_ub_tty() {
if [[ -z "$_UB_FD" ]]; then
# probe first: a bare `exec {fd}>/dev/tty` failure still prints its error
{ : >/dev/tty; } 2>/dev/null || { _UB_FD=""; return 1; }
exec {_UB_FD}>/dev/tty 2>/dev/null || { _UB_FD=""; return 1; }
fi
[[ -n "$_UB_FD" ]]
}
_ub_depth() {
local n
n=$(tput colors 2>/dev/null || echo 0)
case "$n" in ''|*[!0-9]*) n=0 ;; esac
if [[ "$n" -ge 256 ]]; then echo 256
elif [[ "$n" -ge 8 ]]; then echo 16
else echo 0; fi
# bare Linux VT: cap regardless of what tput reports
case "${TERM:-}" in linux*|console*) echo 16 ;; esac
}
# paint one background-coloured block (a SPACE) + message
_ub_paint() {
printf '\r\033[%sm \033[0m %s' "$1" "$2" >&"$_UB_FD"
}
_ub_busy_loop() {
local depth pal=() i tick=0.05
depth=$(_ub_depth)
[[ "$depth" -eq 16 ]] && tick=0.15 # console pulse: slow, not flicker
case "$depth" in
256) pal=(17 19 21 27 33 39 45 39 33 27 21 19) ;; # deep breathe: dark navy to bright blue
16) pal=(44 44 100 100) ;; # blue <-> dark grey, constant layout
*) pal=("|" "/" "-" "\\") ;; # last-resort spinner
esac
while :; do
for i in "${pal[@]}"; do
if [[ "$depth" -eq 256 ]]; then
_ub_paint "48;5;$i" "$_UB_MSG" # 256-colour cube background
elif [[ "$depth" -eq 16 ]]; then
_ub_paint "$i" "$_UB_MSG" # blue <-> dark grey pulse
else
printf '\r[%s] %s' "$i" "$_UB_MSG" >&"$_UB_FD"
fi
sleep "$tick" || return 0
done
done
}
_ub_stop() {
if [[ -n "$_UB_PID" ]]; then
kill "$_UB_PID" 2>/dev/null
wait "$_UB_PID" 2>/dev/null
_UB_PID=""
fi
}
ui_block_start() {
_UB_MSG="${1:-Working}"
_ub_tty || return 0 # no tty: silent no-op
_ub_stop
_ub_busy_loop &
_UB_PID=$!
}
ui_block_ok() {
local msg="${1:-Done}" d
_ub_tty || return 0
_ub_stop
d=$(_ub_depth)
case "$d" in
256) _ub_paint "48;5;34" "$msg" ;;
16) _ub_paint "42" "$msg" ;;
*) printf '\r[OK] %s' "$msg" >&"$_UB_FD" ;;
esac
printf '\033[0m\n' >&"$_UB_FD"
}
ui_block_fail() {
local msg="${1:-Failed}" d
_ub_tty || return 0
_ub_stop
d=$(_ub_depth)
case "$d" in
256) printf '\r\033[48;5;196m \033[0m %s' "$msg" >&"$_UB_FD" ;;
16) printf '\r\033[41m \033[0m %s' "$msg" >&"$_UB_FD" ;;
*) printf '\r[!] %s' "$msg" >&"$_UB_FD" ;;
esac
printf '\n' >&"$_UB_FD"
}
ui_block_close() {
_ub_stop
if [[ -n "$_UB_FD" ]]; then
exec {_UB_FD}>&- 2>/dev/null
_UB_FD=""
fi
return 0 # an EXIT trap's status becomes the script's exit code — never fail here
}
# ALWAYS install a cleanup trap in the sourcing script:
# trap 'ui_block_close' EXIT INT TERM
+4
View File
@@ -12,6 +12,10 @@ source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/session.sh"
# shellcheck source=lib/ui.sh
source "$SCRIPT_DIR/lib/ui.sh"
# shellcheck source=lib/ui-block.sh
source "$SCRIPT_DIR/lib/ui-block.sh"
# never leave a block pulse loop running if the script is interrupted
trap 'ui_block_close' EXIT INT TERM
# 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}"