Skip to content

feat(ui): add game shortcuts to the Ports menu - #9

Open
alexander-clawthorne wants to merge 13 commits into
Producdevity:feat/geforce-nowfrom
alexander-clawthorne:feat/game-shortcuts
Open

alexander-clawthorne wants to merge 13 commits into
Producdevity:feat/geforce-nowfrom
alexander-clawthorne:feat/game-shortcuts

Conversation

@alexander-clawthorne

@alexander-clawthorne alexander-clawthorne commented Oct 4, 2026 •

Copy link
Copy Markdown

Based on feat/geforce-now like #8, and independent of it: the two touch different parts of handheld_ui.zig and merge cleanly. Branch name and commits follow the feat/<kebab-case> and conventional-commit patterns of #2-#4. The fix for a bug found during device testing is a separate commit, as #3 did.

What does this change?

Adds game shortcuts: a game from the library gets its own Ports entry that opens straight into that game. The idea comes from the shortcut feature in Kasumi (a GeForce NOW client for the 3DS). No Kasumi code is used, since it is GPL-3.0.

  • In the library, Select on a title writes <Title>.sh next to GreenOvercast.sh, and Select again removes it. The prompt bar shows SELECT PORTS when shortcuts are available, and a short notice confirms each action in place of the second prompt row.
  • The shortcut sets GREENOVERCAST_SERVICE and GREENOVERCAST_AUTOSTART=1, then execs the real launcher with the title's product id as $1. exec keeps $0 pointing at the real launcher, which PortMaster's control.txt derives GAMEDIR from.
  • On startup, autostart starts that title once if it is in the catalog. The existing argv[1] cursor handling is reused through a new exact-match library.findTitle. If the title is no longer in the catalog, the library opens as usual. After the session ends, the library is shown, as for any other launch.
  • muOS: the title's cached artwork is converted to a PNG that fits inside 320x240 and written to the Ports catalogue box/ folder. The folder and the "fit inside 320x240, never enlarge" rule are the ones PortMaster's muos/image_smash.txt uses for installed ports. The launcher exports GREENOVERCAST_PORT_ARTWORK_DIR only when that folder exists. Other CFWs get the entry without artwork; their frontends store artwork differently and I can't test them.
  • Safety:
    • Every generated script carries a marker line, and only files with that marker are ever replaced or removed. A real port with the same name is left alone ("A PORT WITH THIS NAME ALREADY EXISTS").
    • Titles are reduced to a conservative ASCII file name.
    • The launcher path and product id are validated before they are single-quoted into the script.
    • Select acts on release and is disarmed by Start, so the Select + Start exit chord never creates a shortcut.

New code:

  • src/ui/port_shortcut.zig: file names, script generation, add and remove.
  • src/ui/cover_image.zig: box-filter fit and a small PNG encoder built on std.compress.zlib. The FFmpeg build has no PNG encoder.
  • Loader.cachedCoverPng in artwork_loader.zig.
  • The launcher exports GREENOVERCAST_PORT_LAUNCHER and, on muOS, GREENOVERCAST_PORT_ARTWORK_DIR.

How did you test it?

  • tools/zig.sh build test and tools/zig.sh build fmt-check pass. New tests cover:
    • file name sanitising
    • exact script contents
    • quoting rejection (launcher with a quote, relative launcher, product id with shell syntax)
    • add and remove without touching a same-named foreign port, including the 0755 mode
    • image fitting and box filtering
    • PNG header and IEND CRC, plus a zlib round-trip of the image data
  • tools/zig.sh build release passes, and nm -D --undefined-only webrtc_stream | grep -c __aarch64_ is 0.
  • On an Anbernic RG35XX-H (H700, muOS, kernel 4.9.170), with GeForce NOW and CONTROL:
    1. Select added CONTROL Resonant.sh to Ports, along with box/CONTROL Resonant.png (170x240 from the 256-wide portrait art). The entry showed its cover in the muOS Ports list.
    2. Launching the entry went straight into a session (Starting shortcut title), skipping the library.
    3. Select removed the script and its artwork.
    4. The shortcut also works when GreenOvercast itself is started from muOS Applications through a wrapper that execs the port launcher. On muOS, realpath "$0" resolves the launcher to /mnt/union/ROMS/PORTS/..., and the shortcut works from that path.
  • Bug found and fixed on device: the first build saved shortcuts without artwork on muOS. Dir.readFileAlloc sizes its buffer with File.stat(), which uses statx, and that fails on kernel 4.9 (the limitation stat_compat.zig documents). The second commit reads the cache with readToEndAlloc instead.
  • Not tested: Xbox shortcuts (I only use GFN; the Xbox path uses the same product id matching), and other CFWs.
  • Unrelated observation: in one shortcut-launched session the client exited when I pressed A in CONTROL's launcher. A relaunch worked, and I couldn't reproduce it. The launcher truncates its log on every start, so that run's log was lost. The shortcut code is not involved once the session starts, but I'm mentioning it in case you've seen it.
  • Related upstream note, not changed here: video_pipeline.zig:154 calls file.getEndPos(), which also goes through File.stat()/statx. On 4.9 kernels that probably fails and returns 0, so the H.264 bootstrap file may never be read. I haven't verified this; happy to look at it separately.

