* Animate the About logo with a passing glint fastfetch has no animation of its own, so the sweep is ours. Every frame is composed once up front and a tick writes one of them, repainting only the cells fastfetch drew the logo on, which holds 40fps for under 1% of a core and never reaches the module column six columns to its right. The logo is handed back exactly as it arrived: frames carry the colour fastfetch drew it in rather than a plain reset, so between glints the window is byte-for-byte the one it was before this change. Where the logo on screen might not be the text in the file, it stays still rather than guess — a fastfetch config in any directory searched ahead of Omarchy's own, a logo built from $1 colour placeholders or from a tab fastfetch expands itself, a window too small for the layout to fit without scrolling, and a shell whose locale is counting bytes instead of characters. A resize moves those cells, so a WINCH trap ends the sweep on the frame it happens rather than up to a poll later, which measured 10ms against 830ms. The polling stays as the backstop for a signal that arrived while it could not be taken. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Give the sheen a file of its own The launcher was carrying two things: what a glint looks like, and what it is drawn over. Only the second is about fastfetch. bin/omarchy-logo-sheen now knows how to lean a band of light across an ASCII logo and nothing about About, and it is handed where the logo sits, what colour to give its cells back, and how much room it has. When a frame may be written stays with the launcher, because that is inseparable from how the window closes and resizes. The tests split along the same seam, and the sheen's half no longer strips the launcher's tail to reach the code it tests. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Leave a logo still when one character is not one cell The frames slice the logo by character and the terminal draws it by column, so the sheen only puts a row back where it found it while those two agree. A double-width glyph, a combining mark and a joined emoji each break that, and a cut through one of them renders as something else — pushing the rest of the row right, into the module column, with nothing on screen to say the logo moved. A user can put any of the three in the logo by editing the text. Comparing the row's character count against its width in columns is the assumption itself, so it is what gets checked, rather than a list of the glyph classes that happen to break it today. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Size the About window from the layout fastfetch drew The fit predicted the content height from the logo and the module column, taking the taller of the two. Once the logo is the taller one fastfetch writes a row more than that arithmetic expects, so every logo of 27 rows or more got a window one row short and scrolled its top padding away. The shipped logo is 26 rows, which is why it never showed. Ask fastfetch how tall the layout came out instead of predicting it. That measurement was already being taken for the sheen, which refuses to animate a layout that scrolled — so a tall logo used to lose the glint as well as the padding row. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Leave the logo still when NO_COLOR asks for none fastfetch drops the logo's colour when it writes to a terminal that set NO_COLOR, but not when it writes to the substitution that measures it. The colour to hand those cells back in was therefore measured as green while the logo on screen had none, and the first glint would have left the whole logo green. It stops suppressing autowrap there too, so a narrow window soft-wraps and scrolls where the measurement cannot see it and the frames go on addressing rows that moved. Both follow from animating a screen drawn in a mode the measurement did not reproduce. A glint is colour besides, which is the thing NO_COLOR asks for none of. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Read fastfetch's config paths whole, and do not read silence as an answer Splitting each listed path on whitespace dropped everything after the first space, so a config under a home directory containing one was missed and the fit and the sheen went ahead against a layout neither had measured. The marker fastfetch prints beside the config it settled on is not part of the path either. A listing that failed was also indistinguishable from one that found nothing, because the status of a process substitution is discarded, so a fastfetch that could not enumerate read as "no custom config". Fall back to the directory fastfetch has always looked in first rather than take silence for an answer. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Notice a resize that lands while the grid is being read The sweep read the WINCH flag before the grid rather than after it. A signal arriving during the two command substitutions that read the grid is delivered only once they finish, so the flag was still false when it was tested, the grid still compared equal, and one more frame went out at coordinates the resize had already moved. Read the flag last, after the check it could have landed during. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Check that the render loop is what plays the sheen Every builder the tests drive can be exercised while nothing on screen ever animates. Replacing the render loop's animated branch with the still one it replaced left all of them passing, so nothing was holding the animation onto the screen it belongs on. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Name the animation after the branding it animates omarchy-logo-sheen sat in a group of its own, and a group whose only command is hidden still gets a header printed for it with nothing underneath. It belongs beside omarchy-branding-about, whose art it animates: the group already exists, the two halves of About branding are next to each other, and `omarchy branding about` still routes to the command rather than the helper now sharing its prefix. The tests take the names of what they cover, the way the rest of the suite does. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fewer moving parts in the sheen Four questions about a logo turned out to be one. Whether the shell is counting characters or bytes, whether a tab or an escape is in the line, whether a glyph is double-width or a combining mark or a joined emoji — each was asking whether one character is one cell, so that is the only thing asked now. It is also less strict in the one case that deserves it: plain ASCII art animates in a C locale, where the locale probe used to refuse everything. One band of light instead of a white core inside a green halo, which is three colour spans a row rather than five and no helper to clamp four cuts with. Only the left cut needs clamping at all: a slice starting past the end of a line is already empty, while a negative offset would count from the end of it. One loop instead of two, because an empty frame list plays nothing. A logo that cannot be animated now waits in the loop the animated one rests in rather than in a second copy of it, and the build that failed leaves no frames behind, since that loop plays whatever it finds. The logo's colour comes out of one match rather than a loop eating one escape at a time. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Say in the manual which art the glint leaves alone The line promised the glint to any art in the file, and the code does not: art whose characters are not one column wide is left still on purpose, as is the whole screen for anyone keeping a fastfetch config of their own. Somebody reading the old line and seeing a still logo would take deliberate behaviour for a broken feature. Name the condition rather than the list of glyph classes behind it, and say that both modes of Set From Image meet it, because that is the path almost everyone is on. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Draw the band in a colour bold cannot collapse A terminal can be told to render bold text in a brighter colour — foot's bold-text-in-bright, whose palette-based form brightens a bold regular colour into its bright counterpart. fastfetch draws the logo bold green, so under that setting the logo is already bright green, which is the colour the band was using: the glint came out the same green as the art and nothing appeared to happen at all. None of the four terminal configs set it, so it was waiting on whoever turned it on. Bright white instead, because no regular colour brightens into it, and the band shows whatever the terminal does with bold. Narrower with it, since white against green needs less width to read than a second green did. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Codex XHigh <noreply@openai.com>
326 lines
12 KiB
Bash
Executable File
326 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Launch the fastfetch TUI that gives information about the current system.
|
|
|
|
# The size that hugs the About content depends on the terminal font and the user's
|
|
# logo, so it can only be measured from inside the terminal. We remember the size
|
|
# that fit and apply it as a window rule before launching, so the window opens at
|
|
# it instead of resizing after the first paint. Bash defers WINCH traps while read
|
|
# blocks, so poll for size changes and re-render fastfetch whenever it is resized.
|
|
|
|
LOGO_FILE="$HOME/.config/omarchy/branding/about.txt"
|
|
FIT_FILE="$HOME/.local/state/omarchy/windows/about.fit"
|
|
OMARCHY_FASTFETCH_DIR=/etc/fastfetch
|
|
|
|
# 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 ]]
|
|
}
|
|
|
|
# wc -L counts display columns only in a UTF-8 locale. A session that never set
|
|
# one counts every box-drawing and Nerd Font glyph in the About layout as
|
|
# nothing, which measures the content narrower than it renders.
|
|
display_columns() {
|
|
LC_ALL=C.UTF-8 wc -L
|
|
}
|
|
|
|
logo_dimensions() {
|
|
[[ -f $LOGO_FILE ]] || return 1
|
|
printf '%s %s' "$(display_columns <"$LOGO_FILE")" "$(wc -l <"$LOGO_FILE")"
|
|
}
|
|
|
|
hypr_dispatch() {
|
|
local lua="$1"
|
|
shift
|
|
hyprctl dispatch "$lua" >/dev/null 2>&1 || hyprctl dispatch "$@" >/dev/null
|
|
}
|
|
|
|
remember_fit() {
|
|
local directory tmp
|
|
directory=$(dirname "$FIT_FILE")
|
|
mkdir -p "$directory"
|
|
tmp=$(mktemp "$directory/.about.fit.XXXXXX")
|
|
printf '%s %s %s %s\n' "$1" "$2" "$3" "$4" >"$tmp"
|
|
mv "$tmp" "$FIT_FILE"
|
|
}
|
|
|
|
# Replaces the rule from the last launch so a remembered size never outlives the
|
|
# fit it came from. Called without one it only clears, leaving the float rule's
|
|
# starting size — where a Hyprland without the Lua API stays too.
|
|
apply_size_rule() {
|
|
local rule=""
|
|
(( $# == 2 )) && rule="omarchy_about_size_rule = hl.window_rule({ match = { class = \"org.omarchy.about\" }, size = { $1, $2 } })"
|
|
|
|
hyprctl eval "if omarchy_about_size_rule then omarchy_about_size_rule:set_enabled(false) end; omarchy_about_size_rule = nil; $rule" >/dev/null 2>&1
|
|
}
|
|
|
|
# Sized before the terminal is spawned, so the window maps at its final size.
|
|
presize_window() {
|
|
local logo_w logo_h fit_logo_w fit_logo_h fit_w fit_h
|
|
|
|
if ! custom_fastfetch_config && [[ -r $FIT_FILE ]]; then
|
|
read -r logo_w logo_h <<<"$(logo_dimensions)"
|
|
read -r fit_logo_w fit_logo_h fit_w fit_h <"$FIT_FILE"
|
|
|
|
# A different logo needs a different window, so leave that launch to the
|
|
# float rule and let the fit measure the new size.
|
|
if [[ -n ${logo_w:-} && $logo_w == "$fit_logo_w" && $logo_h == "$fit_logo_h" ]] &&
|
|
[[ $fit_w =~ ^[0-9]+$ && $fit_h =~ ^[0-9]+$ ]]; then
|
|
apply_size_rule "$fit_w" "$fit_h"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
apply_size_rule
|
|
}
|
|
|
|
# Hyprland animates a resize and the terminal reflows to every step of it, so
|
|
# wait for the grid to hold still before measuring it.
|
|
settle_grid() {
|
|
local current previous="" held=0
|
|
|
|
for _ in {1..20}; do
|
|
current=$(stty size)
|
|
if [[ $current == $previous ]]; then
|
|
(( ++held == 3 )) && break
|
|
else
|
|
held=0
|
|
previous=$current
|
|
fi
|
|
sleep 0.05
|
|
done
|
|
}
|
|
|
|
fit_window() {
|
|
custom_fastfetch_config && return 0
|
|
|
|
local logo_w logo_h
|
|
read -r logo_w logo_h <<<"$(logo_dimensions)"
|
|
[[ -n ${logo_w:-} ]] || return 1
|
|
|
|
# The guard character keeps command substitution from eating the trailing
|
|
# break line, which provides the bottom padding row.
|
|
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)
|
|
|
|
# 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. 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
|
|
read -r rows cols <<<"$(stty size)"
|
|
read -r address width height <<<"$(hyprctl clients -j | jq -r '.[] | select(.class == "org.omarchy.about") | "\(.address) \(.size[0]) \(.size[1])"')"
|
|
[[ -n ${address:-} ]] || return 1
|
|
(( cols > 0 && rows > 0 && width > 0 && height > 0 )) || return 1
|
|
|
|
# A window has to land on the terminal's cell boundaries, so take a cell of
|
|
# slack over chasing an exact grid, and remember where it came to rest.
|
|
if (( cols >= target_c && cols <= target_c + 1 && rows >= target_r && rows <= target_r + 1 )); then
|
|
remember_fit "$logo_w" "$logo_h" "$width" "$height"
|
|
return 0
|
|
fi
|
|
|
|
# Two nudges is the budget, and each one is measured before the next is spent.
|
|
(( ++nudges <= 2 )) || return 1
|
|
|
|
# Move by the cells the window is off by, rather than scaling it to the grid,
|
|
# which would multiply up the terminal's padding along with them. Dividing a
|
|
# window that carries that padding still leaves the cell a touch generous, so
|
|
# round the move away from the grid that would clip.
|
|
shift_w=$(( (target_c - cols) * width ))
|
|
shift_h=$(( (target_r - rows) * height ))
|
|
target_w=$(( width + (shift_w >= 0 ? (shift_w + cols - 1) / cols : shift_w / cols) ))
|
|
target_h=$(( height + (shift_h >= 0 ? (shift_h + rows - 1) / rows : shift_h / rows) ))
|
|
|
|
hypr_dispatch "hl.dsp.window.resize({ window = \"address:$address\", x = $target_w, y = $target_h })" resizewindowpixel "exact $target_w $target_h,address:$address"
|
|
hypr_dispatch "hl.dsp.window.center({ window = \"address:$address\" })" centerwindow
|
|
settle_grid
|
|
done
|
|
|
|
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
|
|
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
|
|
# time. Beyond that, a window that will not settle would be fitted again on
|
|
# every repaint.
|
|
if [[ $fitted == false ]] && (( ++passes <= 2 )); then
|
|
fit_window && fitted=true
|
|
fi
|
|
# 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
|
|
passes=0
|
|
fi
|
|
done
|
|
fi
|
|
|
|
presize_window
|
|
exec omarchy-launch-or-focus-tui --app-id=org.omarchy.about omarchy-launch-about --render
|