Files
punktfunk/scripts/gen-os-icons.sh
enricobuehler be8183caab feat(console): host tiles get their OS mark, the chip gets a battery, the strip gets Rescan
`HostRow.os` has been plumbed since the model landed, with a comment saying the
drawing was a follow-up because "the Skia glyph set doesn't exist yet". It does
exist: assets/os-icons ships thirteen licensed masters, and
`pf_client_core::os::os_icon_tokens` already resolves a chain to them - walking
most-specific-first and applying the brand aliases (`macos` -> `apple`,
`steamos` -> `steam`). Every other front-end walks that same list.

So the console takes the shared resolver rather than inventing one, and gets its
table GENERATED from the masters (`scripts/gen_os_mark_table.py`, hooked into
the existing `gen-os-icons.sh`) rather than hand-transcribed. Thirteen paths of
up to 3.5 kB where one mangled character is a silently wrong logo is not work
for a human, which is precisely the reasoning the launcher-icon tables already
carry. A new master now reaches the console for free; the script's closing note
says so.

Two corrections to the plan this implements, both found in the code:
- The chain is SLASH-separated and resolves most-specific-FIRST, not "the first
  known token of a `;`-chain". A `linux/fedora/bazzite` host draws Bazzite, and
  falls back through Fedora to Tux - so the console is right about thirteen
  distros rather than the four the plan scoped.
- The hint bar was already a glass pill, not "ink on the field". What it was
  missing is that it mixed its OWN glass (a flat wash and a hand-rolled stroke),
  making it the one floating surface that ignored the palette; it now goes
  through `theme::panel` like the chip and the toast, and picks up the lit edge.

`draw_monogram` becomes `draw_badge`: the OS mark when the chain resolves, the
initial when it doesn't. A substitution, not an addition - a badge showing both
a Tux and an "L" says the same thing twice - and an older host that advertises
no `os` keeps its monogram pixel for pixel.

The controller chip gains a pad silhouette and a battery pip. `PadInfo` gets an
additive `battery: Option<PadBattery>`; nothing crosses the wire, this is local
SDL state. The plan expected to poll "on the existing pad-refresh cadence" -
there isn't one, `publish()` is entirely event-driven (hotplug, pin change). And
`pad_info` is deliberately open-free because an open GRABS the hardware, while
SDL only reports power for an OPEN device. So the level is read from the ONE pad
the service already holds open - `menu_open`, the nav pad, which is open exactly
while a console is on screen and is the only pad any UI asks about - on a 15 s
poll inside the loop that already wakes every 10 ms. Every other pad publishes
`None`, which is the honest answer.

`None` renders as no battery at all, never 0 %: a wired pad, a Steam virtual pad
and SDL's `-1` "powered, level unknown" are all the same non-answer, and 0 % is
the one reading that sends someone hunting for a charger. Charging outranks the
low-charge red, because a pad at 4 % on the cable is not the problem a pad at
4 % off it is.

Finally, Rescan: a second sentinel tile trailing Add Host, sending the
`ConsoleCmd::Probe` that has existed unsent by any screen since it was written.
A controller surface has no pull-to-refresh, so the affordance has to be a tile.
The two trailing tiles are actions rather than hosts, so `hosts.get(i)`
answering both "which host" and "which action" with one `None` became a `Slot`
enum - with a second action tile that ambiguity is a bug waiting, and the test
that matters is that an accidental A on the end of the strip can never start a
session.

Verified in the pf-gtkflow container: fmt, clippy --all-targets -D warnings,
plain build, 104 tests green.
2026-08-16 15:36:36 +02:00

