Repository navigation
Theme server, translation editor, instant theme/language switching, and port links - #361
Merged
Merged
Conversation
tools/theme_server/serve.py renders every PortMaster screen with a theme and shows them in a web page that refreshes when a theme file is saved: any resolution, colour scheme and language, a view of one screen at every common size, and region boxes showing each region's area and its final theme values. --export writes the screens as PNGs instead. Each render process (worker.py) is a headless pugwash for one theme version, scheme, resolution and language, since SDL allows one GUI per process and those are fixed when pugwash starts. Screens use the real ports list and screenshots, downloaded once into a cache. pugwash: - PM_PRETEND_RESOLUTION=WxH sets the screen size in HM_TESTING mode, in place of the commented-out pretend_resolution. - create_screenshot() takes a file name, and reads the pixels before switching back to the window: it saved the window's last frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
The unsupported-system warning, as a TrimUI-based OS gets it, and with the longest set of warnings (spruce on a device with under 1 GB of RAM). The renderer pretends to be such a device for get_startup_warnings(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
PortMaster's translations move from gettext .po/.mo files (synced with
Crowdin) to plain JSON in PortMaster/pylibs/locales, two per language
like messages.po and themes.po were:
<lang>.json {"English": "translation"} the code's _("...")
<lang>.themes.json {"English": "translation"} themes' "text" values
strings.json and strings.themes.json list every English string and where
it's used; tools/i18n.py extract keeps them in step with the code (an ast
scan for _("..."), the same 298 strings xgettext found) and the themes,
and tools/i18n.py check finds unused strings and lost {placeholders}.
- harbourmaster/i18n.py: _() and theme_text(), no dependencies; every
module imports _ from it instead of gettext. A theme can carry its own
"#translations", which win over PortMaster's.
- All 15 languages converted from the .po files: every one of the 4920
lookups gives what gettext gave. Fuzzy entries were already none; the
5 translations of strings the code no longer has are dropped.
- German "Unable to download {runtime}, ..." had lost {runtime} (and had
a typo): fixed.
- The build no longer needs msgfmt, CI no longer installs gettext, and
do_i18n.sh, theme_msgfmt.py and the xgettext snapshot test are gone
(tests/test_i18n.py replaces it and runs everywhere, devices too).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
A Translate view next to Preview lists every string of PortMaster and of
the themes with a language's translation, filtered to the untranslated
ones, those with problems or those on the rendered screens, and saves
them into the checkout's language files (in strings.json's order, so
diffs stay small; a new language gets both its files).
Selecting a string shows the screens that use it, in that language, at
the chosen resolution, and a saved translation re-renders them: the
renderer records which strings each screen looked up, and renderers are
keyed by the theme and the language files together.
Saving only works from the page: /api/translate needs an X-Theme-Server
header, which another site can't send without a CORS preflight the
server never answers.
tools/i18n.py check (and the editor) also flag a translation that lost a
space the English starts or ends with. It found "Music: " and the like
in German ("Musik", which showed "MusikDeaktiviert"), French and Korean:
fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
- PM_HEADLESS=Y (testing mode only): pugwash draws onto a surface with SDL's software renderer, with no video or controller subsystem. The workers use it, so they don't need SDL's dummy driver: muOS's SDL has only "mali". Tried on muOS Jacaranda (RG35XX-H): the server ran there, rendering every screen next to a pugwash that owned the screen. On the desktop the renders are pixel-identical to the dummy driver's. - Chinese, Japanese and Korean showed as boxes: the theme matches against harbourmaster.device_info(), and with PM_PRETEND_RESOLUTION pugwash added the language to a copy, so "font[ja_JP]" never matched. pugwash now writes the capabilities it works out back into device_info(). - serve.py unpacks the Noto fonts at start, as the PortMaster/pugwash script does: on a fresh checkout nothing else had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
An image name starting with "qr:" is a QR code of the rest of the text, made with the vendored qrcode package (unused until now). Through the image templates it updates when its tags change. It's black on white with the quiet zone, one pixel per module and scaled without smoothing, so it stays sharp at any size: per texture on SDL 2.0.12 and up, the scale quality hint before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
The theme server now ships with PortMaster, in pylibs/theme_server (about 100 KB;
tools/theme_server/serve.py and tools/i18n.py run it from a checkout as before).
Options -> Theme Editor starts it and shows its address as text and as a QR
code; a computer or phone on the same network gets the previews and the
translation editor, with the device's themes and ports list. B stops it.
- pugwash/scenes/theme_server.py runs `python3 -m theme_server.serve --managed`
on port 8090 or the next free one, with as many render processes as
MemAvailable allows (1-4). --managed reports the port as a JSON line and stops
when stdin closes, so pugwash dying stops it too. Log: PortMaster/theme_server.txt.
- The default theme has a theme_server section ({theme_server.url},
{theme_server.status}, "qr:{theme_server.url}"); other themes get a plain
screen in PortMaster's own font, so the address stays readable. The preview
catalogue has the screen too.
- Workers run outside testing mode: PM_HEADLESS is honoured anywhere, and
PM_PRETEND_RESOLUTION when headless. They take the ports from a config folder
(--ports-config): the cache's or PortMaster's own, copying the files and
linking the screenshot folders, and never as a first run, so that folder is
left as it was (tested).
- The theme server tests no longer need a git checkout, so the Test Suite runs
them on devices.
Tried on muOS Jacaranda (RG35XX-H) with test build 2026-10-18_0069: the server
ran from the installed copy, rendered from the device's own ports, saved and
removed a translation on the exFAT card, and stopped cleanly when its stdin
closed. Every QR code rendered decodes to its address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
- The list of screens is in catalogue.py, so the server listens (and PortMaster shows the address) before any renderer starts: that took ~15 seconds on a handheld. The first renderer starts in the background meanwhile. - No scheme means the theme's default scheme by name, so the page (which asks for "Light Mode") uses the renderer the server warmed up instead of starting a second one. - --managed logs every request to theme_server.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Choosing a theme, colour scheme or language used to say "You will have to restart" and quit PortMaster. Now it applies straight away, and you're back in the same list with the new look. - PortMasterGUI.reload_theme(then=...) drops what the old theme loaded (images, fonts and rendered text, sounds and music, animations, pallet, overrides, element rects, its resource path), loads the configured theme again and rebuilds the screens: a new main menu with `then`'s scenes on it. - reload_language() also switches the language (lang.set_language: the translations, CURRENT_LANG, the environment) and swaps the language capability, which picks the theme's font for it (font[ja_JP] & co). - The three "restart" prompts are gone, with their translations. - tests/test_reload.py: a screen after switching to another theme, scheme or language and back is pixel-identical to before, German translates, Japanese picks the Noto font, and the themes list applies a choice without quitting. The theme server can now reuse a renderer instead of starting another for each change (next). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Renderers were keyed by theme version, scheme, resolution and language, so every saved theme file, scheme or language change started a new pugwash (8-10 seconds on a handheld). Now there's one per screen size: the worker's new "load" command switches theme, scheme and language with reload_language(), and serve.py sends it when a render needs something else loaded. On the RG35XX-H (muOS Jacaranda): switching scheme 2.5 s and language 2.8 s, each including a ~1.1 s render, instead of a new renderer. A new screen size still starts one. Tests: a renderer switched to a theme, scheme and language draws the same as a new one with those settings; a saved theme reloads in the same process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
A renderer spent most of its start importing pugwash and loading the ports. Now one process, worker.py --zygote, imports pugwash once and forks a renderer per screen size. serve.py sends it a request with the renderer's end of a socket pair attached (SCM_RIGHTS); the renderer talks on that socket as a worker.py does on stdin/stdout. If the zygote can't start, renderers start on their own as before. - Each forked renderer's HarbourMaster has its own tools folder (Renderer(tools_dir=...)); the zygote's holds the theme links. - ThemeEngine.force_scheme (and PortMasterGUI(force_scheme=...)), like force_theme, so a renderer starts in the scheme asked for. - The zygote has no threads (select on its control socket and stdin): it forks. On the RG35XX-H (muOS Jacaranda): the first screen 2.4 s (was 8.4), a new screen size 2.0 s (was 12.5), switching scheme or language ~1.1 s, and each screen ~0.45 s (was ~1.1). Tests: forked renderers draw the same as one of their own, and a renderer that dies is replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
…ning - The page asked for every screen at once, so after changing a setting the server still drew the whole old batch first. Renders now go through a queue, two at a time; a change (theme, scheme, size, language, view, a saved file) drops what's waiting and aborts what's running. - A line with a spinner says "Rendering 3 of 25...", or "Starting the renderer for 1024x768, a few seconds..." / "Loading the theme and language..." from the new /api/status (which renderers are starting, loading or rendering). Tried in Chromium: changing the scheme mid-render, no more renders of the old one were asked for and the new set finished 1.4 s later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
The language list was made when pugwash.lang was imported, before any translation loaded, so its names were always English; now that the language can change without a restart that showed. lang_list() translates them when it's called, and the language screen asks for it each time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
The Test Suite runs these on devices now; up to six renderers at 60-90 MB each is too much for the low-memory ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
The Test Suite leaves NotoSans.tar.xz out, so on a device there's no Japanese font for these to load (found running them on muOS). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
On GitHub's runners ctypes.util.find_library (pysdl2 looking for SDL) runs gcc, which can leave its temporary files in TMPDIR, so the folder wasn't always empty and the test failed now and then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
- pugwash/main.py: setup_proxy from main, _ from harbourmaster.i18n. - The language screen keeps the instant switch, with main's guard for the blank row. - main's language, theme and colour scheme tests expected the restart prompt; they now check the instant reload instead, keeping the regressions they covered (choosing the system's language with none in the config, a new theme dropping the old scheme). - strings.json re-extracted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Port 8090 (and the nine after it) could clash with a CFW's own services. The on-device editor now picks a random port from 49152-65535, which is never assigned to a service, and keeps it in config.json (theme_server_port) so the address stays the same between sessions. If it's taken, the next free one of the 20 after it, or any free port (serve.py --any-port), is used and kept instead. listen() stops at 65535. The desktop serve.py keeps --port 8090. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
X on the port info screen opens a port's links: its page on the PortMaster
website (ports from the PortMaster and Multiverse sources) and every store
in its port.json, the game's page and the developer's, each shown as a QR
code and its address, to open on a phone.
port.json "store" names are typed by hand ("Itch", "itch.io", "GoG ",
"Gamejolt", ...), so they're tidied into one name per store; entries that
aren't http(s) links are skipped.
The default theme has a port_links section, with a smaller QR and text at
320x240. Themes without one get a plain built-in screen in PortMaster's own
font, as the theme editor's screen does (plain_font() is shared now). The
theme designer lists the screen, using a port with several links.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
…s list Themes made before the port links and theme editor screens showed a plain built-in screen for both, and custom themes that ship with CFWs (or are on people's devices) won't be updated. When a theme has no "port_links" or "theme_server" section, one is now made from its "themes_list" section as it loads: its list, panels and fonts, the QR code where the theme screenshot goes, the address where the description goes (the editor's instructions where the list goes, a size smaller). A font with capitals only (Doom's) is swapped for the list's, so addresses read right. A themes list without a screenshot region (basic_theme) still gets the plain screen. Checked by rendering the themes as published before (the nine in PortMaster-Themes, and the MinUI theme PakUI and TRIMUI ship) at 480x320 to 1280x720. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
… Host
- The code calls .format() on what _() returns, so a translation with
a {field} the English doesn't have, or a stray { or }, raised there
({lang_name} (Selected) -> {bogus} stopped the language screen with a
KeyError). harbourmaster.i18n leaves those out when it loads a
language (the string shows in English), and the translation editor
refuses to save them, saying why. No shipped translation is affected.
- The editor's POSTs (saving translations) also need a Host header that
is an IP address or localhost: a DNS rebinding page reaches the
server under its own domain name, and could otherwise send the
page's header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Noto Sans JP/SC/TC/HK/KR have a much taller line height than DejaVu
Sans at the same size, so in Japanese the main menu's six entries ran
into the footer at 640x480 ("終了" over "PortMaster: ..."). The theme
server's export showed it. Lines and list rows are now 0.8 of the
font's height in those languages: everything fits where it does in
English.
The theme editor's preview shows an address with a port from its new
range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
…elds Found by sending it bad requests: render.png?theme=../../../etc was a 500 (the renderer's "no theme" error), a translation with "lang": 5 dropped the connection (a TypeError nothing caught), and render.png took any \w\w_\w\w as a language where the rest of the API takes xx_XX only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
The theme editor screen made from a theme's themes list put the instructions in the list's area without its border-x, so the text ran under the panel's edges (Doom, FFVII) or past it (Zelda). Seen rendering the device's themes with their own sections taken out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
English:
- "Port X installed failed." -> "Port X failed to install."
- "sucessfully" -> "successfully"; "Unable do download a runtime when
in offline mode." -> "Unable to download a runtime in offline mode."
- One "Select Color Scheme" (Options said Color, the screen Colour),
with a full stop on its description; "soundfx" -> "sound effects";
the move-ports question gets its question mark.
- PortMaster's own update says "PortMaster installed successfully." /
"PortMaster failed to install." instead of "Port 'PortMaster' ...".
- American spelling ("color") in the theme editor page and the docs;
pugwash's --no-colour/--force-colour flags are unchanged.
- A theme string from no known theme ("... dance until tomorrow") is
dropped.
Each language keeps its translations under the corrected English (a
full stop or question mark added where the English gained one).
Translations whose line breaks didn't match the English: Russian ran
"HM: ..." and the free space together on one line, Dutch put three
lines of port info on one, Romanian split a one-line layout in three,
and German/Chinese had extra or missing blank lines in message boxes.
German also: "Möchten Sie", "installierte". tools/i18n.py check is
clean for every language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
About 1,080 translations, so every string in strings.json and
strings.themes.json has one in each language (strings made only of
{tags} need none). Most are the strings since the last Crowdin sync:
the theme editor, port links, autoinstall's summary, the startup
warnings, sorting, debug mode. Italian, Japanese, Swedish and Romanian
also get the older ones they never had. Each language keeps the words
it already used (ports are "jogos" in Brazilian Portuguese, the
runtime is "Körtid" in Swedish, and so on). Every one passes
tools/i18n.py check and safe_translation. They're machine-written:
native speakers should look them over in the translation editor.
The startup warnings screen used DejaVu Sans whatever the language, so
Chinese, Japanese and Korean showed as boxes once translated: its
hard-coded section now has the default theme's font for each
language. Its "Wait N seconds" and "Please wait." are translatable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
New: test_region_validation.py (every Region._verify_* check),
test_theme_loading.py (#base, elements, requirement tags, colour
schemes, resources, a theme's #translations, what PortMaster adds),
test_lang.py (picking and switching the language, the language list,
i18n lookups) and test_i18n_tool.py (extract, check, problems).
What they found:
- check_lang("") was German: every language "starts with" "", so an
empty LANG (or ".UTF-8", "_US") made DEFAULT_LANG German while the
screen was English, and choosing German "reset" to the system's.
- "font-scale": 2 or "line-height": 1 (whole numbers) were "not an
float"; a theme's "text": 5 raised a TypeError (it raised a str)
instead of a theme error.
- theme_load read #base in its loop, so a #base after the sections
(a colour scheme's is added last) never applied; an unknown colour
scheme fell back to the first one, not the theme's default-scheme;
and a section with a requirement ("main_menu[640x480]") merged each
region with the whole old section and dropped the rest. No shipped
theme hit these: every render of the device's 10 themes is the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
test_theme_server_http.py runs the web API against a fake render pool (no SDL): every route, render parameters and cache headers, a missing section (404) and a renderer error (500), bad sizes, languages, screens and themes, versions changing with the theme and the translations, translation saves (problems, removal, theme strings, ten at once, the ones refused and the requests forbidden), the themes listed and their info, the languages, and --export. test_theme_editor_scene.py drives Options -> Theme Editor with a fake server process: the command it runs, the port it keeps, and what the screen says when the server can't start, refuses, dies before answering, stops by itself or the device has no network; B stopping it, and a server that won't stop being killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Theme preview server (
tools/theme_server/serve.py,pylibs/theme_server/):--exportwrites the screens as PNGs, for checking a theme at every size.Translations as JSON instead of gettext:
locales/<lang>.jsonand<lang>.themes.json.strings.jsonlists every string and where it's used, andtools/i18n.py extract/checkkeep them up to date..po/.mofiles and the Crowdin sync are gone.Theme Editor on the device (Options → Theme Editor):
"image": "qr:{tag}".Instant switching: changing the theme, colour scheme or language applies straight away, with no restart.
Port links: X on a port's info screen shows the port's PortMaster website page and its store pages as QR codes. Themes made before these screens get them built from their themes list.
Safety:
{placeholder}the English doesn't have, or a stray brace, would raise in the code's.format(). These are skipped when a language loads (the English shows instead), and the editor refuses to save them.Hostthat's an IP address orlocalhost.Default theme: Chinese, Japanese and Korean lines are 0.8 of the font's height, so the main menu fits at 640×480.
Testing: