diff --git a/configs/airootfs/usr/local/bin/omarchy-install-dashboard b/configs/airootfs/usr/local/bin/omarchy-install-dashboard index 91e960b..e20e61a 100755 --- a/configs/airootfs/usr/local/bin/omarchy-install-dashboard +++ b/configs/airootfs/usr/local/bin/omarchy-install-dashboard @@ -38,9 +38,7 @@ CLEAR="${CSI}2J${CSI}H" CLEAR_LINE="${CSI}2K" CLEAR_TO_END="${CSI}J" GREEN="${CSI}32m" -WHITE="${CSI}37m" RED="${CSI}31m" -DARK="${CSI}90m" # The dashboard owns the final reboot prompt now, so keep the gum confirm # styling that used to come from install/helpers/presentation.sh. @@ -53,6 +51,22 @@ export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0 child_pid="" child_pgid="" +whimsy=( + "delivering delightful defaults" + "saving setup struggles" + "picking polished packages" + "curating cohesive colors" + "preparing pleasant pixels" + "hyping happy hyprland" + "integrating intuitive interfaces" + "bootstrapping beautiful boxes" + "crafting cozy commands" + "making machines marvelous" + "polishing pretty pixels" + "tuning tasteful terminals" + "weaving wonderful workflows" +) + tips=( "Super + Space opens the Omarchy menu for apps, settings, and more" "Super + K shows all the key bindings" @@ -115,10 +129,58 @@ trap 'interrupt 143' TERM term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; } term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; } -repeat() { - local char="$1" count="$2" out="" i - for ((i=0; i= deadline: + break + finally: + termios.tcsetattr(fd, termios.TCSANOW, settings) + print(8, 16) # Conventional console font when dimensions aren't available. +PY } logo_width() { @@ -151,22 +213,16 @@ LOGO_HEIGHT="$(logo_height)" (( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81 (( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1 CONTENT_WIDTH="$LOGO_WIDTH" -# Rows the progress screen occupies: logo, blank, title, blank, bar, blank, tip. -PROGRESS_HEIGHT=$((LOGO_HEIGHT + 6)) TOP_ROW=1 -DYNAMIC_ROW=$((LOGO_HEIGHT + 3)) LAST_SIZE="" LAST_PM=10 -# The finish screen mirrors the greeter's frame — logo, blank, one line, gap, -# one button — so the last thing the installer shows matches the first. It runs -# one row taller: `gum confirm` always paints prompt/blank/button and clears all -# three, so the button cannot sit closer than two rows under a title we drew. +# Wordmark, blank, title, and the three rows gum confirm uses. FINISH_HEIGHT=$((LOGO_HEIGHT + 5)) FINISH_TOP_ROW=2 # ── Install progress ───────────────────────────────────────────────────────── # -# Position = max(floor, work), both non-decreasing, in per-mille of the bar. +# Position = max(floor, work), both non-decreasing, in per-mille of the drawing. # # floor lo + (span-1) * t / (t+tau), on time since this process saw the # phase. Asymptotic, so it approaches the band end without reaching it. @@ -189,8 +245,8 @@ fi # A wrong denominator is worse than none: it never triggers the fallback. (( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0 -PKG_BONUS_MAX_PM=100 # cap on how far the bar may run ahead of the count -PKG_TAIL_PM=40 # band held back so a stalled bar has somewhere to go +PKG_BONUS_MAX_PM=100 # cap on how far progress may run ahead of the count +PKG_TAIL_PM=40 # band held back so stalled progress has somewhere to go PKG_FLOOR_TAU=900 # with a work signal, time is only a backstop until the # first package, so slow it until it cannot outrun data @@ -263,13 +319,161 @@ render_logo() { fi } -progress_bar() { - local pm="$1" width="${2:-34}" filled empty - (( pm < 0 )) && pm=0 - (( pm > 1000 )) && pm=1000 - filled=$(( pm * width / 1000 )) - empty=$(( width - filled )) - printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET" +# The icon uses half-block glyphs, scaled to the terminal's actual cell aspect +# ratio so its square silhouette stays square. No graphics protocol is needed. +# Coordinates follow the 15 x 15 grid of Omarchy's icon.svg. Each segment +# paints the full stroke width, starting at the bottom middle of the outer ring. +ICON_WIDTH=30 +ICON_HEIGHT=15 +CELL_WIDTH=8 +CELL_HEIGHT=16 +ICON_LEFT=1 +ICON_STEPS=0 +ICON_DRAWN=0 +DISPLAY_PM=0 +ICON_FRAME="" +declare -a ICON_ORDER=() ICON_TRACE=() ICON_FILL=() + +icon_segment() { + local scale="$1" x1="$2" y1="$3" x2="$4" y2="$5" + local x y from to + if (( y1 == y2 )); then + from=$(( (x1 * ICON_WIDTH + 14) / 15 )) + to=$(( ((x2 + 1) * ICON_WIDTH + 14) / 15 )) + if (( x2 < x1 )); then + from=$(( ((x1 + 1) * ICON_WIDTH + 14) / 15 - 1 )) + to=$(( (x2 * ICON_WIDTH + 14) / 15 - 1 )) + fi + for ((x=from; x!=to; x+=(x2 >= x1 ? 1 : -1))); do + for ((y=y1 * scale; y<(y1 + 1) * scale; y++)); do + icon_pixel "$((y * ICON_WIDTH + x))" + done + done + else + from=$(( y1 * scale )) + to=$(( (y2 + 1) * scale )) + if (( y2 < y1 )); then + from=$(( (y1 + 1) * scale - 1 )) + to=$(( y2 * scale - 1 )) + fi + for ((y=from; y!=to; y+=(y2 >= y1 ? 1 : -1))); do + for ((x=(x1 * ICON_WIDTH + 14) / 15; x<((x1 + 1) * ICON_WIDTH + 14) / 15; x++)); do + icon_pixel "$((y * ICON_WIDTH + x))" + done + done + fi +} + +icon_pixel() { + local pixel="$1" + if [[ ! -v ICON_TRACE[pixel] ]]; then + ICON_ORDER+=("$pixel") + ICON_TRACE[pixel]=${#ICON_ORDER[@]} + fi +} + +build_icon() { + local scale="$1" x y row + local -a mask=( + 111111111111111 + 100000010000001 + 101111110001101 + 101000000000101 + 101000000000101 + 101000000000101 + 101000000000101 + 111000000000101 + 101000000000101 + 101000000000101 + 101000000000101 + 101000000000101 + 101111111111101 + 100000010000001 + 111111110111111 + ) + ICON_WIDTH=$(( (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) )) + ICON_HEIGHT=$(((15 * scale + 1) / 2)) + ICON_ORDER=() ICON_TRACE=() ICON_FILL=() + ICON_DRAWN=0 + # Start at the bottom middle, follow the outer left edge to the top cross, + # then descend into the inner shape. Fill its two bridges as we reach them. + icon_segment "$scale" 7 14 0 14 + icon_segment "$scale" 0 14 0 0 + icon_segment "$scale" 0 0 7 0 + icon_segment "$scale" 7 0 7 2 + icon_segment "$scale" 7 2 2 2 + icon_segment "$scale" 2 2 2 7 + icon_segment "$scale" 2 7 0 7 + icon_segment "$scale" 2 7 2 12 + icon_segment "$scale" 2 12 7 12 + icon_segment "$scale" 7 12 7 14 + icon_segment "$scale" 7 12 12 12 + icon_segment "$scale" 12 12 12 2 + icon_segment "$scale" 12 2 11 2 + # Finish from the top cross: right along the outside, down, then left. + icon_segment "$scale" 7 0 14 0 + icon_segment "$scale" 14 0 14 14 + icon_segment "$scale" 14 14 9 14 + ICON_STEPS=${#ICON_ORDER[@]} + for ((y=0; y<15 * scale; y++)); do + row=${mask[y / scale]} + for ((x=0; x 0 )) && upper=90 + (( step > 0 && step <= drawn )) && upper=32 + step=${ICON_TRACE[pixel + ICON_WIDTH]:-0} + (( step > 0 )) && lower=90 + (( step > 0 && step <= drawn )) && lower=32 + fi + if (( upper == 0 && lower == 0 )); then + ICON_FRAME+="${RESET} " + elif (( upper == lower )); then + ICON_FRAME+="${CSI}${upper};49m█" + elif (( lower == 0 )); then + ICON_FRAME+="${CSI}${upper};49m▀" + elif (( upper == 0 )); then + ICON_FRAME+="${CSI}${lower};49m▄" + else + ICON_FRAME+="${CSI}${upper};$((lower + 10))m▀" + fi +} + +render_icon() { + local pm="$1" full="${2:-0}" drawn i pixel row col + drawn=$((pm * ICON_STEPS / 1000)) + ICON_FRAME="" + if (( full || pm == 1000 )); then + for ((row=0; row/dev/null || printf '8 16\n') + (( rows >= 34 && cols >= 49 )) && scale=2 + (( rows >= 50 && cols >= 64 )) && scale=3 + while (( scale > 1 && (15 * scale * CELL_HEIGHT + CELL_WIDTH) / (2 * CELL_WIDTH) > cols - 4 )); do + scale=$((scale - 1)) + done + build_icon "$scale" + TOP_ROW="$(centered_top_row "$((ICON_HEIGHT + 6))")" + ICON_LEFT=$(( (cols - ICON_WIDTH) / 2 + 1 )) + (( ICON_LEFT < 1 )) && ICON_LEFT=1 + PROGRESS_TEXT_WIDTH=$((cols - 2)) + (( PROGRESS_TEXT_WIDTH > CONTENT_WIDTH )) && PROGRESS_TEXT_WIDTH=$CONTENT_WIDTH + return 0 } render_static() { measure_progress_layout - printf '%s%s%d;1H' "$CLEAR" "$CSI" "$TOP_ROW" - render_logo - blank_line + printf '%s' "$CLEAR" + render_icon "$DISPLAY_PM" 1 } -render_dynamic() { - local pm - # The console can still resize under us (the framebuffer settles late in VMs). - # Horizontal padding is recomputed every frame anyway, but the logo is drawn - # once, so redraw the static half at the new center rather than leave the - # centered layout stranded against a stale geometry. - [[ "$(term_size)" == "$LAST_SIZE" ]] || render_static +update_progress() { set_now install_progress - pm=$PROGRESS_PM + local pm=$PROGRESS_PM [[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM - (( pm > 1000 )) && pm=1000 + # Only the child's successful exit can complete the drawing. + (( pm > 999 )) && pm=999 # POS is monotone by construction; this is a guard, not the mechanism. (( pm < LAST_PM )) && pm=$LAST_PM LAST_PM=$pm +} - printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW" - center "Installing Omarchy" "$CONTENT_WIDTH" +render_dynamic() { + local step message tip + # The framebuffer can settle late in VMs. Rebuild at the new size, preserving + # progress, instead of leaving the drawing at the old screen coordinates. + [[ "$(term_size)" == "$LAST_SIZE" ]] || render_static + # Ease phase jumps over several frames, with the percentage following the + # same position as the trace. The measured progress still never goes back. + step=$(( (LAST_PM - DISPLAY_PM + 3) / 4 )) + DISPLAY_PM=$((DISPLAY_PM + step)) + render_icon "$DISPLAY_PM" + + message="$(truncate_for_width "$(current_whimsy)" "$PROGRESS_TEXT_WIDTH")" + tip="$(truncate_for_width "Tip: $(current_tip)" "$PROGRESS_TEXT_WIDTH")" + printf '%s%d;1H' "$CSI" "$((TOP_ROW + ICON_HEIGHT + 1))" + center "${GREEN}$((DISPLAY_PM / 10))%${RESET}" "$PROGRESS_TEXT_WIDTH" + center "${GREEN}${message}${RESET}" "$PROGRESS_TEXT_WIDTH" blank_line - line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) "" - progress_bar "$pm" 34 - printf '\n' - blank_line - center "${DIM}Tip:${RESET} ${GREEN}$(current_tip)${RESET}" "$CONTENT_WIDTH" + center "${DIM}${tip}${RESET}" "$PROGRESS_TEXT_WIDTH" printf '%s' "$CLEAR_TO_END" } @@ -493,13 +718,11 @@ install_duration() { } render_finish() { - local duration effect_canvas_width + local duration effect_canvas_width title="Installed Omarchy" duration="$(install_duration || true)" - duration="${duration:-Complete}" - # ttfx centers its text two columns right of plain centering, so hand it a - # canvas two columns narrower and the animated logo lands exactly on the - # static one left_padding already drew. Otherwise the logo jumps a column - # sideways when the effect starts and again when it ends. + [[ -n $duration ]] && title+=" in ${duration}" + # ttfx centers its text two columns right of plain centering. A canvas two + # columns narrower aligns the animation with the title and fallback logo. effect_canvas_width="$(term_cols)" (( effect_canvas_width > 2 )) && effect_canvas_width=$((effect_canvas_width - 2)) @@ -508,23 +731,21 @@ render_finish() { FINISH_TOP_ROW="$(centered_top_row "$FINISH_HEIGHT")" { - printf '%s%s%s%d;1H' "$SHOW_CURSOR" "$CLEAR" "$CSI" "$FINISH_TOP_ROW" - render_logo - blank_line - center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH" + # Leave the wordmark area blank: laseretch reveals it for the first time. + printf '%s%s%s%d;1H' "$HIDE_CURSOR" "$CLEAR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 1))" + center "$title" "$CONTENT_WIDTH" blank_line } >"$TTY_PATH" if [[ -f $LOGO_PATH ]]; then # Reuse mode restores a saved cursor before moving up over its input. Save - # this anchor so the effect redraws the same logo rows shown above. + # this anchor so the effect occupies the reserved wordmark rows. printf '%s%d;1H\0337' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT))" >"$TTY_PATH" # Laseretch runs ~4s at 260fps, so the 8s cap is a stuck-process guard rather # than a trim. --xterm-colors keeps the whole effect in the 256-index palette # the way the greeter does, since the console reduces indexed codes well and - # truecolor ones badly, and it settles on indexed 2 — the green gum already - # drew the logo in above, so the send-off ends on the frame it started from - # instead of recoloring it. The laser and sparks keep their own hot palette. + # truecolor ones badly. It settles on indexed 2, the Tokyo Night green used + # throughout the installer. The laser and sparks keep their own hot palette. timeout 8s ttfx -i "$LOGO_PATH" \ --canvas-width "$effect_canvas_width" \ --anchor-text c \ @@ -533,9 +754,14 @@ render_finish() { --xterm-colors \ laseretch \ --final-gradient-stops 2 \ - >"$TTY_PATH" 2>/dev/null || dashboard_log "logo effect skipped (ttfx exited $?)" - printf '%s%d;1H' "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH" + >"$TTY_PATH" 2>/dev/null || { + dashboard_log "logo effect skipped (ttfx exited $?)" + { printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH" + } + else + { printf '%s%d;1H' "$CSI" "$FINISH_TOP_ROW"; render_logo; } >"$TTY_PATH" fi + printf '%s%s%d;1H' "$SHOW_CURSOR" "$CSI" "$((FINISH_TOP_ROW + LOGO_HEIGHT + 5))" >"$TTY_PATH" } reboot_prompt() { @@ -805,9 +1031,13 @@ PHASE_T0=$NOW PKG_LAST_CHANGE=$NOW launch_child +frame=0 while jobs -pr | grep -qx "$child_pid"; do + # Keep package counting/state reads at 2 Hz; animate the trace at 10 Hz. + (( frame % 5 == 0 )) && update_progress render_dynamic >"$TTY_PATH" 2>/dev/null || true - sleep 0.5 + frame=$((frame + 1)) + sleep 0.1 done set +e @@ -820,8 +1050,14 @@ child_pgid="" dashboard_log "installer child exited with status $status" if (( status == 0 )); then - # finished_at lands microseconds before the child exits, so the 0.5s poll - # rarely renders a full bar before the finish screen replaces it. + # Finish the trace even when a short-lived child exits between state polls. + LAST_PM=1000 + PHASE_NAME="Installation complete" + while (( DISPLAY_PM < 999 )); do + render_dynamic >"$TTY_PATH" 2>/dev/null || true + sleep 0.05 + done + DISPLAY_PM=1000 render_dynamic >"$TTY_PATH" 2>/dev/null || true sleep 0.4 fi