109 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Derive the per-client host-OS-icon assets from the assets/os-icons masters.
#
# The masters are monochrome `fill="currentColor"` SVGs, one per icon token of the host's
# OS-identity chain (see assets/os-icons/README.md). Three clients need a baked derivative
# because they cannot consume the master directly:
#
# GTK shell symbolic SVG, black fill -> clients/linux/data/icons/scalable/actions/
# Windows shell PNG, h=32, mid-grey -> clients/windows/assets/os/
# Apple clients vector PDF, black fill -> clients/apple/.../OsIcons.xcassets/
#
# The web console, the Decky plugin and the Android client transcribe the master's path
# data inline instead — those are hand-kept, and this script prints them at the end so a
# new token can be pasted straight in.
#
# Idempotent. Usage: bash scripts/gen-os-icons.sh [token ...] (default: every master)
set -euo pipefail
cd "$(dirname "$0")/.."
MASTERS=assets/os-icons
GTK=clients/linux/data/icons/scalable/actions
WIN=clients/windows/assets/os
APPLE=clients/apple/Sources/PunktfunkKit/Resources/OsIcons.xcassets
# The Windows shell has no vector element and no theme-aware tint, so its PNGs are baked in
# one mid-grey that stays legible on both the light and the dark WinUI theme.
WIN_GREY='#8A8F98'
WIN_HEIGHT=32
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
command -v rsvg-convert >/dev/null 2>&1 || {
echo "rsvg-convert not found (brew install librsvg / apt install librsvg2-bin)" >&2
exit 1
}
tokens=("$@")
if [ ${#tokens[@]} -eq 0 ]; then
for f in "$MASTERS"/*.svg; do tokens+=("$(basename "$f" .svg)"); done
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
for t in "${tokens[@]}"; do
src="$MASTERS/$t.svg"
[ -f "$src" ] || { echo "no master for token '$t' ($src)" >&2; exit 1; }
log "$t"
# GTK: the master with the fill resolved to black — Adwaita recolors a `-symbolic` icon
# from the fill it finds, so the value only has to be a real colour, not the final one.
sed 's/currentColor/#000000/' "$src" > "$GTK/pf-os-$t-symbolic.svg"
# Windows: same black-to-grey substitution, rasterized at a fixed height so every mark
# shares an optical size and keeps its own aspect ratio.
sed "s/currentColor/$WIN_GREY/" "$src" > "$tmp/$t.grey.svg"
rsvg-convert -h "$WIN_HEIGHT" -f png -o "$WIN/$t.png" "$tmp/$t.grey.svg"
# Apple: a vector PDF at the master's natural size, in a template imageset — SwiftUI
# tints it from foregroundStyle, so the baked colour is irrelevant.
sed 's/currentColor/#000000/' "$src" > "$tmp/$t.black.svg"
mkdir -p "$APPLE/os-$t.imageset"
rsvg-convert -f pdf -o "$APPLE/os-$t.imageset/$t.pdf" "$tmp/$t.black.svg"
cat > "$APPLE/os-$t.imageset/Contents.json" <<JSON
{
"images" : [
{ "filename" : "$t.pdf", "idiom" : "universal" }
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
JSON
done
echo
# The Skia console parses SVG path data at RUNTIME, so it needs no baked derivative — it
# needs the path string, and gets it as a generated Rust table rather than a hand-kept
# inline registry. Thirteen paths of up to 3.5 kB where one mangled character is a silently
# wrong logo is not transcription work for a human. Always regenerated from EVERY master,
# whatever tokens this script was invoked with: it is one file, and a partial rewrite would
# drop the rest.
log "console Rust table (crates/pf-console-ui/src/os_marks.rs)"
python3 scripts/gen_os_mark_table.py
echo
log "Inline path data (web/src/components/os-icon.tsx, clients/decky/src/os-icon.tsx,"
log " clients/android/.../components/OsIcons.kt — hand-kept, paste from here)"
for t in "${tokens[@]}"; do
python3 - "$MASTERS/$t.svg" "$t" <<'PY'
import re, sys
svg = open(sys.argv[1]).read()
box = re.search(r'viewBox="([^"]+)"', svg).group(1)
d = re.search(r'<path[^>]* d="([^"]+)"', svg).group(1)
w, h = box.split()[2:]
print(f'\n {sys.argv[2]}: viewBox "{box}" (viewport {w} x {h})\n {d}')
PY
done
echo
log "Remember: a NEW token also has to be added to each client's shipped-token list —"
log " clients/linux/src/ui_hosts.rs, clients/linux/data/resources.gresource.xml,"
log " clients/windows/src/app/os_icons.rs, clients/apple/.../PunktfunkKit/OsIcon.swift,"
log " plus the three inline registries above. (The console's table is generated above and"
log " needs no list — it ships whatever masters exist.)"