Review follow-up (cubic)

I fixed five of cubic's six findings in two focused commits. The sixth is documented rather than changed; my reasons are in its thread.

  • b3bbf8e fix(ui):
    • a marked script only counts as this title's shortcut when it launches the same product id, so same-named titles stay apart
    • writes go through std.fs.AtomicFile (random name, exclusive create)
    • removal deletes the artwork first, best effort, and the script last
    • a cache miss no longer logs
    • a comment explains why cover generation stays on the caller's thread
  • 3cb0f78 fix(portmaster): the launcher resolves itself through BASH_SOURCE and logs when shortcuts are disabled.

Re-verified on the RG35XX-H after these commits: remove and re-add worked, the cover was rewritten, the script is mode 755, and no temporary files were left behind.

Review round 2 (maintainer)

Both requested changes are fixed, one commit each:

  • d4842b5 fix(ui): shortcut artwork is created only when no image exists, and its CRC-32 and size are recorded in the shortcut's own script. It is replaced or removed only while that record still matches the file, so other ports' images in muOS's shared catalogue are never overwritten or deleted. Regression tests cover an existing PNG with and without a cached cover, owned artwork, and artwork replaced after creation.
  • 5c17dc0 fix(portmaster): the launcher path is resolved before PortMaster's helpers run or the directory changes, and exported only when it is an existing file, so bash ./GreenOvercast.sh from the Ports folder works.

Re-verified: build test, fmt-check and build release (0 __aarch64_ imports). ShellCheck is unchanged from the base. Your P2 and P3 reproductions now pass, the P3 mock covering absolute, relative and sourced starts. On the RG35XX-H, add → remove → add created the cover, recorded it, deleted it and recreated it, with no temporary files left behind.

Review round 3

  • 3e77e68 fix(ui): addresses cubic's two findings. add() now builds the script before touching any file, creates new artwork exclusively (O_EXCL), and rolls the artwork back if saving the script fails.

While skimming the full change, I also found and fixed a few more issues:

  • 94550d0: only absolute shortcut paths are accepted from the environment, because openDirAbsolute() asserts. Autostart requires GREENOVERCAST_AUTOSTART=1.
  • ad94ad9: a pending Select is cancelled when the search keyboard or settings open.
  • 5d97941: a shortcut whose game is gone shows "SHORTCUT GAME NOT FOUND", and shortcut problems are shown in red.
  • 8823480: a shortcut's script is kept while its artwork can't be removed, so no image is orphaned. Names can't imitate the artwork record.
  • 80ef3ee: the rollback tests now prove the rollback ran, and the root check works on macOS hosts.

Each commit passes build test and fmt-check on its own. build release passes with 0 __aarch64_ imports. Re-tested on the RG35XX-H: add and remove with artwork, the missing-game notice, and Select with search open.

AI disclosure

Per CONTRIBUTING: this was written with AI assistance (Claude). I reviewed the diff, built it, and tested it on hardware myself. I'm happy to change the button, the naming, or how other CFWs should be handled.

🤖 Generated with Claude Code


Summary by cubic

Adds game shortcuts to the Ports menu: pressing Select on a library title creates a launcher next to GreenOvercast.sh that starts that game directly, and pressing Select again removes it. The launcher re-runs the port launcher with the title's product id and autostart, so launching the shortcut skips the library. On muOS, the title's cached artwork is also written to the Ports catalogue as a PNG so the entry shows a cover.

