Files
nyaterm/scripts/check-icon-references.sh
T
Kang ebb81fcaa8 fix(ui): paint the svg icons that never declared a colour
`Svg::paint` does `self.path.zip(style.text.color)`, and `compute_style` starts
from `Style::default()`, whose `text.color` is `None`. A parent's
`.text_color(..)` cascades to real text but never to an `svg()` child, so a
glyph without a colour of its own was skipped entirely at paint time — no
error, no fallback, just an invisible icon still taking up layout.

59 call sites were affected: the window minimise/maximise/close controls, tab
close buttons, the title bar, the quick-command toolbar, the transfer browser
and queue, tunnels, settings and the AI panels. Shared button helpers are fixed
first, so most sites are covered by ~10 functions. Where the parent brightened
the icon on hover, the pair becomes `.group(..)` plus `.group_hover(..)`, since
a `.hover()` refinement cannot reach a child either.

`scripts/check-icon-references.sh` now fails on an `svg()` whose chain has a
`.path(..)` but no `.text_color(..)`. It checks depth 0 only: a colour that
lives solely inside `.group_hover(.., |s| s.text_color(..))` still leaves the
icon invisible until the pointer is over it.
2026-07-26 23:21:58 +08:00

114 lines
4.2 KiB
Bash

#!/usr/bin/env bash
#
# Verify that every icon path the UI references is actually bundled, and that it
# is painted with the matching element.
#
# GPUI fails silently here: a missing asset makes `svg()`/`img()` draw nothing,
# with no error and no log line. Painting a full-color logo through `svg()` is
# just as quiet — it renders as a flat silhouette.
#
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
ASSET_DIR="crates/nyaterm-app/assets"
SRC_DIRS=(crates/nyaterm-desktop/src crates/nyaterm-ui/src)
failures=0
fail() {
printf 'icon-reference: %s\n' "$*" >&2
failures=$((failures + 1))
}
# 1. Every referenced asset exists.
referenced="$(grep -rEoh '"(icons|color)/[A-Za-z0-9._/-]+\.(svg|png)"' "${SRC_DIRS[@]}" |
tr -d '"' | sort -u)"
if [[ -z "$referenced" ]]; then
fail "no icon references found; the search pattern is probably stale"
fi
while IFS= read -r asset; do
[[ -z "$asset" ]] && continue
[[ -f "$ASSET_DIR/$asset" ]] || fail "$asset is referenced but not bundled"
done <<< "$referenced"
# 2. The prefix and the element agree. `icons/**` is an alpha mask, `color/**` is
# a raster; swapping them is silent but wrong.
while IFS= read -r hit; do
[[ -z "$hit" ]] && continue
fail "svg() is given a full-color asset (use color_icon): $hit"
done <<< "$(grep -rn '\.path("color/' "${SRC_DIRS[@]}" || true)"
while IFS= read -r hit; do
[[ -z "$hit" ]] && continue
fail "img() is given a monochrome asset (use mono_icon): $hit"
done <<< "$(grep -rn 'img("icons/' "${SRC_DIRS[@]}" || true)"
# 3. Every svg() declares its own color. GPUI resolves an element's style from
# Style::default(), so `text.color` is None unless the element sets it: a
# parent's `.text_color(..)` reaches text but never an svg() child, and
# `Svg::paint` skips a glyph with no color entirely. The result is an
# invisible icon that still occupies layout — no error, no log line.
#
# Needs to follow the builder chain across newlines, which grep cannot do.
while IFS= read -r hit; do
[[ -z "$hit" ]] && continue
fail "svg() has no text_color, so it is never painted: $hit"
done <<< "$(python3 - "${SRC_DIRS[@]}" <<'PY' || true
import pathlib, re, sys
# The color has to be on the svg's own chain, not nested inside a closure: an
# icon whose only color lives in `.group_hover(.., |s| s.text_color(..))` is
# invisible until the pointer is over it. So both markers are counted at depth 0.
def chain_flags(source, start):
index, depth = start, 0
has_path = has_color = False
while index < len(source):
char = source[index]
if char == "(":
if depth == 0:
if source.startswith(".path(", index - 5):
has_path = True
elif source.startswith(".text_color(", index - 11):
has_color = True
depth += 1
elif char == ")":
if depth == 0:
break
depth -= 1
elif depth == 0 and char in ",;":
break
index += 1
return has_path, has_color
for root in (pathlib.Path(arg) for arg in sys.argv[1:]):
for path in sorted(root.rglob("*.rs")):
source = path.read_text(encoding="utf-8", errors="ignore")
for match in re.finditer(r"\bsvg\(\)", source):
has_path, has_color = chain_flags(source, match.end())
if has_path and not has_color:
line = source.count("\n", 0, match.start()) + 1
print(f"{path.as_posix()}:{line}")
PY
)"
# 4. Nothing bundled is orphaned. A warning, not a failure: an asset reached only
# through a lookup table is still referenced by literal, but a future
# indirection could legitimately hide one.
while IFS= read -r asset; do
[[ -z "$asset" ]] && continue
if ! grep -qxF "$asset" <<< "$referenced"; then
printf 'icon-reference: warning: %s is bundled but never referenced\n' "$asset" >&2
fi
done <<< "$(cd "$ASSET_DIR" && find icons color -type f \( -name '*.svg' -o -name '*.png' \) 2>/dev/null | sort)"
if (( failures )); then
printf 'icon-reference: %d problem(s)\n' "$failures" >&2
exit 1
fi
printf 'icon-reference: all referenced icons are bundled and correctly painted\n'