Files
libretro/scripts/sitesteps.py
T
Abdessamad Derraz 77fef7289f feat: show the handheld demo and steps per tab
make_demo.py records a second variant: the full-screen interface at the
640x480 screen of an R36S, on an ArkOS root made with bwrap, started as
the Tools menu launcher starts it (RETROBIOS_PAD=gptokeyb), driven by the
keys gptokeyb sends for the buttons. demo.json holds one entry per
variant with its alt text. No Windows variant: under pwsh on Linux the
installer's summary gives --dest /mnt/e/bios back.

The quick install box of Get your files gets an "On the device" tab with
the handheld recording, the terminal recording under the others but
Windows, and, under the text of each computer tab, a drawing of opening
the terminal and pasting (sitesteps.py, inline SVG in the theme's
colours, title and desc). The terminal steps name the physical action.
2026-11-09 07:35:03 +01:00

270 lines
10 KiB
Python

"""What the install box of Get your files shows under each tab.
Two things per tab, both drawn from data:
- The steps that differ by system, opening a terminal and pasting the line,
as an inline SVG: one card per step, a pictogram and a short caption,
drawn with sitediagrams' primitives so it takes the theme's colours in
light and dark. The text steps above it (platforms/_guides.yml,
terminals) stay the accessible source. The SVG says the same in its title
and desc. The pictograms are generic shapes (a menu, a search field, a
window, keys, a mouse, the power symbol), no vendor's interface or logo.
- The installer demo recorded for that place (scripts/make_demo.py): the
full-screen interface on a handheld under "On the device", the terminal
recording under the others but Windows. No Windows recording is
published (make_demo.py says why), and a curl line recorded in a Linux
terminal under the PowerShell command would contradict it.
"""
from __future__ import annotations
from dataclasses import dataclass
from xml.sax.saxutils import escape
import sitemarkup as markup
from sitediagrams import (
FONT,
MONO_STYLE,
NARROW,
STROKE,
TEXT_SIZE,
WIDE,
_badge,
_badge_side,
_line,
_n,
_rect,
_text,
wrap,
)
KEY_SIZE = 11.5
ICON_H = 44.0
@dataclass(frozen=True)
class Step:
"""One card: a pictogram (*icon* and its *word*, the text it shows:
keys joined by "+", the word typed in a search field) and a caption."""
icon: str
caption: str
word: str = ""
STEPS: dict[str, tuple[str, tuple[Step, ...]]] = {
"windows": ("Windows: open PowerShell and paste the line", (
Step("menu", "Open the Start menu"),
Step("search", "Type PowerShell and press Enter", "PowerShell"),
Step("copy", "Click Copy, the button by the line"),
Step("mouse", "Right-click in the window to paste"),
Step("keys", "Press Enter", "Enter"),
)),
"macos": ("macOS: open Terminal and paste the line", (
Step("keys", "Press Cmd+Space", "Cmd+Space"),
Step("search", "Type Terminal and press Return", "Terminal"),
Step("copy", "Click Copy, the button by the line"),
Step("keys", "Press Cmd+V to paste", "Cmd+V"),
Step("keys", "Press Return", "Return"),
)),
"linux": ("Linux: open a terminal and paste the line", (
Step("keys", "Open a terminal, often Ctrl+Alt+T", "Ctrl+Alt+T"),
Step("copy", "Click Copy, the button by the line"),
Step("keys", "Press Ctrl+Shift+V to paste", "Ctrl+Shift+V"),
Step("keys", "Press Enter", "Enter"),
)),
"steamdeck": ("Steam Deck: open Konsole and paste the line", (
Step("power", "Hold power, pick Switch to Desktop"),
Step("window", "Open Konsole"),
Step("copy", "Click Copy, the button by the line"),
Step("keys", "Press Ctrl+Shift+V to paste", "Ctrl+Shift+V"),
Step("keys", "Press Enter", "Enter"),
)),
}
# The recording each tab shows (docs_assets/demo/demo.json keys), "" for none.
TAB_DEMOS = {"device": "handheld", "windows": ""}
DEFAULT_DEMO = "default"
def demo_variant(tab: str) -> str:
return TAB_DEMOS.get(tab, DEFAULT_DEMO)
def description(tab: str) -> str:
"""The steps in words: the SVG's desc."""
return " ".join(f"{index}. {step.caption}." for index, step in
enumerate(STEPS[tab][1], start=1))
# Pictograms, drawn in a box *w* wide and ICON_H high from (x, y).
def _stroke(d: str, cls: str = "rb-d-line", width: float = 1.5) -> str:
return f'<path d="{d}" fill="none" stroke="{STROKE}" stroke-width="{_n(width)}" class="{cls}"/>'
def _key_width(label: str) -> float:
return max(20.0, len(label) * KEY_SIZE * 0.62 + 10)
def _keys(x: float, y: float, w: float, word: str) -> list[str]:
labels = word.split("+")
total = sum(_key_width(label) for label in labels) + 4 * (len(labels) - 1)
left = x + (w - total) / 2
out = []
top = y + ICON_H / 2 - 12
for label in labels:
width = _key_width(label)
out.append(_rect(left, top, width, 24, accent=True))
out.append(
f'<text x="{_n(left + width / 2)}" y="{_n(top + 16.5)}" font-size="{_n(KEY_SIZE)}" '
f'text-anchor="middle" fill="currentColor" style="{MONO_STYLE}">{escape(label)}</text>')
left += width + 4
return out
def _menu(x: float, y: float, w: float) -> list[str]:
cx, top = x + w / 2, y + 4
out = [_rect(cx - 22, top, 44, 36)]
out += [_stroke(f"M{_n(cx - 14)} {_n(top + 10 + 8 * i)} H{_n(cx + 14)}") for i in range(3)]
return out
def _search(x: float, y: float, w: float, word: str) -> list[str]:
width = min(w - 4, len(word) * KEY_SIZE * 0.62 + 34)
left, top = x + (w - width) / 2, y + ICON_H / 2 - 12
return [
_rect(left, top, width, 24, accent=True),
f'<circle cx="{_n(left + 11)}" cy="{_n(top + 11)}" r="5" fill="none" stroke="{STROKE}" '
'stroke-width="1.5" class="rb-d-line"/>',
_stroke(f"M{_n(left + 15)} {_n(top + 15)} L{_n(left + 19)} {_n(top + 19)}"),
f'<text x="{_n(left + 25)}" y="{_n(top + 16.5)}" font-size="{_n(KEY_SIZE)}" '
f'fill="currentColor" style="{MONO_STYLE}">{escape(word)}</text>',
]
def _window(x: float, y: float, w: float) -> list[str]:
cx, top = x + w / 2, y + 2
return [
_rect(cx - 32, top, 64, 40),
_stroke(f"M{_n(cx - 32)} {_n(top + 9)} H{_n(cx + 32)}"),
f'<text x="{_n(cx - 24)}" y="{_n(top + 28)}" font-size="{_n(KEY_SIZE)}" '
f'fill="currentColor" style="{MONO_STYLE}">&gt;_</text>',
]
def _copy(x: float, y: float, w: float) -> list[str]:
cx, top = x + w / 2, y + 6
return [_rect(cx - 12, top, 18, 24), _rect(cx - 6, top + 7, 18, 24, accent=True)]
def _mouse(x: float, y: float, w: float) -> list[str]:
cx, top = x + w / 2, y + 2
return [
f'<rect x="{_n(cx - 13)}" y="{_n(top)}" width="26" height="40" rx="12" fill="none" '
f'stroke="{STROKE}" stroke-width="1.5" class="rb-d-line"/>',
# The right button, the one to press.
f'<path d="M{_n(cx + 1)} {_n(top + 1)} A12 12 0 0 1 {_n(cx + 12)} {_n(top + 13)} '
f'L{_n(cx + 1)} {_n(top + 13)} Z" fill="currentColor" class="rb-d-num"/>',
_stroke(f"M{_n(cx)} {_n(top)} V{_n(top + 14)} M{_n(cx - 13)} {_n(top + 14)} "
f"H{_n(cx + 13)}"),
]
def _power(x: float, y: float, w: float) -> list[str]:
cx, cy = x + w / 2, y + ICON_H / 2 + 2
return [
_stroke(f"M{_n(cx - 9)} {_n(cy - 11)} A14 14 0 1 0 {_n(cx + 9)} {_n(cy - 11)}", width=2.5),
_stroke(f"M{_n(cx)} {_n(cy - 18)} V{_n(cy - 2)}", width=2.5),
]
def _icon(step: Step, x: float, y: float, w: float) -> list[str]:
if step.icon == "keys":
return _keys(x, y, w, step.word)
if step.icon == "search":
return _search(x, y, w, step.word)
return {"menu": _menu, "window": _window, "copy": _copy, "mouse": _mouse,
"power": _power}[step.icon](x, y, w)
def _wide(steps: tuple[Step, ...]) -> tuple[float, float, list[str]]:
"""The cards in a row, joined by arrows."""
gap = 14.0
width = (WIDE - gap * (len(steps) - 1) - 2) / len(steps)
captions = [wrap(step.caption, width - 12, TEXT_SIZE) for step in steps]
height = 34 + ICON_H + 10 + max(len(lines) for lines in captions) * 18 + 8
out: list[str] = []
for index, (step, lines) in enumerate(zip(steps, captions)):
x = 1 + index * (width + gap)
out.append(_rect(x, 1, width, height))
out += _badge(x + 8, 8, str(index + 1))
out += _icon(step, x + 4, 30, width - 8)
y = 30 + ICON_H + 24
for line in lines:
out.append(_text(x + width / 2, y, line, anchor="middle"))
y += 18
if index < len(steps) - 1:
mid = 1 + height / 2
out += _line([(x + width + 1, mid), (x + width + gap - 1, mid)])
return WIDE, height + 2, out
def _narrow(steps: tuple[Step, ...]) -> tuple[float, float, list[str]]:
"""One card a row: number, pictogram, caption."""
icon_w, row_gap = 112.0, 8.0
text_x = 8 + _badge_side("9") + 6 + icon_w + 8
out: list[str] = []
y = 1.0
for index, step in enumerate(steps):
lines = wrap(step.caption, NARROW - text_x - 8, TEXT_SIZE)
height = max(ICON_H + 12, len(lines) * 18 + 16)
out.append(_rect(1, y, NARROW - 2, height))
out += _badge(8, y + height / 2 - 10, str(index + 1))
out += _icon(step, 8 + _badge_side("9") + 6, y + (height - ICON_H) / 2, icon_w)
ty = y + height / 2 - (len(lines) - 1) * 9 + 5
for line in lines:
out.append(_text(text_x, ty, line))
ty += 18
y += height + row_gap
return NARROW, y - row_gap + 1, out
def _svg(tab: str, layout: str) -> str:
title, steps = STEPS[tab]
width, height, body = (_wide if layout == "wide" else _narrow)(steps)
base = f"rb-steps-{tab}-{layout}"
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {_n(width)} {_n(height)}" '
f'width="{_n(width)}" height="{_n(height)}" role="img" '
f'aria-labelledby="{base}-t {base}-d" font-family="{FONT}" '
f'class="rb-diagram rb-diagram-{layout}">'
f'<title id="{base}-t">{escape(title)}</title>'
f'<desc id="{base}-d">{escape(description(tab))}</desc>'
+ "".join(body) + "</svg>"
)
def steps_figure(tab: str) -> list[str]:
"""The figure of a tab's steps, both layouts, or nothing for a tab with
no drawing."""
if tab not in STEPS:
return []
return [f'<figure class="rb-figure rb-steps" {markup.search_exclude()}>',
_svg(tab, "wide") + _svg(tab, "narrow"), "</figure>", ""]
def tab_demo(tab: str) -> list[str]:
"""The recording a tab shows, as generate_site writes the component."""
from generate_site import install_demo # noqa: PLC0415
variant = demo_variant(tab)
return install_demo(variant, tab) if variant else []
def install_box(tabs: tuple[str, ...]) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
"""What terminal_tabs puts under each tab's steps and under its command."""
return ({tab: steps_figure(tab) for tab in tabs},
{tab: tab_demo(tab) for tab in tabs})