Only scripts carrying the shortcut marker are ever replaced or removed, and only when they launch the same product id, so existing ports with the same name are never touched. Writes go through atomic files, the launcher path is resolved from BASH_SOURCE before PortMaster's helpers change directories, and launcher paths are capped at 512 bytes so every generated script fits in the recognition buffer.

Bug Fixes

  • Shortcuts were saved without artwork on muOS because the artwork read used File.stat(), which needs statx and fails on the 4.9 kernels these handhelds ship; it now reads with readToEndAlloc.
  • Artwork in muOS's shared catalogue is only replaced or removed while the CRC-32 and size recorded in the shortcut's own script still match the file on disk.
  • A failed script save rolls the artwork back, a failed artwork removal keeps the shortcut and its record so a later attempt can clean up, and new artwork is created exclusively so another writer's file is never replaced.
  • Invalid launcher or artwork environment paths now disable shortcuts instead of ending the app on Select, and a pending Select press is cancelled when the search keyboard or settings opens.
  • A shortcut whose game is no longer in the catalog shows SHORTCUT GAME NOT FOUND in red instead of opening the library without explanation.

Written for commit 80ef3ee. Summary will update on new commits.

View guided diff

alexander-clawthorne and others added 2 commits October 4, 2026 10:54
Pressing Select on a library title writes a launcher for it next to
GreenOvercast.sh, so the game gets its own Ports entry. The shortcut
re-runs the port launcher with the title's product id and
GREENOVERCAST_AUTOSTART, and the library starts that title once when it
is in the catalog; otherwise the library opens as usual. After the
session the library is shown as for any other launch. Pressing Select
again removes the shortcut.

On muOS the cached artwork is also written to the Ports catalogue as a
PNG fitted inside 320x240, matching what PortMaster does for installed
ports. Other CFWs get the entry without artwork.

Only files carrying the shortcut marker are replaced or removed, so an
existing port with the same name is never touched. Select acts on
release, so the Select + Start exit chord never creates a shortcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readFileAlloc sizes its buffer with File.stat(), which uses statx and
fails on the 4.9 kernels H700 devices ship, so shortcuts were saved
without artwork on muOS. Read the cached image with readToEndAlloc, as
the settings store does, and log which step fails if a cover cannot be
produced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 1f305c47-5771-4d19-b0f8-90fcae38d521

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply to a comment to ask cubic a question or push back. It learns from your replies.

Re-trigger cubic

Comment thread src/ui/port_shortcut.zig Outdated
Comment thread src/ui/port_shortcut.zig Outdated
Comment thread src/ui/artwork_loader.zig
Comment thread src/ui/artwork_loader.zig
Comment thread src/ui/port_shortcut.zig Outdated
Comment thread packaging/portmaster/greenovercast/GreenOvercast.sh Outdated
alexander-clawthorne and others added 2 commits October 4, 2026 17:08
- Treat a generated script as this title's shortcut only when it launches
  the same product id. Different titles can share a port name, and
  Select on one must not remove the other's shortcut.
- Write through std.fs.AtomicFile, which creates a randomly named
  temporary file exclusively, instead of truncating <name>.sh.tmp in the
  shared Ports folder.
- Remove the artwork first and best-effort, then the script, so a
  reported failure always matches what is left on disk.
- Stay quiet when a title's artwork is simply not cached yet, and note
  why cover generation runs on the caller's thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the launcher from BASH_SOURCE so the path stays right if a
wrapper sources it, and log that game shortcuts are disabled when the
path cannot be resolved instead of dropping them silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/ui/port_shortcut.zig Outdated
writeScript accepted launcher paths of any length, while state() only
reads the first 1024 bytes, so an unusually long Ports path could
produce a shortcut that could never be recognised, removed or replaced.
Cap the launcher path at 512 bytes and assert at compile time that the
longest possible script fits in the read buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@Producdevity Producdevity left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 69517dd against feat/geforce-now (455f4b5).

Requesting changes for the shared muOS artwork ownership bug: creating/removing a shortcut can overwrite/delete another port’s PNG. The no-cover deletion case is reproduced against the production module. There is also a smaller relative-launch path bug; both have concrete reproductions and requested fixes inline.

