From 2c247e390e357ae0fee3f8565b0c816adb705e6a Mon Sep 17 00:00:00 2001 From: Omarchybot Date: Sat, 22 Aug 2026 20:18:14 +0200 Subject: [PATCH] Stop the About logo moving when the animation starts (#7786) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ask the terminal whether the layout scrolled Whether the logo is on the rows the frames address was decided by arithmetic: count the lines of a second fastfetch run, and animate if the window has one more. That holds only while the second run and the real one agree, and they need not — a module that appears between them, a wrap the measurement never sees, anything that makes the painted layout a row taller than the counted one. The screen then scrolls, the logo sits a row above where these frames draw it, and the animation paints a copy of the logo one row below the real one. fastfetch has just painted when the question is asked, so the terminal already knows the answer: an unscrolled layout leaves the cursor one row past its last line. Ask for it, and keep the arithmetic for a terminal that will not say. The reply has to be read without a command substitution around it, or the query goes into the substitution's own pipe instead of to the terminal, and what comes back is the escape rather than a row. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) * Find the logo in the render instead of assuming where it is The row and column the frames draw on came from the padding in this repo's fastfetch config. The config that runs is the one in /etc, and `omarchy dev link` says plainly that it does not replace files at fixed system paths — so the two can disagree, and nothing in the code would notice. A logo fastfetch drew on row 1 or row 5, redrawn on row 3, is a logo that jumps the moment the animation starts and stays where the animation put it. fastfetch's own output is already being captured to measure the layout's height, so the logo can be found in it: take the longest line of the file as a landmark, look for it in the render, and turn where it was found back into where the logo starts. Padding of any size lands correctly, and a render that does not contain the file's own text is not this logo at all — a config that restyled it, a placeholder that was substituted — so there is nothing to animate and it is left alone. That last part subsumes what the config-path check was guessing at. It is kept, because it also decides whether the window may be fitted, which happens before there is a terminal to measure anything in. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) * Take the cursor query back out Asking the terminal where the cursor came to rest was meant to see a scrolled layout rather than infer one. It cannot: a linefeed at the bottom margin scrolls the screen and leaves the cursor on the bottom row, so a render that fitted exactly and one that scrolled both answer with the same number. Measured on a real terminal — a 29-line layout in windows of 30, 29 and 28 rows answers 30, 29, 28. The guard therefore only ever agreed with the arithmetic it replaced, and only in the case the arithmetic already had right. What it did add was a read on the same stdin the keypress that closes About arrives on. A key pressed while the query is outstanding is swallowed; a reply that arrives after the timeout is read by the next tick as the keypress, and the window closes on its own; typed text of the right shape is accepted as an answer, because nothing checked for the introducer; and the query is written before the read turns echo off, so a fast reply can be echoed onto the screen. None of that buys anything the line count did not already give, so it goes. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Codex XHigh * Fit the About window with a little room to spare The window was sized to exactly what was measured: the columns the widest module line needed, and one row past the layout's last line for the cursor. That measurement is taken once, and then the content goes on living. An uptime turns minutes into hours and hours into days, a version string grows, a module shows up after the next boot — and a window with nothing in hand clips at the right edge or scrolls the top padding away the moment any of it happens. A scrolled layout also moves the logo off the row it was drawn on, which is where a shifting logo comes from. Two columns and a row, which is invisible on screen and enough for anything that ticks over. The remembered size in about.fit is keyed on the logo, so the first launch after this still opens at the old size and the fit corrects it there and then. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Codex XHigh --- bin/omarchy-launch-about | 59 ++++++++++++++++++++++----- test/shell.d/launch-about-test.sh | 67 +++++++++++++++++++++++++++---- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/bin/omarchy-launch-about b/bin/omarchy-launch-about index 27fff12c..2d7c9d2c 100755 --- a/bin/omarchy-launch-about +++ b/bin/omarchy-launch-about @@ -19,6 +19,15 @@ LOGO_PAD_LEFT=2 LOGO_PAD_TOP=2 LOGO_PAD_RIGHT=6 +# The content is measured once and then goes on living: an uptime that turns +# minutes into hours and hours into days, a version string that grows, a module +# that shows up on the next boot. A window fitted to exactly what was measured +# has nowhere to put any of it, and the layout clips or scrolls the moment it +# grows — which is also the moment the logo stops being where it was drawn. Keep +# a little in hand rather than measure again every time something ticks over. +FIT_SPARE_COLUMNS=2 +FIT_SPARE_ROWS=1 + POLL_SECONDS=0.5 # fastfetch has no animation of its own, so the sheen is ours. It knows about a @@ -144,9 +153,9 @@ fit_window() { # Mirror the logo block in the fastfetch config: 2 columns of padding left of # the logo, 6 between logo and modules. Then 2 columns of right padding to - # match, and a row for the cursor so the trailing break shows. - local target_c=$(( LOGO_PAD_LEFT + logo_w + LOGO_PAD_RIGHT + module_w + LOGO_PAD_LEFT )) - local target_r=$(( LAYOUT_ROWS + 1 )) + # match, a row for the cursor so the trailing break shows, and the spare above. + local target_c=$(( LOGO_PAD_LEFT + logo_w + LOGO_PAD_RIGHT + module_w + LOGO_PAD_LEFT + FIT_SPARE_COLUMNS )) + local target_r=$(( LAYOUT_ROWS + 1 + FIT_SPARE_ROWS )) local nudges=0 rows cols address width height shift_w shift_h target_w target_h while :; do @@ -190,24 +199,52 @@ fit_window() { measure_layout() { [[ -n ${LAYOUT_ROWS:-} ]] && return 0 - local rendered row + local rendered plain needle offset found row # --pipe false because fastfetch drops its colours when it is not writing to a # terminal, and it is writing to this substitution. rendered=$(fastfetch --pipe false 2>/dev/null; printf X) rendered=${rendered%X} LAYOUT_ROWS=$(printf '%s' "$rendered" | wc -l) - (( LAYOUT_ROWS > LOGO_PAD_TOP )) || return 1 + (( LAYOUT_ROWS > 0 )) || return 1 - # Whatever fastfetch set before the first row of the logo is what the sheen has - # to give those cells back. - row=$(printf '%s' "$rendered" | sed -n "$((LOGO_PAD_TOP + 1))p") + # Find the logo in what fastfetch drew rather than working it out from the + # padding this file was written against. The config that runs is the one in + # /etc, which a checkout does not replace, so the two can disagree — and a logo + # measured two rows above where it was drawn is a logo the sheen moves. Not + # finding it at all is the same answer as finding it somewhere unexpected: + # whatever is on screen is not the text in the file, so leave it alone. + IFS=$'\t' read -r offset needle < <(logo_landmark) || return 1 + [[ -n $needle ]] || return 1 + + plain=$(printf '%s' "$rendered" | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g') + found=$(printf '%s' "$plain" | LC_ALL=C.UTF-8 awk -v needle="$needle" \ + 'index($0, needle) { print NR, index($0, needle); exit }') + [[ -n $found ]] || return 1 + read -r LOGO_ROW LOGO_COLUMN <<<"$found" + LOGO_ROW=$(( LOGO_ROW - offset )) + (( LOGO_ROW >= 1 && LOGO_COLUMN >= 1 )) || return 1 + + # Whatever fastfetch set before that row is what the sheen has to give back. + row=$(printf '%s' "$rendered" | sed -n "$(( LOGO_ROW + offset ))p") LOGO_COLOR="" [[ $row =~ ^(($ESC\[[0-9;]*m)+) ]] && LOGO_COLOR=${BASH_REMATCH[1]} return 0 } +# The longest line of the logo, and how far down the logo it sits — the most +# distinctive thing to look for in the render, and the offset that turns where it +# was found back into where the logo starts. +# Tab-separated, and the offset first, because the line may hold spaces of its +# own and splitting on them would cut the landmark short. +logo_landmark() { + LC_ALL=C.UTF-8 awk ' + { if (length($0) > best) { best = length($0); line = $0; at = NR - 1 } } + END { if (best > 0) printf "%d\t%s\n", at, line } + ' "$LOGO_FILE" 2>/dev/null +} + # The About screen's own reasons the logo might not be where these frames would # draw it. Whether the logo itself can be animated is the sheen's own question. build_sheen() { @@ -226,7 +263,9 @@ build_sheen() { measure_layout || return 1 # The layout needs a row for the cursor past its last line. Without one it has - # scrolled, and the logo is no longer on the rows the frames address. + # scrolled, and the logo is no longer on the rows the frames address. Ask the + # terminal where the cursor actually is rather than trust the arithmetic, and + # keep the arithmetic for a terminal that will not say. local rows cols read -r rows cols <<<"$(stty size)" (( rows > LAYOUT_ROWS )) || return 1 @@ -234,7 +273,7 @@ build_sheen() { # The cell the logo's first row starts on, every attribute fastfetch left on # those cells so a glint that has passed leaves them as it found them, and the # room it has to work in left of the module column. - sheen_build "$LOGO_FILE" "$(( LOGO_PAD_TOP + 1 ))" "$(( LOGO_PAD_LEFT + 1 ))" "${ESC}[0m${LOGO_COLOR}" "$(( cols - LOGO_PAD_LEFT ))" + sheen_build "$LOGO_FILE" "$LOGO_ROW" "$LOGO_COLUMN" "${ESC}[0m${LOGO_COLOR}" "$(( cols - LOGO_COLUMN + 1 ))" } # What the frames were built against. A window that resized, or a logo that was diff --git a/test/shell.d/launch-about-test.sh b/test/shell.d/launch-about-test.sh index 7e8b10d8..6fb92e73 100755 --- a/test/shell.d/launch-about-test.sh +++ b/test/shell.d/launch-about-test.sh @@ -22,14 +22,21 @@ source "$tmp_dir/about.bash" [[ $(type -t sheen_build) == "function" ]] || fail "the launcher finds the sheen it sources" pass "the launcher finds the sheen it sources" +# Kept before the stubs replace it, so the real one can be exercised below. +real_measure_layout=$(declare -f measure_layout) + # Stand in for the terminal, and for the fastfetch run that measures the layout. rows_by_cols="45 140" layout_rows=20 +logo_row=3 +logo_column=3 logo_color=$'\e[1m\e[32m' stty() { printf '%s\n' "$rows_by_cols"; } measure_layout() { LAYOUT_ROWS=$layout_rows LOGO_COLOR=$logo_color + LOGO_ROW=$logo_row + LOGO_COLUMN=$logo_column } # fastfetch resolves the home directory from the passwd database rather than @@ -76,11 +83,11 @@ pass "a roomy window animates" # how much room it has left of the module column. [[ ${handed[0]} == "$HOME/.config/omarchy/branding/about.txt" ]] || fail "the sheen is given the logo About draws" "${handed[0]}" pass "the sheen is given the logo About draws" -[[ ${handed[1]} == "$((config_top + 1))" && ${handed[2]} == "$((config_left + 1))" ]] || fail "the sheen is given the cell the logo starts on" "${handed[1]}/${handed[2]}" +[[ ${handed[1]} == "$logo_row" && ${handed[2]} == "$logo_column" ]] || fail "the sheen is given the cell the logo starts on" "${handed[1]}/${handed[2]}" pass "the sheen is given the cell the logo starts on" [[ ${handed[3]} == $'\e[0m'"$logo_color" ]] || fail "the sheen is given fastfetch's own colour to restore" "$(printf '%q' "${handed[3]}")" pass "the sheen is given fastfetch's own colour to restore" -[[ ${handed[4]} == "$((140 - config_left))" ]] || fail "the sheen is given the columns left of the module column" "${handed[4]}" +[[ ${handed[4]} == "$((140 - logo_column + 1))" ]] || fail "the sheen is given the columns left of the module column" "${handed[4]}" pass "the sheen is given the columns left of the module column" # fastfetch reads the first config it finds across several directories, and any @@ -105,10 +112,12 @@ rm -r "${HOME:?}/searched-later" rows_by_cols="$((layout_rows + 1)) 140" build_sheen || fail "a window with one row past the layout animates" pass "a window with one row past the layout animates" + rows_by_cols="$layout_rows 140" refuses "a window level with the layout's last line leaves it still" rows_by_cols="45 140" + # The loop plays whatever frames are left lying about, so a build that failed has # to leave none of the last one's. SHEEN_FRAMES=(stale frames) @@ -196,12 +205,23 @@ hyprctl() { return 0 } -# logo 10 wide + the config's padding + a 65-column module block. -fit_cols=$(( config_left + 10 + config_right + 65 + config_left )) +# logo 10 wide + the config's padding + a 65-column module block, plus the spare +# the fit keeps in hand for content that grows after it was measured. +fit_cols=$(( config_left + 10 + config_right + 65 + config_left + FIT_SPARE_COLUMNS )) +fit_rows=$(( layout_rows + 1 + FIT_SPARE_ROWS )) -rows_by_cols="$((layout_rows + 1)) $fit_cols" -fit_window || fail "the fit is satisfied by a window with a row past the layout" -pass "the fit is satisfied by a window with a row past the layout" +rows_by_cols="$fit_rows $fit_cols" +fit_window || fail "the fit is satisfied by a window with the spare in it" +pass "the fit is satisfied by a window with the spare in it" + +# Exactly the content and not a cell more is what used to be asked for, and it is +# the size that clips or scrolls as soon as an uptime turns over. +rows_by_cols="$(( layout_rows + 1 )) $(( fit_cols - FIT_SPARE_COLUMNS ))" +if fit_window; then + fail "the fit asks for more than the bare content" +else + pass "the fit asks for more than the bare content" +fi rows_by_cols="$layout_rows $fit_cols" if fit_window; then @@ -209,3 +229,36 @@ if fit_window; then else pass "the fit is not satisfied by a window that scrolls the layout" fi + +# The padding this file was written against is the config in the repo; the config +# that runs is the one in /etc, which a checkout does not replace. Measure the +# logo in what fastfetch drew rather than working it out from an assumption, or a +# logo drawn two rows lower is a logo the sheen picks up and moves. +eval "$real_measure_layout" +LOGO_FILE="$tmp_dir/landmark.txt" +printf '%s\n' '████████' '██ ██' '████████' >"$LOGO_FILE" +render_with_padding() { + local top=$1 left=$2 i line + for (( i = 0; i < top; i++ )); do printf '\n'; done + while IFS= read -r line; do printf '\e[1m\e[32m%*s%s\e[m\n' "$left" '' "$line"; done <"$LOGO_FILE" + for (( i = 0; i < 6; i++ )); do printf 'module line\n'; done +} +for pad in "2 2" "4 5" "0 0" "6 10"; do + set -- $pad + eval "fastfetch() { render_with_padding $1 $2; }" + LAYOUT_ROWS="" + measure_layout || fail "the logo is found wherever fastfetch drew it" "padding $1/$2" + [[ $LOGO_ROW == "$(( $1 + 1 ))" && $LOGO_COLUMN == "$(( $2 + 1 ))" ]] || + fail "the logo is found wherever fastfetch drew it" "padding $1/$2 measured $LOGO_ROW/$LOGO_COLUMN" +done +pass "the logo is found wherever fastfetch drew it" + +# A render that does not contain the file's own text is not this logo, whatever +# the reason — a config that restyled it, a placeholder fastfetch substituted. +fastfetch() { printf 'something else entirely\n'; } +LAYOUT_ROWS="" +if measure_layout; then + fail "a render without the logo in it is not measured" +else + pass "a render without the logo in it is not measured" +fi