diff --git a/bin/omarchy-branding-about-animation b/bin/omarchy-branding-about-animation new file mode 100755 index 0000000000..460f04e00f --- /dev/null +++ b/bin/omarchy-branding-about-animation @@ -0,0 +1,104 @@ +#!/bin/bash + +# omarchy:summary=Shared helpers for animating the About branding (source this, don't run it). +# omarchy:group=branding +# omarchy:name=about-animation +# omarchy:hidden=true + +# The About branding's animation: a sheen, and the frames that make it. *When* +# those frames are written stays with the caller, because that is inseparable +# from how the caller's window closes and resizes; this only says what to write, +# and where — it is handed a logo and knows nothing about About beyond that. + +# A band of light leans across the logo, rests, and leans across again. Two +# columns per row puts it at 45 degrees on screen, where a cell is twice as tall +# as it is wide. +ESC=$'\e' +# Bright white, so the band does not depend on how the terminal reads bold. Foot's +# bold-text-in-bright brightens a bold regular colour into its bright counterpart, +# which turns a bold green logo bright green — exactly the colour a bright green +# band would have used, leaving a glint nobody can see. No regular colour +# brightens into bright white, so this one shows either way. +SHEEN_BAND="${ESC}[1m${ESC}[97m" +SHEEN_SLANT=2 +SHEEN_HALF=2 +SHEEN_FRAME_SECONDS=0.025 +SHEEN_REST_TICKS=8 +# Re-measure the grid every half second, the cadence a still logo already cost, +# rather than spawning a process per frame. +SHEEN_POLL_FRAMES=20 + +# A frame is one string holding every row of the logo, positioned and coloured, +# so a tick writes the whole logo at once and never shows it half drawn. +compose_frame() { + local centre=$1 + local row line length at from to frame="" + + for (( row = 0; row < SHEEN_ROWS; row++ )); do + line=${SHEEN_LINES[row]} + length=${#line} + + # Where the band crosses this row. Running off the right needs no clamp, + # because a slice that starts past the end of a line is already empty, but a + # negative offset would count from the end of it instead of off the left. + at=$(( centre - row * SHEEN_SLANT )) + from=$(( at - SHEEN_HALF )) + from=$(( from < 0 ? 0 : from )) + to=$(( at + SHEEN_HALF + 1 )) + to=$(( to < 0 ? 0 : to )) + + frame+="${ESC}[$((SHEEN_TOP + row));${SHEEN_LEFT}H" + frame+="${SHEEN_BASE}${line:0:from}${SHEEN_BAND}${line:from:to - from}${SHEEN_BASE}${line:to}" + done + + SHEEN_COMPOSED=$frame +} + +# Builds every frame up front: a tick that recomputed a logo's worth of colour +# spans in bash would not hold the frame rate, and the sweep is the same every +# time. Leaves them in SHEEN_FRAMES, and answers whether this logo can be +# animated at all — one it cannot put back exactly as it found it is one to leave +# alone, because nothing on screen would say the difference. +# +# sheen_build +sheen_build() { + local file=$1 columns=$5 + SHEEN_TOP=$2 + SHEEN_LEFT=$3 + SHEEN_BASE=$4 + + SHEEN_LINES=() + # A failing redirection reports itself before 2>/dev/null would apply, so order + # it first: the caller's window must not get a shell error painted across it. + mapfile -t SHEEN_LINES 2>/dev/null <"$file" || return 1 + SHEEN_ROWS=${#SHEEN_LINES[@]} + (( SHEEN_ROWS > 0 )) || return 1 + + local row line width=0 + for (( row = 0; row < SHEEN_ROWS; row++ )); do + line=${SHEEN_LINES[row]} + + # A renderer substitutes $1 to $9 for colours, so a logo written with those is + # not the text that reached the screen. + [[ $line == *'$'* ]] && return 1 + + # These frames slice the row by character and a terminal draws it by column, + # so one character has to be one cell — and everything that breaks that breaks + # it here. A wide glyph, a combining mark or a joined emoji is not one cell; a + # tab or an escape is one the renderer expanded itself; and a shell whose + # locale is counting bytes is not counting characters at all. + (( ${#line} == $(printf '%s' "$line" | LC_ALL=C.UTF-8 wc -L) )) || return 1 + + if (( ${#line} > width )); then + width=${#line} + fi + done + (( width > 0 && width <= columns )) || return 1 + + SHEEN_FRAMES=() + local centre last=$(( width + SHEEN_ROWS * SHEEN_SLANT + SHEEN_HALF )) + for (( centre = -SHEEN_HALF; centre <= last; centre++ )); do + compose_frame "$centre" + SHEEN_FRAMES+=("$SHEEN_COMPOSED") + done +} diff --git a/bin/omarchy-launch-about b/bin/omarchy-launch-about index 76ece301fc..27fff12cfb 100755 --- a/bin/omarchy-launch-about +++ b/bin/omarchy-launch-about @@ -10,10 +10,41 @@ LOGO_FILE="$HOME/.config/omarchy/branding/about.txt" FIT_FILE="$HOME/.local/state/omarchy/windows/about.fit" +OMARCHY_FASTFETCH_DIR=/etc/fastfetch -# A user-level fastfetch config can relocate or restyle the logo in ways this -# measurement cannot see, so leave sizing to the float rule in that case. +# The logo block in the fastfetch config. The fit below reproduces this layout to +# size the window, and the sheen has to repaint the very cells fastfetch drew the +# logo on, so both read the padding from here. +LOGO_PAD_LEFT=2 +LOGO_PAD_TOP=2 +LOGO_PAD_RIGHT=6 + +POLL_SECONDS=0.5 + +# fastfetch has no animation of its own, so the sheen is ours. It knows about a +# logo and nothing about About, which is why it is a file of its own. +source omarchy-branding-about-animation + +# A user's own fastfetch config can relocate or restyle the logo in ways this +# measurement cannot see, so leave sizing to the float rule in that case. It can +# sit in any of several directories fastfetch searches ahead of Omarchy's own, so +# ask fastfetch for that order rather than keep a copy here for its next release +# to outdate. custom_fastfetch_config() { + local directory listed=false + + # A whole line at a time, because a home directory may contain a space, and the + # marker fastfetch puts beside the config it settled on is not part of the path. + while IFS= read -r directory; do + listed=true + directory=${directory% (\*)} + [[ ${directory%/} == "$OMARCHY_FASTFETCH_DIR" ]] && return 1 + [[ -f ${directory%/}/config.jsonc ]] && return 0 + done < <(fastfetch --list-config-paths 2>/dev/null) + + # Silence is not the same answer as "none of them", so fall back to the + # directory fastfetch has always looked in first rather than read it as one. + [[ $listed == true ]] && return 1 [[ -f $HOME/.config/fastfetch/config.jsonc ]] } @@ -100,17 +131,22 @@ fit_window() { # The guard character keeps command substitution from eating the trailing # break line, which provides the bottom padding row. - local modules module_w module_h + local modules module_w modules=$(fastfetch --logo none | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g'; printf X) modules=${modules%X} module_w=$(printf '%s' "$modules" | display_columns) - module_h=$(printf '%s' "$modules" | wc -l) + + # Ask fastfetch how tall its layout came out rather than predicting it from the + # logo and the module column: once the logo is the taller of the two, fastfetch + # writes a row more than that arithmetic expects, and a window sized by it + # scrolls the top padding away. + measure_layout || return 1 # Mirror the logo block in the fastfetch config: 2 columns of padding left of - # the logo, 6 between logo and modules, 2 rows above it. Then 2 columns of - # right padding to match, and a row for the cursor so the trailing break shows. - local target_c=$(( 2 + logo_w + 6 + module_w + 2 )) - local target_r=$(( (logo_h + 2 > module_h ? logo_h + 2 : module_h) + 1 )) + # 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 )) local nudges=0 rows cols address width height shift_w shift_h target_w target_h while :; do @@ -146,17 +182,125 @@ fit_window() { return 1 } +# One run answers both questions the sheen has to ask first. How tall the layout +# is, because a window too small for it scrolls, which moves the logo off the rows +# the frames address. And what colour fastfetch drew the logo in, because the +# glint has to hand every cell back in the colour it arrived in — assume it, and +# a logo fastfetch colours differently comes out of the first glint a new one. +measure_layout() { + [[ -n ${LAYOUT_ROWS:-} ]] && return 0 + + local rendered 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 + + # 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") + LOGO_COLOR="" + [[ $row =~ ^(($ESC\[[0-9;]*m)+) ]] && LOGO_COLOR=${BASH_REMATCH[1]} + + return 0 +} + +# 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() { + # Whatever the last build left is not this window's, and the loop below plays + # whatever is here — so a build that fails has to leave nothing to play. + SHEEN_FRAMES=() + + custom_fastfetch_config && return 1 + + # fastfetch honours NO_COLOR when it writes to a terminal but not when it writes + # to the measurement below, so a logo drawn without colour would be measured as + # green and left green by the first glint. A glint is colour anyway, which is + # the thing NO_COLOR asks for none of. + [[ -n ${NO_COLOR:-} ]] && return 1 + + 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. + local rows cols + read -r rows cols <<<"$(stty size)" + (( rows > LAYOUT_ROWS )) || return 1 + + # 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 ))" +} + +# What the frames were built against. A window that resized, or a logo that was +# rebranded, needs fastfetch run again before anything is drawn over it. +content_changed() { + [[ $resized == true ]] && return 0 + + [[ $(stty size) != "$grid" || $(stat -c %Y "$LOGO_FILE" 2>/dev/null) != "$logo_stamp" ]] +} + +# A tick either times out, which is the delay, or a key arrives and About closes. +# Anything else on stdin is a terminal that went away, which closes it too. +tick() { + read -t "$1" -n 1 -s && exit + (( $? > 128 )) || exit +} + +play_sheen() { + local index + + for (( index = 0; index < ${#SHEEN_FRAMES[@]}; index++ )); do + # Stop before painting a frame rather than after: a resize has already moved + # the cells these address, and the rest of a sweep would land across them. + # The grid costs a process, so it stays on the poll interval. The trap costs + # nothing, so it is read last — a signal that arrived while the grid was being + # read would otherwise be seen only after another frame had gone out. + (( index % SHEEN_POLL_FRAMES == 0 )) && content_changed && return 1 + [[ $resized == true ]] && return 1 + + printf '%s' "${SHEEN_FRAMES[index]}" + tick "$SHEEN_FRAME_SECONDS" + done + + return 0 +} + +# The logo is still between glints, so About is a quiet window to leave open. +rest_sheen() { + local ticks + + for (( ticks = 0; ticks < SHEEN_REST_TICKS; ticks++ )); do + tick "$POLL_SECONDS" + content_changed && return 1 + done + + return 0 +} + if [[ ${1:-} == "--render" ]]; then printf '\e[?25l' + # A sweep runs for seconds between polls, so it reads this instead. The polling + # stays as the backstop, for a signal that arrived while it could not be taken. + resized=false + trap 'resized=true' WINCH + # Give the compositor a moment to apply the window rules before measuring cells. settle_grid fitted=false passes=0 while :; do - size=$(stty size) + grid=$(stty size) logo_stamp=$(stat -c %Y "$LOGO_FILE" 2>/dev/null) + resized=false + LAYOUT_ROWS="" clear fastfetch # A second pass picks up a fit that could not measure the window the first @@ -165,10 +309,10 @@ if [[ ${1:-} == "--render" ]]; then if [[ $fitted == false ]] && (( ++passes <= 2 )); then fit_window && fitted=true fi - while [[ $(stty size) == $size && $(stat -c %Y "$LOGO_FILE" 2>/dev/null) == $logo_stamp ]]; do - read -t 0.5 -n 1 -s && exit - (( $? > 128 )) || exit - done + # An empty frame list plays nothing, so a logo that cannot be animated waits + # here exactly as the still one did, and there is one loop rather than two. + build_sheen + while play_sheen && rest_sheen; do :; done # A rebranded logo changes the content dimensions, so measure again. if [[ $(stat -c %Y "$LOGO_FILE" 2>/dev/null) != $logo_stamp ]]; then fitted=false diff --git a/manual/41-branding.md b/manual/41-branding.md index 6121d3dae7..2cf19001b1 100644 --- a/manual/41-branding.md +++ b/manual/41-branding.md @@ -30,6 +30,8 @@ There are three entries in that menu: The same three options are under _Style > About_ for the _About_ screen you get from the Omarchy menu, and they work identically — the file is `~/.config/omarchy/branding/about.txt`, and the About window pops up after each change. The About art is converted to a smaller size than the screensaver's, since it has to fit in a window rather than fill your display. +While the window is open a glint of green leans across the art every few seconds and then leaves it still again. Your own art gets it too, as long as every character in it is one column wide — anything _Set From Image_ produces is. Art built from emoji or double-width characters stays still instead, and so does the screen if you keep a fastfetch config of your own: a still logo in those cases is the animation keeping out of the way rather than failing, since sliding a glint across them would land the rest of the line in the wrong place. + ![branding-about](images/branding-about.webp) ### Converting images yourself diff --git a/test/shell.d/branding-about-animation-test.sh b/test/shell.d/branding-about-animation-test.sh new file mode 100755 index 0000000000..23d6b4582e --- /dev/null +++ b/test/shell.d/branding-about-animation-test.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +animation="$ROOT/bin/omarchy-branding-about-animation" +source "$animation" + +logo="$tmp_dir/logo.txt" +write_logo() { printf '%s\n' "$@" >"$logo"; } +base=$'\e[0m\e[1m\e[32m' +top=3 left=3 columns=120 + +build() { sheen_build "$logo" "$top" "$left" "$base" "$columns"; } +refuses() { + if build; then + fail "$1" + else + pass "$1" + fi +} + +write_logo '████████████████████████' '████████ ████████' '████████████████████████' +build || fail "a logo of plain block art animates" +pass "a logo of plain block art animates" + +mapfile -t expected <"$logo" +(( ${#SHEEN_FRAMES[@]} > ${#expected[0]} )) || fail "the glint takes more frames than the logo is wide" "${#SHEEN_FRAMES[@]}" +pass "the glint takes more frames than the logo is wide" + +# Every frame is the same logo in different colours. A frame that changed a +# character, reached past the logo, or lit more than the band is wide would be +# drawing over whatever the caller put beside it, and nothing on screen would say so. +esc=$'\e' +band_width=$(( SHEEN_HALF * 2 + 1 )) +misplaced=0 rewritten=0 overrun=0 washed=0 unbased=0 widest_glint=0 first_lit=0 +lit_rows="" +for index in "${!SHEEN_FRAMES[@]}"; do + row=0 + frame_lit=0 + while IFS= read -r drawn; do + [[ -n $drawn ]] || continue + [[ $drawn == "$esc[$((top + row));${left}H"* ]] || misplaced=$((misplaced + 1)) + + rest=${drawn#*H} + [[ $rest == "$base"* ]] || unbased=$((unbased + 1)) + rest=${rest#"$base"} + head=${rest%%"$SHEEN_BAND"*} && rest=${rest#*"$SHEEN_BAND"} + lit=${rest%%"$base"*} && tail=${rest#*"$base"} + + [[ $head$lit$tail == "${expected[row]}" ]] || rewritten=$((rewritten + 1)) + (( ${#head} + ${#lit} + ${#tail} > ${#expected[row]} )) && overrun=$((overrun + 1)) + (( ${#lit} > band_width )) && washed=$((washed + 1)) + if (( ${#lit} > 0 )); then + lit_rows+="$row " + frame_lit=$((frame_lit + 1)) + fi + row=$((row + 1)) + done < <(printf '%s\n' "${SHEEN_FRAMES[index]}" | sed "s/$esc\[[0-9]*;[0-9]*H/\n&/g") + (( row == ${#expected[@]} )) || fail "a frame draws every row of the logo" "drew $row of ${#expected[@]}" + (( frame_lit > widest_glint )) && widest_glint=$frame_lit + (( index == 0 )) && first_lit=$frame_lit +done + +(( misplaced == 0 )) || fail "every row is drawn on the cell it was given" "$misplaced misplaced" +pass "every row is drawn on the cell it was given" + +(( unbased == 0 )) || fail "every row starts in the colour it was handed" "$unbased rows" +pass "every row starts in the colour it was handed" + +(( rewritten == 0 )) || fail "the sheen only recolours the logo, never rewrites it" "$rewritten rows changed" +pass "the sheen only recolours the logo, never rewrites it" + +(( overrun == 0 )) || fail "no frame reaches past the logo" "$overrun overruns" +pass "no frame reaches past the logo" + +# A band of light leaning across the logo, not a wash over half of it. +(( washed == 0 )) || fail "the glint stays a band the whole way across" "$washed rows lit wider than $band_width" +pass "the glint stays a band the whole way across" + +for row in "${!expected[@]}"; do + [[ " $lit_rows" == *" $row "* ]] || fail "the glint crosses every row of the logo" "row $row is never lit" +done +pass "the glint crosses every row of the logo" + +(( widest_glint == ${#expected[@]} )) || fail "the glint leans across the whole logo at once" "widest frame lit $widest_glint of ${#expected[@]}" +pass "the glint leans across the whole logo at once" + +(( first_lit == 1 )) || fail "a glint arrives from off the logo" "the first frame lights $first_lit rows" +pass "a glint arrives from off the logo" + +settled="" +for row in "${!expected[@]}"; do + settled+="$esc[$((top + row));${left}H$base${expected[row]}$SHEEN_BAND$base" +done +[[ ${SHEEN_FRAMES[-1]} == "$settled" ]] || fail "a glint settles back to the logo it was given" +pass "a glint settles back to the logo it was given" + +# A terminal that renders bold text in bright colours — foot's bold-text-in-bright +# does exactly this — maps a bold regular colour to its bright counterpart, so a +# band using one of those vanishes into a logo drawn in the matching regular +# colour, and nobody sees the animation at all. +[[ ! $SHEEN_BAND =~ \[9[0-6]m ]] || fail "the band avoids the colours a bold logo can brighten into" "$(printf '%q' "$SHEEN_BAND")" +pass "the band avoids the colours a bold logo can brighten into" + +# One character has to be one cell, or putting a row back moves what follows it. +# Everything that breaks that is refused by the one check, so everything that +# breaks it is tested against the one check. +write_logo '$1████' ' ████' +refuses "a logo built from colour placeholders is left still" +write_logo "$(printf 'A\tB')" 'CC' +refuses "a logo with a tab someone else expands is left still" +write_logo "$(printf 'A\033[31mB')" 'CCCCC' +refuses "a logo carrying an escape is left still" +write_logo 'AAA中文BBB' 'CCCCCCCCCC' +refuses "a logo with double-width glyphs is left still" +write_logo "$(printf 'AAAe\xcc\x81BBB')" 'CCCCCCCC' +refuses "a logo with a combining mark is left still" +write_logo "$(printf 'AA\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9BB')" 'CCCCCCC' +refuses "a logo with a joined emoji is left still" + +# The same check answers for a shell that is counting bytes rather than +# characters, which is the only thing that would make a block logo unsafe here. +write_logo '████████' '████████' +byte_counting=$(LC_ALL=C bash -c 'source "$1"; sheen_build "$2" 3 3 "" 120 && echo animated || echo still' _ "$animation" "$logo") +[[ $byte_counting == "still" ]] || fail "a shell counting bytes leaves a block logo still" "$byte_counting" +pass "a shell counting bytes leaves a block logo still" + +ascii_counting=$(LC_ALL=C bash -c 'printf "%s\n" AAAA BBBB > "$2"; source "$1"; sheen_build "$2" 3 3 "" 120 && echo animated || echo still' _ "$animation" "$tmp_dir/ascii.txt") +[[ $ascii_counting == "animated" ]] || fail "a shell counting bytes still animates plain ASCII" "$ascii_counting" +pass "a shell counting bytes still animates plain ASCII" + +write_logo '████████████████████████' '████████ ████████' +columns=10 +refuses "a logo wider than the columns it was given is left still" +columns=120 + +: >"$logo" +refuses "an empty logo is left still" + +rm -f "$logo" +missing=$(build 2>&1 >/dev/null || true) +[[ -z $missing ]] || fail "a missing logo says nothing on the terminal it would draw on" "$missing" +pass "a missing logo says nothing on the terminal it would draw on" diff --git a/test/shell.d/launch-about-test.sh b/test/shell.d/launch-about-test.sh new file mode 100755 index 0000000000..7e8b10d86b --- /dev/null +++ b/test/shell.d/launch-about-test.sh @@ -0,0 +1,211 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +# The launcher ends by taking over the process, so source it short of that line +# and its half of the question can be asked here, without a terminal to draw on. +about="$ROOT/bin/omarchy-launch-about" +grep -q '^presize_window$' "$about" || fail "About launcher can be sourced short of its launch" +sed '/^presize_window$/,$d' "$about" >"$tmp_dir/about.bash" + +export HOME="$tmp_dir/home" +export PATH="$ROOT/bin:$PATH" +mkdir -p "$HOME/.config/omarchy/branding" +printf '%s\n' '████████' '████████' >"$HOME/.config/omarchy/branding/about.txt" + +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" + +# Stand in for the terminal, and for the fastfetch run that measures the layout. +rows_by_cols="45 140" +layout_rows=20 +logo_color=$'\e[1m\e[32m' +stty() { printf '%s\n' "$rows_by_cols"; } +measure_layout() { + LAYOUT_ROWS=$layout_rows + LOGO_COLOR=$logo_color +} + +# fastfetch resolves the home directory from the passwd database rather than +# $HOME, so it would answer for the real one. Stand in for it with the search +# order it prints, pointed at this test's directories. +config_paths=( + "$HOME/.config/fastfetch/" + "$HOME/fastfetch/" + "$OMARCHY_FASTFETCH_DIR/ (*)" + "$HOME/searched-later/fastfetch/" +) +fastfetch() { + [[ ${1:-} == "--list-config-paths" ]] || return 1 + printf '%s\n' "${config_paths[@]}" +} + +# Record what the launcher hands the sheen rather than building any frames. +handed=() +sheen_build() { handed=("$@"); } + +refuses() { + if build_sheen; then + fail "$1" + else + pass "$1" + fi +} + +# The sheen repaints the cells fastfetch drew the logo on, so the launcher's idea +# of the padding has to be the config's. Read them from the config rather than +# from the launcher, or a drift between the two would agree with itself. +config_top=$(jq -r '.logo.padding.top' "$ROOT/etc/fastfetch/config.jsonc") +config_left=$(jq -r '.logo.padding.left' "$ROOT/etc/fastfetch/config.jsonc") +config_right=$(jq -r '.logo.padding.right' "$ROOT/etc/fastfetch/config.jsonc") + +[[ $LOGO_PAD_TOP == "$config_top" && $LOGO_PAD_LEFT == "$config_left" && $LOGO_PAD_RIGHT == "$config_right" ]] || + fail "the launcher's padding is the fastfetch config's" "config: $config_top/$config_left/$config_right, launcher: $LOGO_PAD_TOP/$LOGO_PAD_LEFT/$LOGO_PAD_RIGHT" +pass "the launcher's padding is the fastfetch config's" + +build_sheen || fail "a roomy window animates" +pass "a roomy window animates" + +# The sheen is told where the logo is, what colour to hand the cells back in, and +# 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]}" +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]}" +pass "the sheen is given the columns left of the module column" + +# fastfetch reads the first config it finds across several directories, and any +# of them ahead of Omarchy's own can put the logo somewhere else entirely. +for directory in .config/fastfetch fastfetch; do + mkdir -p "$HOME/$directory" + touch "$HOME/$directory/config.jsonc" + refuses "a fastfetch config in ~/$directory leaves the logo still" + rm -r "${HOME:?}/$directory" +done + +# One fastfetch would never read, because Omarchy's own comes first, is not a +# reason to stop: the logo on screen is still the one About drew. +mkdir -p "$HOME/searched-later/fastfetch" +touch "$HOME/searched-later/fastfetch/config.jsonc" +build_sheen || fail "a config fastfetch searches after Omarchy's own still animates" +pass "a config fastfetch searches after Omarchy's own still animates" +rm -r "${HOME:?}/searched-later" + +# A window with no room for the cursor past the layout's last line has scrolled, +# and the logo is no longer on the rows the frames address. +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) +NO_COLOR=1 +build_sheen || true +unset NO_COLOR +(( ${#SHEEN_FRAMES[@]} == 0 )) || fail "a build that failed leaves no frames to replay" "${#SHEEN_FRAMES[@]} left" +pass "a build that failed leaves no frames to replay" + +# fastfetch drops the logo's colour for a terminal that asked for none, but not +# for the measurement, so the colour to restore would be measured wrong — and a +# glint is colour besides. +NO_COLOR=1 +refuses "a session that asked for no colour leaves the logo still" +unset NO_COLOR + +# A home directory may contain a space, and the marker fastfetch puts beside the +# config it settled on is not part of the path. +spacey="$tmp_dir/example user/.config/fastfetch" +mkdir -p "$spacey" +touch "$spacey/config.jsonc" +config_paths=("$tmp_dir/example user/.config/fastfetch/" "$OMARCHY_FASTFETCH_DIR/ (*)") +custom_fastfetch_config || fail "a fastfetch config in a path with a space is found" +pass "a fastfetch config in a path with a space is found" +rm -r "$tmp_dir/example user" +config_paths=("$HOME/.config/fastfetch/" "$HOME/fastfetch/" "$OMARCHY_FASTFETCH_DIR/ (*)") + +# An enumeration that said nothing is not the same answer as "none of them". +mkdir -p "$HOME/.config/fastfetch" +touch "$HOME/.config/fastfetch/config.jsonc" +listing=$(declare -f fastfetch) +fastfetch() { return 7; } +custom_fastfetch_config || fail "an enumeration that failed does not read as no config" +pass "an enumeration that failed does not read as no config" +eval "$listing" +rm -r "${HOME:?}/.config/fastfetch" + +# The grid costs a process and is only read on the poll interval, so a resize can +# land while it is being read. The sweep has to see that before it paints again. +SHEEN_FRAMES=("first" "second" "third") +tick() { :; } +resized=false +content_changed() { resized=true; return 1; } +painted=$(play_sheen || true) +[[ -z $painted ]] || fail "a resize landing during the check stops the sweep before it paints" "$(printf '%q' "$painted")" +pass "a resize landing during the check stops the sweep before it paints" + +resized=false +content_changed() { return 1; } +painted=$(play_sheen || true) +[[ $painted == "firstsecondthird" ]] || fail "an undisturbed sweep writes every frame" "$(printf '%q' "$painted")" +pass "an undisturbed sweep writes every frame" + +# Every builder above can be exercised while nothing on screen ever animates, so +# check that the render loop is what calls them. +render_block=$(sed -n '/--render/,$p' "$about") +for called in build_sheen play_sheen rest_sheen; do + [[ $render_block == *"$called"* ]] || fail "the render loop plays the sheen" "it never calls $called" +done +pass "the render loop plays the sheen" + +measure_layout() { return 1; } +refuses "a layout fastfetch cannot be measured from leaves it still" + +# Once the logo is taller than the module column, fastfetch writes a row more +# than logo-plus-padding, so a window sized by that arithmetic scrolls its top +# padding away — and a scrolled layout is one the sheen then refuses. The fit +# asks fastfetch how tall the layout came out instead. +rm -rf "${HOME:?}/.local" +printf '%s\n' $(for i in $(seq 40); do echo '██████████'; done) >"$HOME/.config/omarchy/branding/about.txt" +layout_rows=43 +measure_layout() { + LAYOUT_ROWS=$layout_rows + LOGO_COLOR=$logo_color +} +fastfetch() { + case ${1:-} in + --list-config-paths) printf '%s\n' "${config_paths[@]}" ;; + --logo) for i in $(seq 29); do printf '%065d\n' 0; done ;; + *) return 1 ;; + esac +} +hyprctl() { + [[ $1 == "clients" ]] && printf '[{"class":"org.omarchy.about","address":"0x1","size":[800,600]}]\n' + return 0 +} + +# logo 10 wide + the config's padding + a 65-column module block. +fit_cols=$(( config_left + 10 + config_right + 65 + config_left )) + +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="$layout_rows $fit_cols" +if fit_window; then + fail "the fit is not satisfied by a window that scrolls the layout" +else + pass "the fit is not satisfied by a window that scrolls the layout" +fi