The earlier script-collision, temporary-file, shell-quoting, and maximum-script-length fixes hold up. A generated-script execution test preserved literal shell metacharacters and the exact launcher/product arguments without executing substitutions. The Xbox/GFN selection and input paths passed the focused checks.

I accepted and resolved the synchronous-artwork concern separately. Its bounded, add-only muOS path does not justify forcing a general worker redesign here.

Validation on this commit:

  • tools/zig.sh build test: 318/318 tests passed, 99/99 steps succeeded.

  • tools/zig.sh build fmt-check, git diff --check, and bash -n: passed. ShellCheck reports only the same two existing PortMaster-source/context warnings as the base.

  • 15 additional headless checks of the actual UI/controller code passed: both providers, exact ID matching, one-shot/missing-ID autostart, Select-release toggling, both exit-chord orders, and inactive-controller events.

  • Four filesystem/shell probes confirmed the two artwork failure cases, same-folder foreign-port protection, and exact safe script execution. The failure-case probes assert the observed bug; they are not regression tests of desired behavior.

  • Two supplemental artwork allocation/stride tests passed, and the generated PNG decoded independently with Python/zlib and FFmpeg.

  • tools/zig.sh build release: passed (ARM64 ReleaseSafe). The output is an AArch64 ELF executable, with 0 undefined __aarch64_* symbols.

No physical handheld or live cloud session was exercised in this review. CodeRabbit’s GitHub status is a skipped review; its CLI could not authenticate here (environment_unsupported), so these conclusions come from the independent source review and tests.

Comment thread src/ui/port_shortcut.zig Outdated
Comment on lines +138 to +141
if (artwork) |directory| {
var cover_buffer: [name_capacity + 8]u8 = undefined;
const cover_name = try std.fmt.bufPrint(&cover_buffer, "{s}.png", .{name});
directory.deleteFile(cover_name) catch |err| switch (err) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Preserve artwork that this shortcut does not own.

The script check only covers this Ports directory, but muOS uses a shared External - Ports/box catalogue. For example, a native Celeste.sh on one card can coexist with GreenOvercast’s shortcut on another card while both use Celeste.png.

I reproduced this against the unchanged module: seed an existing PNG, call add(..., "Celeste", null) after a cache miss, then remove the shortcut. The original PNG survives creation but is deleted here, even though this shortcut never wrote it. With a non-null cover, add() also overwrites the existing PNG at lines 124–129.

Please preserve pre-existing artwork and record whether/which image the shortcut owns, then replace or delete only that owned image. Skipping an existing PNG during creation alone is insufficient because removal still deletes it. Add a regression for an existing PNG plus cover_png == null, and retain the existing same-directory foreign-script protections.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, reproduced and fixed in d4842b5.

A shortcut now records ownership in its own script (# GreenOvercast artwork: crc32=… size=…), and only when it created the image. add() writes artwork only if no <Name>.png exists, or if the existing one is still exactly the image this shortcut recorded; otherwise it keeps it and logs "Existing artwork kept". remove() deletes the image only when the record exists and the file still matches its CRC-32 and size, so artwork created or replaced by anything else, such as a native port installed later, is kept. Re-adding without a cached cover carries an existing matching record forward.

Regressions, with your scenario first: existing artwork survives a shortcut added without a cover (existing PNG, cover_png == null, add then remove), existing artwork is neither overwritten nor removed by a shortcut with a cover, a shortcut replaces and removes only the artwork it wrote, and artwork replaced after the shortcut saved it is kept. The same-directory foreign-script tests are unchanged. Your reproduction now keeps native-port-art in both the null and non-null cover cases.

On the RG35XX-H, add → remove → add created the cover, recorded crc32=9688531c size=104952 (matching the file), deleted it on removal and recreated it.

export GREENOVERCAST_ARTWORK_CACHE_DIR="$artwork_cache_dir"

# Game shortcuts are written next to this launcher and run it again with a title id.
if port_launcher=$(realpath "${BASH_SOURCE[0]:-$0}"); then

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Resolve the launcher path before changing directories.

cd "$GAMEDIR" has already run at line 30. When started from the Ports folder with bash ./GreenOvercast.sh, BASH_SOURCE[0] remains relative, so this resolves to .../ports/greenovercast/GreenOvercast.sh instead of .../ports/GreenOvercast.sh. GNU realpath succeeds when only the final component is missing, so support stays enabled and shortcuts are written inside the data directory with a nonexistent target.

I reproduced this with the unmodified launcher and a harmless mock PortMaster/application environment: absolute invocation exported an existing launcher; relative invocation exported the nonexistent path above. Normal muOS frontend invocation uses an absolute path, so this is specifically a relative/manual-launch bug.

Please capture the absolute launcher path before sourcing helpers or changing directories, verify it is an existing file, and export that saved value here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduced and fixed in 5c17dc0. The launcher path is now resolved at the top of the script, before PortMaster's helpers are sourced or cd "$GAMEDIR" runs, and exported only when it names an existing file; otherwise it logs "Game shortcuts disabled". I renamed the variable to greenovercast_launcher, since it's now set before control.txt is sourced.

With a mock PortMaster environment, absolute, relative (bash ./GreenOvercast.sh from the Ports folder) and sourced-wrapper starts all export the existing …/ports/GreenOvercast.sh. ShellCheck output is unchanged from the base (the same SC1090/SC2154/SC1091 PortMaster-source notices).

alexander-clawthorne and others added 2 commits October 10, 2026 13:16
muOS keeps every port's box art in one shared catalogue, also across both
cards, so a same-named PNG can belong to a native port. add() overwrote
it when a cover was cached, and remove() deleted it either way.

A shortcut now writes its artwork only when no image exists yet, and
records the CRC-32 and size of what it wrote in its own script. remove()
deletes the image only when that record exists and still matches the
file, so artwork created or replaced by anything else is kept. Re-adding
a shortcut replaces only its own unchanged image and keeps the record
when no new cover is available. The image is read rather than stat'ed,
since statx is unavailable on 4.9 kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shortcut launcher path was resolved after `cd "$GAMEDIR"`, so a
relative start such as `bash ./GreenOvercast.sh` exported a nonexistent
path inside the data directory. Resolve it before PortMaster's helpers
run or the directory changes, and export it only when it names an
existing file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Re-trigger cubic

Comment thread src/ui/port_shortcut.zig Outdated
Comment thread src/ui/port_shortcut.zig Outdated
alexander-clawthorne and others added 6 commits October 10, 2026 13:37
add() wrote the artwork before building and saving the script, so a
failed script save left the previous script recording the old image
while a new one was on disk, orphaning it. Build and validate the script
first, and if saving it fails, delete artwork that was just created or
restore the image the shortcut owned from the bytes read when checking
ownership. A failed artwork write saves the script with the previous
record, so an image that is not on disk is never recorded.

New artwork is created exclusively, so a file another writer creates
after the existence check is kept instead of replaced. Exclusive creation
is used rather than a no-replace rename, which FUSE filesystems such as
the muOS catalogue's do not reliably support. remove() still verifies and
deletes in separate steps, which POSIX cannot make atomic; that is
documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
openDirAbsolute() asserts that its path is absolute, so a relative or empty
GREENOVERCAST_PORT_LAUNCHER or GREENOVERCAST_PORT_ARTWORK_DIR ended the app on
Select in ReleaseSafe builds. Accept only non-empty absolute paths and treat
anything else as shortcuts being unavailable. Autostart now requires
GREENOVERCAST_AUTOSTART=1 rather than any value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search keyboard and settings consume Select's release, so a press made
before opening them could toggle a shortcut on a later release back in the
library. Clear the pending press before either view opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… red

A shortcut whose game is no longer in the catalog opened the library without
explanation. Show SHORTCUT GAME NOT FOUND instead. Notices now carry a kind,
and problems (game not found, save or removal failures, name in use, missing
folders) draw in a new danger() red so they stand out from ADDED and REMOVED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remove() deleted the script, the only record of the artwork it owns, even when
deleting that artwork failed, leaving an image nothing could ever remove. Keep
the script in that case, and when a configured artwork folder cannot be opened,
so a later attempt can clean up. A missing artwork folder still means the
system has none, so shortcuts there remain removable.

writeScript() also enforces name_capacity and rejects ':' in names, so a name
can never imitate the artwork record line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rollback tests only checked the end state, so they also passed if the
artwork was never written. A test-only hook, compiled out of other builds, now
records the artwork between its write and the script save, proving each
rollback actually undid something. skipAsRoot() uses libc geteuid() off Linux,
since host tests also run on macOS. add() documents that it also handles
re-adding, which the UI does not do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants