From 068f37faedd12a335eb980626e315dd263f1063b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 01:28:59 +0000 Subject: [PATCH 1/2] docs: complete reference for pak architecture, capabilities and limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PAKS.md was inherited from upstream as an emulator-pak authoring tutorial. It never documented the execution model, the environment paks run in, tool paks, or what paks can and cannot reach — the questions authors actually hit first. Expanded it into a full reference: - the exit-and-handoff launch model, which explains nearly every limitation (the launcher terminates before a pak starts; /tmp/next is consumed and removed by the boot loop, so paks cannot chain-launch through it) - complete environment variable table, PATH/LD_LIBRARY_PATH, CPU governor, and the daemons that stay resident across launches - tool paks: example, visibility rules (dot-prefix, .disabled, map.txt), and renaming via map.txt - emulator pak resolution order, documenting that a user pak in /Emus// overrides a built-in of the same tag - capabilities: helper binaries, settings via minuisettings.txt/nextval.elf, drop-in assets, and the hardcoded Settings/Pak Store/FN integrations - an explicit "what paks cannot do" section - lifecycle: the .system-wiped/.userdata-persists asymmetry and the orphaned-file problem that follows from it - debugging and platform/pixel-format caveats HOOKS.md corrections against run_hooks.sh: - "scripts run in alphabetical order" was misleading; they are *started* in glob order but run concurrently unless named *.sync.sh - documented HOOK_CATEGORY, which run_hooks.sh exports but the doc omitted - HOOK_PHASE does not identify the event: pre-launch.d and pre-sleep.d both report "pre", so HOOK_CATEGORY is the only disambiguator - the HOOK_CMD/TYPE/EMU_PATH/ROM_PATH/LAST variables are set only for the launch phases, not for boot.d or the sleep phases - the runner waits on background hooks, so backgrounding does not make a slow hook free - sleep hooks do not fire on desktop, which has no bin/suspend - pre-sleep.d runs with network up, post-resume.d runs before it returns Also added hook installation guidance, since hooks live in .userdata/ and survive both updates and pak deletion. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Hu2BibaMPDtFk7S3NYywZE --- HOOKS.md | 182 ++++++++++++++++---- PAKS.md | 508 +++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 583 insertions(+), 107 deletions(-) diff --git a/HOOKS.md b/HOOKS.md index bd6583c83..10ab9fded 100644 --- a/HOOKS.md +++ b/HOOKS.md @@ -1,76 +1,145 @@ -# About NextUI hooks +# NextUI hooks + +Hooks are the only mechanism that runs pak-supplied code *outside* of a pak launch. A hook is a shell script the OS executes at a fixed point in its lifecycle — at boot, around every ROM and pak launch, and around suspend and resume. + +If you are writing a pak, read **[PAKS.md](PAKS.md)** first; hooks are an extension of that model, not a replacement for it. ## The idea -Hooks are platform-specific, just like paks. The launcher reads them from: +Hooks are platform-specific, just like paks. The launcher reads them from `$USERDATA_PATH/.hooks/`: ``` $USERDATA_PATH/.hooks/ - boot.d/ # scripts run on boot - pre-launch.d/ # scripts run before launch - post-launch.d/ # scripts run after launch exits - pre-sleep.d/ # scripts run before device goes to sleep - post-resume.d/ # scripts run after device wakes from sleep + boot.d/ # after auto.sh, before the launcher starts + pre-launch.d/ # before a ROM or pak launches + post-launch.d/ # after it exits + pre-sleep.d/ # before the device suspends + post-resume.d/ # after it wakes ``` -On device, `USERDATA_PATH` resolves to: +On device, `USERDATA_PATH` resolves to `/mnt/SDCARD/.userdata/$PLATFORM`, so the real paths are: ``` -/mnt/SDCARD/.userdata/$PLATFORM +/mnt/SDCARD/.userdata/tg5040/.hooks/post-launch.d/shortcuts-resume.sh ``` -So the actual hook directories on device are: +If a directory doesn't exist, nothing happens and there is no overhead. The directories are not created for you — `mkdir -p` them at install time. -``` -/mnt/SDCARD/.userdata//.hooks/pre-launch.d/ -/mnt/SDCARD/.userdata//.hooks/post-launch.d/ +Note that hooks live in `.userdata/`, **not** inside your pak. See [Installing and removing hooks](#installing-and-removing-hooks) for what that implies. -``` +## Phases -Example installed hook path: +| Directory | Runs | Fired by | +|---|---|---| +| `boot.d` | once at boot, after `auto.sh`, before the launcher's first start | `MinUI.pak/launch.sh` | +| `pre-launch.d` | before every ROM **and** pak launch | `MinUI.pak/launch.sh` | +| `post-launch.d` | after the ROM or pak exits, before the launcher restarts | `MinUI.pak/launch.sh` | +| `pre-sleep.d` | before suspend, while wifi and bluetooth are **still up** | `bin/suspend` | +| `post-resume.d` | immediately after wake, before wifi and bluetooth are **restarted** | `bin/suspend` | -``` -/mnt/SDCARD/.userdata/tg5040/.hooks/post-launch.d/shortcuts-resume.sh +Two things worth planning around: -``` +- **`boot.d` is where a resident daemon belongs.** It runs before the launcher exists and its children outlive it, so a daemon started here lives for the whole session — the closest thing to a persistent background service a pak can get. +- **Sleep hooks have no network.** `pre-sleep.d` runs before wifi and bluetooth are stopped, so it can still reach the network. `post-resume.d` runs before they are restarted, so it cannot. Anything needing connectivity after wake must poll or retry. -If these directories don't exist, nothing happens and there is no overhead. +### Platform availability + +`boot.d`, `pre-launch.d` and `post-launch.d` exist on every platform. `pre-sleep.d` and `post-resume.d` are fired by `bin/suspend`, which exists on `tg5040` and `tg5050` but **not** on `desktop` — sleep hooks never fire on the desktop build. ## Environment variables -Hook scripts inherit all standard NextUI environment variables (`SDCARD_PATH`, `PLATFORM`, `USERDATA_PATH`, `SHARED_USERDATA_PATH`, etc.) plus these launch-specific ones: +Hook scripts inherit all standard NextUI environment variables (`SDCARD_PATH`, `PLATFORM`, `USERDATA_PATH`, `SHARED_USERDATA_PATH`, `LOGS_PATH`, …). See the environment table in [PAKS.md](PAKS.md#environment). + +`run_hooks.sh` additionally exports: + +| Variable | Value | Set in | +|---|---|---| +| `HOOK_CATEGORY` | the directory name, e.g. `pre-launch.d` | every phase | +| `HOOK_PHASE` | `pre`, `post`, or `boot` | every phase | + +And for launch phases only, the boot script exports: | Variable | Description | |---|---| -| `HOOK_PHASE` | `pre` or `post` | | `HOOK_TYPE` | `rom` or `pak` | -| `HOOK_CMD` | The raw launch command | -| `HOOK_EMU_PATH` | Path to the emulator or pak `launch.sh` | -| `HOOK_ROM_PATH` | Path to the ROM file (empty for pak launches) | -| `HOOK_LAST` | Contents of `/tmp/last.txt` (the last selected menu entry) | +| `HOOK_CMD` | the raw launch command | +| `HOOK_EMU_PATH` | path to the emulator or pak `launch.sh` | +| `HOOK_ROM_PATH` | path to the ROM (empty for pak launches) | +| `HOOK_LAST` | contents of `/tmp/last.txt`, the last selected menu entry | -These can then be used by the underlying Pak to ingest information about the hook that just occurred. +> **`HOOK_PHASE` does not identify the event.** It is derived from the directory name prefix, so `pre-launch.d` and `pre-sleep.d` both report `pre`, and `post-launch.d` and `post-resume.d` both report `post`. Use `HOOK_CATEGORY` when you need to know what actually happened. + +> **The `HOOK_*` launch variables are only set for `pre-launch.d` and `post-launch.d`.** They are populated by `parse_hook_cmd()` in the boot loop, which only runs on the launch path. In `boot.d`, `pre-sleep.d` and `post-resume.d` they are unset — guard with `${HOOK_TYPE:-}` if a script is shared across phases. ## Writing a hook script -A hook script is any executable `.sh` file in one of the hook directories. Scripts run in alphabetical order. +A hook is a `.sh` file in one of the hook directories. It is invoked directly, so give it a shebang. ```sh #!/bin/sh # my-hook.sh — log every ROM launch -[ "$HOOK_TYPE" = "rom" ] || exit 0 +[ "${HOOK_TYPE:-}" = "rom" ] || exit 0 echo "$(date): launched $HOOK_ROM_PATH" >> "$LOGS_PATH/launches.log" ``` -## Rules +### Execution model + +`run_hooks.sh` iterates the directory's `*.sh` files in glob order (alphabetical) and, for each: + +- a file ending in **`.sync.sh`** runs **synchronously** — the runner blocks until it exits +- every other file is started in a **background subshell** + +After starting everything, the runner `wait`s for all background scripts before returning. + +Two consequences that are easy to get wrong: + +- **Scripts are *started* in alphabetical order, but background scripts *run concurrently*.** Filenames do not give you ordering. If script B depends on script A having finished, both must be `.sync.sh`, and A must sort first. +- **Backgrounding does not make a slow hook free.** The `wait` means every hook, background or not, delays whatever comes next — the launch, the return to the menu, or the boot. Slow work must be detached explicitly: + +```sh +#!/bin/sh +# fire-and-forget: survives the runner's wait +( sleep 30; do_slow_thing ) /dev/null 2>&1 & +``` + +`pre-sleep.d` is invoked with `--sync-only`, which forces *every* script in it to run synchronously regardless of name — suspend must not race with a half-finished hook. + +### Rules + +- Each script runs in a subshell. A crash or non-zero exit cannot affect the launcher, the suspend sequence, or other hooks. +- **Output is discarded.** `run_hooks.sh` redirects stdout and stderr to `/dev/null`. If you need logging, write to your own file under `$LOGS_PATH`. +- **Pre-launch hooks cannot cancel a launch.** They are for observation and setup only; the return value is ignored. +- Keep hooks fast, for the reason above. +- Unlike `auto.sh`, hooks are composable — every pak manages its own script. Use a descriptive, namespaced filename to avoid collisions. + +## Installing and removing hooks + +Hooks live in `.userdata/`, outside your pak. This has a consequence worth designing for: **`.userdata/` survives both NextUI updates and pak deletion.** A hook you install keeps running after the user deletes the pak that installed it, and nothing runs on uninstall to clean it up. + +So: + +**1. Namespace the filename.** `myapp-resume.sh`, never `resume.sh`. Collisions are silent. + +**2. Make the hook self-checking**, so an orphan exits cleanly instead of failing on every launch forever: + +```sh +#!/bin/sh +# myapp-sync.sh +MYAPP="$SDCARD_PATH/Tools/$PLATFORM/MyApp.pak" +[ -x "$MYAPP/myapp" ] || exit 0 # pak is gone; nothing to do +"$MYAPP/myapp" --sync >> "$LOGS_PATH/myapp-sync.txt" 2>&1 +``` + +**3. Install idempotently** from your pak's `launch.sh`, since it may run many times: -- Each script runs in a subshell. A crash or non-zero exit will not affect the launcher or other hooks. -- Script output (stdout/stderr) is suppressed. If you need logging, write to your own log file. -- Pre-launch hooks cannot cancel the launch. They are for observation and setup only. -- Keep hooks fast. A slow hook delays the launch or the return to the menu. -- Unlike auto.sh, each pak should manage their own hook and use a descriptive filename to avoid collisions. +```sh +HOOK_DIR="$USERDATA_PATH/.hooks/post-launch.d" +mkdir -p "$HOOK_DIR" +cp -f "$(dirname "$0")/hooks/myapp-sync.sh" "$HOOK_DIR/myapp-sync.sh" +``` +**4. Offer a way to remove it.** A "Disable" entry in your pak that deletes its own hook is the only uninstall path a user has that doesn't involve a file manager. ## Example: sync after ROM exit @@ -78,10 +147,53 @@ echo "$(date): launched $HOOK_ROM_PATH" >> "$LOGS_PATH/launches.log" #!/bin/sh # shortcuts-resume.sh — one-shot resume metadata sync after a ROM exits -[ "$HOOK_TYPE" = "rom" ] || exit 0 +[ "${HOOK_TYPE:-}" = "rom" ] || exit 0 SHORTCUTS_PAK="$SDCARD_PATH/Tools/$PLATFORM/Shortcuts.pak" [ -x "$SHORTCUTS_PAK/shortcuts" ] || exit 0 "$SHORTCUTS_PAK/shortcuts" --resume-sync-hook >> "$LOGS_PATH/shortcuts-resume-sync.txt" 2>&1 ``` + +## Example: a boot daemon + +```sh +#!/bin/sh +# myapp-daemon.sh — start a background service for the session + +MYAPP="$SDCARD_PATH/Tools/$PLATFORM/MyApp.pak" +[ -x "$MYAPP/myappd" ] || exit 0 +pgrep -f myappd >/dev/null && exit 0 # already running + +"$MYAPP/myappd" >> "$LOGS_PATH/myappd.txt" 2>&1 & +``` + +Started from `boot.d`, this outlives the runner and every launcher restart. It still cannot draw to the screen or intercept menu input — see [What paks cannot do](PAKS.md#what-paks-cannot-do). + +## Debugging hooks + +Hook output goes to `/dev/null`, so a broken hook is silent. To see what happened, log explicitly: + +```sh +exec >> "$LOGS_PATH/myhook.txt" 2>&1 +set -x +``` + +To verify a hook fires at all, have it touch a file and check the timestamp: + +```sh +date >> "$LOGS_PATH/myhook-fired.txt" +``` + +You can also run the phase by hand from a shell with the NextUI environment loaded: + +```sh +"$SYSTEM_PATH/bin/run_hooks.sh" post-launch.d +``` + +## See also + +- **[PAKS.md](PAKS.md)** — what paks are, how they run, and what they can and cannot do +- `skeleton/SYSTEM//bin/run_hooks.sh` — the runner itself, 36 lines +- `skeleton/SYSTEM//paks/MinUI.pak/launch.sh` — the boot loop that fires launch hooks +- `skeleton/SYSTEM//bin/suspend` — the sleep sequence that fires sleep hooks diff --git a/PAKS.md b/PAKS.md index d9d629d1f..6e4d3d770 100644 --- a/PAKS.md +++ b/PAKS.md @@ -1,107 +1,471 @@ -# About NextUI paks +# NextUI paks + +A pak is a folder with a `.pak` extension containing a shell script named `launch.sh`. That is the entire contract. NextUI launches the script; everything else is up to you. + +There are two kinds: + +| Kind | Lives in | Purpose | +|---|---|---| +| **Emulator pak** | `/Emus//` | Runs a game. Matched to ROMs by folder tag. | +| **Tool pak** | `/Tools//` | Anything else. Appears in the Tools menu, launched directly. | + +Both are just folders with a `launch.sh`. The only thing that makes one an emulator and the other a tool is which directory it sits in. + +**Contents** + +- [Where paks live](#where-paks-live) +- [How a pak runs](#how-a-pak-runs) — read this first +- [Environment](#environment) +- [Tool paks](#tool-paks) +- [Emulator paks](#emulator-paks) +- [What paks can do](#what-paks-can-do) +- [What paks cannot do](#what-paks-cannot-do) +- [Installing, updating, uninstalling](#installing-updating-uninstalling) +- [Debugging](#debugging) + +--- + +## Where paks live + +``` +/mnt/SDCARD/ +├── Emus//.pak/ user emulator paks +├── Tools//.pak/ user tool paks +├── Roms/ ()/ games, tagged by folder name +├── Bios// +├── Saves// +├── Cheats// +├── Collections/.txt plain text game lists +├── Shaders/ Overlays/ Palettes/ drop-in assets +├── .system// REPLACED ON EVERY UPDATE +│ ├── bin/ nextui.elf, minarch.elf, helpers +│ ├── cores/ bundled libretro cores +│ ├── lib/ +│ └── paks/ built-in paks (MinUI.pak, Emus/) +└── .userdata/ + ├── / survives updates + │ ├── .hooks/ see HOOKS.md + │ ├── logs/ + │ └── auto.sh legacy boot script + └── shared/ + ├── minuisettings.txt all NextUI settings + └── .minui/ recents, resume state +``` + +Never install a pak into `.system/`. That directory is deleted and recreated by every NextUI update. + +### Platform folders + +Paks are platform-specific. The platform folder name matches the `PLATFORM` envar and is always lowercase. + +| Platform | Devices | `DEVICE` values | +|---|---|---| +| `tg5040` | Trimui Brick, Smart Pro, Brick Pro | `brick`, `smartpro`, `brickpro` | +| `tg5050` | Trimui Smart Pro S | `smartpros` | +| `desktop` | local development build | *(unset)* | + +`DEVICE` distinguishes hardware variants within one platform. A pak can use or ignore it. Do not assume it is set — on `tg5050` it is only exported when the model string matches, so treat an empty `DEVICE` as "the platform's default device". + +```sh +case "$DEVICE" in + brick|brickpro) CFG="tg3040.cfg" ;; + *) CFG="tg5040.cfg" ;; +esac +``` + +--- + +## How a pak runs + +This is the single most important thing to understand, and it explains nearly every limitation further down. + +**NextUI's launcher exits before your pak starts.** -A pak is just a folder with a ".pak" extension that contains a shell script named "launch.sh". +The real init process is the shell loop in `.system//paks/MinUI.pak/launch.sh`: -There are two kinds of paks, emulators and tools. Emulator paks live in the Emus folder. Tool paks live in the Tools folder. These two folders live at the root of your SD card. Extra paks should never be added to the hidden ".system" folder at the root of the SD card. This folder is deleted and replaced every time a user updates NextUI. +```sh +while [ -f $EXEC_PATH ]; do + nextui.elf &> $LOGS_PATH/nextui.txt # the launcher UI runs here, then EXITS -Paks are platform specific. Inside the Emus and Tools folders you will find (or need to create) platform folders. Some platform folders are named after the target device (eg. "rgb30" for the Powkiddy RGB30), others use the device's internal name (eg. "tg5040" for the Trimui Smart Pro), other use an arbitrary shortname (eg. "trimui" for the Trimui Model S), all are completely lowercase. See the extras bundle for up-to-date supported platform folder names. + if [ -f $NEXT_PATH ]; then # /tmp/next + CMD=`cat $NEXT_PATH` + parse_hook_cmd "$CMD" + run_hooks.sh pre-launch.d + eval $CMD # ← your pak runs here + run_hooks.sh post-launch.d + rm -f $NEXT_PATH + fi +done +``` -Some platforms have multiple devices with unique qualities. NextUI differentiates these devices from the base platform with the `DEVICE` envar. eg. the "rg35xxplus" platform has two unique devices "cube" for the RG CubeXX, and "wide" for the RG34xx. It also supports "hdmi" for when outputting to HDMI. A pak can choose to use or ignore this envar. +When you select a pak, `nextui.elf` writes the command to `/tmp/next`, sets its quit flag, and terminates. The shell loop picks the command up, runs it, and re-execs `nextui.elf` when it returns. -# The types of emulator pak +### What follows from this -There are three basic types of emulator paks, which you chose depends on your goals and your desired level of NextUI integration. +- **Your pak owns the device.** Full screen, full input, no launcher competing for either. +- **The launcher is not running.** You cannot call into it, draw over it, or extend it. It is a dead process. +- **Exiting your pak returns to the launcher.** Just return from `launch.sh`. +- **You cannot chain-launch through `/tmp/next`.** The loop does `rm -f $NEXT_PATH` *after* `eval $CMD` returns, so anything you write there is deleted before the launcher restarts. To launch a game from a pak, exec the emulator directly: + ```sh + "$SDCARD_PATH/Emus/$PLATFORM/GB.pak/launch.sh" "$ROMS_PATH/Game Boy (GB)/game.gb" + ``` +- **Startup and teardown are not free.** Every pak launch is a full launcher shutdown and cold restart — a visible black screen for a second or more on each side. +- **Nothing sandboxes you.** Paks run as root with unrestricted filesystem access. The stock `Remove Loading.pak` rewrites `/etc/init.d/runtrimui` with `sed -i`. You can do real and permanent damage; be careful, and be conservative with anything outside the SD card. -The first type reuses a libretro core included with a base NextUI install. This takes advantage of a known working core but allows customizing the default options and separating user configs. An example of this type is the extra GG.pak which uses the default picodrive core. +### What stays running underneath -The second type includes its own libretro core. This allows you to support completely new systems while still taking advantage of NextUI's standard features like resume from menu, quicksave and auto-resume, and consistent in-game menus, behaviors, and options. An example of this type is the extra MGBA.pak which bundles its own mgba core. +Daemons started at boot survive across pak launches: -The third type launches a bundled standalone emulator. This may allow you to squeeze more performance out of a piece of hardware than a libretro core could. The downside of this type is no integration with NextUI. No resume from menu, no quicksave and auto-resume, no consistent in-game menus, behaviors, or options. In some cases the MENU (and if available, POWER) button may not function as expected, if at all. This type of pak should be a last resort. An example of this type is the community developed NDS.pak which is available for a handful of platforms NextUI supports. +| Process | Responsibility | +|---|---| +| `keymon.elf` | global button handling — MENU/POWER combos, brightness, volume | +| `batmon.elf` | battery monitoring | +| `audiomon.elf` | audio device hotplug | +| `trimui_inputd` | stock GPIO input daemon | -In all cases please make clear to your users that I (@shauninman) can't support third-party paks. If I've excluded a console or core from NextUI's base or extra bundles it's usually for good reason, either the core's integration wasn't up to snuff (eg. arcade cores expect roms to have specific, arcane file names with only certain rom sets working with certain cores), has too many bugs (eg. unable to reliably resume from a save state), has poor performance on a given device, or is just a console I have no familiarity with or interest in. +`keymon.elf` is why global shortcuts sometimes half-work inside a standalone binary, and why they sometimes don't — it reacts to key events, but it cannot make an application that ignores those events behave. -# Naming your emulator pak +--- -NextUI maps roms to paks based on the tag in parentheses at the end the name of the rom's parent folder (eg. "/Roms/Game Boy (GB)/Dr. Mario (World).gb" will launch the "GB.pak"). A tag should be all uppercase. When choosing a tag, start with common abbreviations used by other emulation frontends like Retroarch or EmulationStation (eg. FC for Famicom/Nintendo or MD for MegaDrive/Genesis). If that tag is already being used by another pak, use the core name if short (eg. MGBA) or an abbreviation (eg. PKM for pokemini) or truncation (eg. SUPA for mednafen_supafaust) of the core name. +## Environment -# Launching your core +`launch.sh` is invoked from the boot script, so it inherits everything that script exported. -Here's an example "launch.sh": +### Paths - #!/bin/sh - - EMU_EXE=picodrive - - ############################### - - EMU_TAG=$(basename "$(dirname "$0")" .pak) - ROM="$1" - mkdir -p "$BIOS_PATH/$EMU_TAG" - mkdir -p "$SAVES_PATH/$EMU_TAG" - mkdir -p "$CHEATS_PATH/$EMU_TAG" - HOME="$USERDATA_PATH" - cd "$HOME" - minarch.elf "$CORES_PATH/${EMU_EXE}_libretro.so" "$ROM" &> "$LOGS_PATH/$EMU_TAG.txt" +| Variable | Value on device | Notes | +|---|---|---| +| `PLATFORM` | `tg5040` | | +| `DEVICE` | `brick` / `smartpro` / `brickpro` / `smartpros` | may be unset | +| `SDCARD_PATH` | `/mnt/SDCARD` | | +| `ROMS_PATH` | `$SDCARD_PATH/Roms` | | +| `BIOS_PATH` | `$SDCARD_PATH/Bios` | | +| `SAVES_PATH` | `$SDCARD_PATH/Saves` | | +| `CHEATS_PATH` | `$SDCARD_PATH/Cheats` | | +| `SYSTEM_PATH` | `$SDCARD_PATH/.system/$PLATFORM` | wiped on update | +| `CORES_PATH` | `$SYSTEM_PATH/cores` | | +| `USERDATA_PATH` | `$SDCARD_PATH/.userdata/$PLATFORM` | survives updates | +| `SHARED_USERDATA_PATH` | `$SDCARD_PATH/.userdata/shared` | cross-platform | +| `LOGS_PATH` | `$USERDATA_PATH/logs` | | +| `HOOKS_PATH` | `$USERDATA_PATH/.hooks` | see [HOOKS.md](HOOKS.md) | +| `DATETIME_PATH` | `$SHARED_USERDATA_PATH/datetime.txt` | | +| `IS_NEXT` | `yes` | present on NextUI, absent on stock MinUI | +| `TRIMUI_MODEL` | e.g. `Trimui Brick` | Trimui platforms only | -This will open the requested rom using the "picodrive\_libretro.so" core included with the base NextUI install. To use a different core just change the value of `EMU_EXE` to another core name (minus the "_libretro.so"). If that core is bundled in your pak add the following after the `EMU_EXE` line: +`HOME` is set to `$USERDATA_PATH` on device platforms. It is **not** exported on `desktop`, so set it yourself if your pak depends on it. - CORES_PATH=$(dirname "$0") +Use `IS_NEXT` to detect NextUI when writing a pak that also targets stock MinUI: -There's no need to edit anything below the line of hash marks. The rest is boilerplate that will extract the pak's tag from its folder name, create corresponding bios and save folders, set the `HOME` envar to "/.userdata/[platform]/", launch the game, and log any output from minarch and the core to "/.userdata/[platform]/logs/[TAG].txt". +```sh +if [ "$IS_NEXT" = "yes" ]; then + # NextUI-only features +fi +``` -That's it! Feel free to experiement with cores from the stock firmware, other compatible devices, or building your own. +### Search paths -Oh, if you're creating a pak for Anbernic's RG*XX line you'll need to change the last part of the last line from ` &> "$LOGS_PATH/$EMU_TAG.txt"` to ` > "$LOGS_PATH/$EMU_TAG.txt" 2>&1` because its default shell is whack. +```sh +PATH=$SYSTEM_PATH/bin:/usr/trimui/bin:$PATH +LD_LIBRARY_PATH=$SYSTEM_PATH/lib:/usr/trimui/lib:$LD_LIBRARY_PATH +``` -# Option defaults and button bindings +Everything in `.system//bin` is callable by bare name — `minarch.elf`, `show2.elf`, `nextval.elf`, `syncsettings.elf`, `gametimectl.elf`, `governor.sh`, `run_hooks.sh`. -Copy your new pak and some roms to your SD card and launch a game. Press the MENU button and select Options. Configure the Frontend, Emulator, and Controls. NextUI standard practice is to only bind controls present on the physical controller of the original system (eg. no turbo buttons or core-specific features like palette or disk switching). Let the player dig into that if they want to, the same goes for Shortcuts. Finally select Save Changes > Save for Console. Then quit and pop your SD card back into your computer. +### CPU governor -Inside the hidden ".userdata" folder at the root of your SD card, you'll find platform folders, and inside your platform folder a "[TAG]-[core]" folder. Copy the "minarch.cfg" file found within to your pak folder and rename it "default.cfg". Open "default.cfg" and delete any options you didn't customize. Any option name prefixed with a "-" will be set and hidden. This is useful for disabling features that may not be available (eg. overclocking) or perform poorly (eg. upscaling) on a specific platform. Near the bottom of the file you will find the button bindings. Here's an example from the "MGBA.pak": +The boot loop sets the governor to `performance` immediately before running your pak, and again after it exits. If your pak is long-running and not performance-sensitive, be a good citizen: - bind Up = UP - bind Down = DOWN - bind Left = LEFT - bind Right = RIGHT - bind Select = SELECT - bind Start = START - bind A Button = A - bind B Button = B - bind A Turbo = NONE:X - bind B Turbo = NONE:Y - bind L Button = L1 - bind R Button = R1 - bind L Turbo = NONE:L2 - bind R Turbo = NONE:R2 - bind More Sun = NONE:L3 - bind Less Sun = NONE:R3 +```sh +sh "$SYSTEM_PATH/bin/governor.sh" "auto" +``` -Everything after `bind ` up to the `=` is the button label that will appear in the Controls menu. I usually normalize these labels (eg. "Up" instead of "D-pad up", "A Button" instead of just "A"). Everything after the `=` up to the optional `:` is the button mapping. Button mappings are all uppercase. Shoulder buttons and analog stick buttons always include the number, (eg. "L1" instead of just "L"). Use "NONE" if the button should not be bound by default. When customizing or removing a binding, the default core-defined button mapping should always be added after a ":". In the example above, I removed the default "More Sun" binding by changing: +--- - bind More Sun = L3 +## Tool paks -to +A tool pak is any pak in `/Tools//`. It appears in the Tools menu, named after its folder minus the `.pak` extension. - bind More Sun = NONE:L3 +Minimal example — `/Tools/tg5040/Hello.pak/launch.sh`: -# Brightness and Volume +```sh +#!/bin/sh -Some binaries insist on resetting brightness (eg. DinguxCommander on the 40xxH stock firmware) or volume (eg. ppssppSDL everywhere) on every launch. To keep this in sync with NextUI's global settings there's syncsettings.elf. It waits one second then restores NextUI's current brightness and volume settings. In most cases you can just launch it as a daemon before launching the binary: +cd "$(dirname "$0")" +./hello.elf &> "$LOGS_PATH/hello.txt" +``` - syncsettings.elf & - ./DinguxCommander +`cd "$(dirname "$0")"` first is the near-universal convention: it makes bundled binaries and assets addressable by relative path regardless of how the pak was invoked. -But if a binary takes more than one second to initialize you might need to just let it run in a loop the entire time the binary is running: +### Visibility - while :; do - syncsettings.elf - done & - LOOP_PID=$! - - ./PPSSPPSDL --pause-menu-exit "$ROM_PATH" - - kill $LOOP_PID +An entry is hidden from every menu when its name: -# Caveats +- starts with `.` +- ends with `.disabled` +- is exactly `map.txt` -NextUI currently only supports the RGB565 pixel format and does not implement the OpenGL libretro APIs. It may be possible to use the stock firmware's retroarch instead of NextUI's minarch to run certain cores but that is left as an exercise for the reader. \ No newline at end of file +The `.disabled` suffix is the standard self-uninstall mechanism. `Remove Loading.pak` does a one-shot job and then removes itself from the menu: + +```sh +mv "$DIR" "$DIR.disabled" +``` + +The Tools menu entry itself is hidden entirely when `/Tools//` does not exist, or when the user turns off **Show Tools** in Settings. + +### Renaming entries + +A `map.txt` in a listed directory renames entries for display. One `filenameDisplay Name` pair per line. This works in `Roms` subfolders and any browsed directory. + +--- + +## Emulator paks + +### Tags and matching + +NextUI maps a ROM to a pak using the tag in parentheses at the end of its parent folder name: + +``` +/Roms/Game Boy (GB)/Tetris.gb → GB.pak +``` + +Tags are uppercase. Choose one that other frontends already use (`FC`, `MD`, `SFC`). If it's taken, use the core name (`MGBA`), an abbreviation (`PKM` for pokemini), or a truncation (`SUPA` for mednafen_supafaust). + +Resolution order when launching, from `getEmuPath()`: + +1. `/Emus//.pak/launch.sh` — user pak +2. `.system//paks/Emus/.pak/launch.sh` — built-in + +**User paks win.** Dropping a `GB.pak` into `/Emus/tg5040/` overrides the built-in one without touching `.system/`. This is the supported way to customize a stock emulator. + +### The three types + +**1. Reuse a bundled core.** Known-good core, your own defaults and separate configs. Full NextUI integration. `GG.pak` in the extras bundle does this with the stock picodrive core. + +**2. Bundle your own core.** Supports new systems, keeps full integration — resume from menu, quicksave, auto-resume, consistent in-game menus and options. Add one line to point at your own core directory: + +```sh +CORES_PATH=$(dirname "$0") +``` + +`MGBA.pak` in the extras bundle does this. + +**3. Bundle a standalone emulator.** Sometimes squeezes out more performance than a libretro core. The cost is total loss of integration: no resume from menu, no quicksave or auto-resume, no consistent in-game menus, behaviors, or options, and MENU/POWER may misbehave or not work at all. Treat this as a last resort. + +### Boilerplate + +```sh +#!/bin/sh + +EMU_EXE=picodrive + +############################### + +EMU_TAG=$(basename "$(dirname "$0")" .pak) +ROM="$1" +mkdir -p "$BIOS_PATH/$EMU_TAG" +mkdir -p "$SAVES_PATH/$EMU_TAG" +mkdir -p "$CHEATS_PATH/$EMU_TAG" +HOME="$USERDATA_PATH" +cd "$HOME" +minarch.elf "$CORES_PATH/${EMU_EXE}_libretro.so" "$ROM" &> "$LOGS_PATH/$EMU_TAG.txt" +``` + +Change `EMU_EXE` to any core name (minus `_libretro.so`). Nothing below the hash marks needs editing: it derives the tag from the folder name, creates the matching bios/saves/cheats folders, sets `HOME`, launches the game, and logs to `$LOGS_PATH/.txt`. + +On Anbernic RG\*XX platforms, replace the trailing `&> "$LOGS_PATH/$EMU_TAG.txt"` with `> "$LOGS_PATH/$EMU_TAG.txt" 2>&1` — the default shell there doesn't support `&>`. + +### Option defaults and button bindings + +Copy the pak and some ROMs to a card, launch a game, press MENU → Options, and configure Frontend, Emulator, and Controls. Standard practice is to bind only buttons present on the original hardware — no turbo, no core-specific features like palette or disk switching. Then **Save Changes → Save for Console**. + +Back on your computer, find `.userdata//-/minarch.cfg`, copy it into the pak as `default.cfg`, and delete every option you didn't deliberately change. + +Two things worth knowing: + +- Prefixing an option name with `-` sets it *and hides it* from the user. Useful for disabling features that are unavailable or perform badly on a given platform. +- A `default-.cfg` (e.g. `default-brick.cfg`) takes precedence over `default.cfg` on that device. + +Bindings look like this: + +``` +bind Up = UP +bind A Button = A +bind A Turbo = NONE:X +bind L Button = L1 +bind More Sun = NONE:L3 +``` + +Everything between `bind ` and `=` is the label shown in the Controls menu — normalize these (`Up`, not `D-pad up`; `A Button`, not `A`). Everything after `=` up to an optional `:` is the mapping, uppercase, with shoulder and stick buttons numbered (`L1`, not `L`). Use `NONE` to leave a control unbound, and always preserve the core's original default after a `:` when you override or remove one. + +### Brightness and volume + +Some binaries reset brightness or volume on launch (DinguxCommander, ppssppSDL). `syncsettings.elf` waits one second and restores NextUI's current values. Usually launching it as a daemon first is enough: + +```sh +syncsettings.elf & +./DinguxCommander +``` + +If the binary takes longer than a second to initialize, loop it for the binary's lifetime: + +```sh +while :; do syncsettings.elf; done & +LOOP_PID=$! + +./PPSSPPSDL --pause-menu-exit "$ROM_PATH" + +kill $LOOP_PID +``` + +--- + +## What paks can do + +### Bundled helper binaries + +All on `PATH`: + +| Binary | Purpose | +|---|---| +| `minarch.elf` | the libretro frontend — full NextUI game integration | +| `show2.elf` | splash / progress UI. Simple, progress, and daemon modes; daemon mode accepts live updates over `/tmp/show2.fifo`. See `workspace/all/show2/README.md` | +| `syncsettings.elf` | restore NextUI brightness and volume | +| `nextval.elf` | read any NextUI setting as JSON — `nextval.elf wifi` → `{"wifi": 1}`; no args prints everything | +| `gametimectl.elf` | game time tracking | +| `governor.sh` | set the CPU governor (`auto`, `performance`) | +| `run_hooks.sh` | run a hook directory; see [HOOKS.md](HOOKS.md) | + +A progress UI for a long-running pak: + +```sh +show2.elf --mode=daemon --image="$SDCARD_PATH/.system/res/logo.png" --text="Working..." & +echo "PROGRESS:50" > /tmp/show2.fifo +echo "TEXT:Almost done" > /tmp/show2.fifo +killall show2.elf +``` + +### Reading and writing settings + +Every NextUI setting lives in one flat file, `$SHARED_USERDATA_PATH/minuisettings.txt`, as `key=value` lines. Read individual values with `nextval.elf`: + +```sh +wifion=$(nextval.elf wifi | sed -n 's/.*"wifi": \([0-9]*\).*/\1/p') +``` + +You can write the file directly, but the launcher only reads it at startup and rewrites it wholesale on any change — so edit it while the launcher is not running (which, inside a pak, it isn't), and expect concurrent writes from a running `Settings.pak` to clobber yours. + +### Hooks + +The only mechanism that runs pak-supplied code *outside* of a pak launch. Scripts dropped into `$USERDATA_PATH/.hooks/.d/` are executed by the OS at boot, around every ROM and pak launch, and around suspend and resume. + +A `boot.d` hook is how a pak gets a resident background daemon that lives alongside the launcher for the whole session. See **[HOOKS.md](HOOKS.md)** for the full contract. + +### Drop-in assets + +No code required — these are read from fixed locations at runtime: + +| Location | Contents | +|---|---| +| `/Shaders/` | GLSL shaders and `.cfg` presets | +| `/Overlays//` | per-system overlay images | +| `/Palettes/` | UI color palettes | +| `/Collections/.txt` | game lists — one SD-relative path per line; appear in the main menu | +| `.system/res/palettes/` | built-in palettes (wiped on update) | + +Collections are worth calling out: a plain text file is enough to add a curated, user-visible game list to the main menu, with no code at all. + +### Named integrations + +A small number of paks get special treatment by name, all in `Tools//`: + +- **`Settings.pak`** and **`Pak Store.pak`** are promoted into the quick menu's toggle row with their own icons. +- Any tool pak can be bound to **FN1**, **FN2**, or **HOME** through Settings, launching it from anywhere in the menu. + +These are hardcoded in the launcher. There is no registry for adding more. + +--- + +## What paks cannot do + +Because the launcher has exited before your pak starts, and because there is no plugin API: + +- **Add or modify anything in the launcher UI.** Menu rows, list entries, settings pages, quick menu items, and button hints are compiled into `nextui.elf` and built in-process at runtime. There is no data-driven registry for any of them. +- **Draw over or alongside the launcher.** There is one framebuffer and `nextui.elf` owns it while it runs. +- **Intercept input in the launcher.** Menu navigation is read directly by `nextui.elf` through SDL. `keymon.elf` sees global key events, but it cannot change how the launcher interprets what it reads. +- **Add a new launcher screen or view.** Screens are enum values in the launcher's own state machine. +- **Use launcher internals** — its entry list, sorting, deduplication, save-state and resume plumbing, theme colors, or fonts. None of it is exported. +- **Cancel a launch from a pre-launch hook.** Hooks observe and set up; they cannot veto. +- **Run concurrently with the launcher**, except as a daemon started from a `boot.d` hook — and such a daemon still cannot draw or intercept input. + +If a feature needs to live *inside* the menu — a context menu on a listed game, a new row in the quick menu, a search screen sharing resume state — it cannot be built as a pak. It requires patching `nextui.c` and rebuilding, with the maintenance cost that implies, since `.system/` is replaced by every update. + +--- + +## Installing, updating, uninstalling + +**Install:** copy the `.pak` folder to `/Emus//` or `/Tools//`. That's it — no registration, no manifest, no restart. + +**Update behavior:** this asymmetry causes most pak lifecycle bugs. + +| Path | On NextUI update | +|---|---| +| `.system/` | **deleted and replaced** | +| `/Emus/`, `/Tools/` | untouched | +| `.userdata/` | untouched | + +So a pak in `/Tools/` survives updates — but anything it installed into `.system/` does not, and anything it installed into `.userdata/` survives *even after the pak is deleted*. + +**Uninstall:** there is no uninstall hook, and nothing runs when a user deletes a pak folder. If your pak writes outside its own directory — hooks especially — those files are orphaned on removal and will keep running indefinitely. Two mitigations: + +1. Namespace every file you install (`myapp-resume.sh`, not `resume.sh`). +2. Make hooks self-checking, so an orphan exits immediately instead of failing loudly: + +```sh +#!/bin/sh +# myapp-sync.sh +MYAPP="$SDCARD_PATH/Tools/$PLATFORM/MyApp.pak" +[ -x "$MYAPP/myapp" ] || exit 0 # pak was removed; nothing to do +"$MYAPP/myapp" --sync +``` + +Prefer `.disabled` over deletion for anything reversible — it hides the pak from every menu while leaving it in place. + +--- + +## Debugging + +Log to `$LOGS_PATH`, which is the first place to look after anything goes wrong: + +```sh +./mybinary.elf &> "$LOGS_PATH/mypak.txt" +``` + +The launcher's own log is `$LOGS_PATH/nextui.txt`, rewritten on every launcher start — so it holds output from the most recent launcher session only, not from before your pak ran. + +Note that hook output is discarded entirely (`run_hooks.sh` redirects to `/dev/null`), so a hook must log to its own file or it logs nothing. + +`gdbserver` is available at `.system//dbg/`: + +```sh +"$SYSTEM_PATH/dbg/launch.sh" :1234 ./mybinary.elf +``` + +For iteration without hardware, the `desktop` platform builds and runs the same launcher and paks locally. + +--- + +## Caveats + +- NextUI only supports the **RGB565** pixel format and does not implement the OpenGL libretro APIs. Cores requiring either will not work under `minarch.elf`. Using the stock firmware's retroarch instead is possible but unsupported. +- Third-party paks are not supported by the NextUI project. When a console or core is absent from the base or extras bundles it is usually deliberate — poor integration, unreliable save states, weak performance on the target hardware, or arcane ROM set requirements. Make this clear to your users. +- Paths in the launcher are largely fixed-size `char[256]` buffers. Deeply nested directories and very long names can be truncated. +- `$SDCARD_PATH` is FAT32: case-insensitive, no symlinks, no executable permission bit. Scripts run via their shebang regardless of mode bits. + +--- + +## See also + +- **[HOOKS.md](HOOKS.md)** — the hook system: phases, environment, installation +- `workspace/all/show2/README.md` — full `show2.elf` reference +- `skeleton/EXTRAS/Tools//` — the stock tool paks, all readable shell +- `skeleton/SYSTEM//paks/Emus/` — the built-in emulator paks From 362a893cc40505ae68ffffac8266d4616f2fcda5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 02:08:14 +0000 Subject: [PATCH 2/2] docs: rename OS references to nexterUI, keep NextUI where it belongs Follows the rebrand in #3 so the pak docs don't stay on the old name once it merges. Renames references to this OS and its behavior, and keeps "NextUI" for the four places it is still the correct name: - a compatibility note up top: nexterUI keeps the pak format unchanged, so paks built for NextUI work here as-is and this document applies to both - IS_NEXT is set by both nexterUI and NextUI, so it detects the lineage rather than the fork; nothing in the environment separates the two - the third-party support caveat, which is true of upstream and MinUI too Also replaced a "NextUI-only features" code comment with "not available on stock MinUI", which is what the IS_NEXT guard actually means. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Hu2BibaMPDtFk7S3NYywZE --- HOOKS.md | 8 ++++---- PAKS.md | 38 ++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/HOOKS.md b/HOOKS.md index 10ab9fded..e54a93227 100644 --- a/HOOKS.md +++ b/HOOKS.md @@ -1,4 +1,4 @@ -# NextUI hooks +# nexterUI hooks Hooks are the only mechanism that runs pak-supplied code *outside* of a pak launch. A hook is a shell script the OS executes at a fixed point in its lifecycle — at boot, around every ROM and pak launch, and around suspend and resume. @@ -48,7 +48,7 @@ Two things worth planning around: ## Environment variables -Hook scripts inherit all standard NextUI environment variables (`SDCARD_PATH`, `PLATFORM`, `USERDATA_PATH`, `SHARED_USERDATA_PATH`, `LOGS_PATH`, …). See the environment table in [PAKS.md](PAKS.md#environment). +Hook scripts inherit all standard nexterUI environment variables (`SDCARD_PATH`, `PLATFORM`, `USERDATA_PATH`, `SHARED_USERDATA_PATH`, `LOGS_PATH`, …). See the environment table in [PAKS.md](PAKS.md#environment). `run_hooks.sh` additionally exports: @@ -115,7 +115,7 @@ Two consequences that are easy to get wrong: ## Installing and removing hooks -Hooks live in `.userdata/`, outside your pak. This has a consequence worth designing for: **`.userdata/` survives both NextUI updates and pak deletion.** A hook you install keeps running after the user deletes the pak that installed it, and nothing runs on uninstall to clean it up. +Hooks live in `.userdata/`, outside your pak. This has a consequence worth designing for: **`.userdata/` survives both nexterUI updates and pak deletion.** A hook you install keeps running after the user deletes the pak that installed it, and nothing runs on uninstall to clean it up. So: @@ -185,7 +185,7 @@ To verify a hook fires at all, have it touch a file and check the timestamp: date >> "$LOGS_PATH/myhook-fired.txt" ``` -You can also run the phase by hand from a shell with the NextUI environment loaded: +You can also run the phase by hand from a shell with the nexterUI environment loaded: ```sh "$SYSTEM_PATH/bin/run_hooks.sh" post-launch.d diff --git a/PAKS.md b/PAKS.md index 6e4d3d770..5ff02bbb1 100644 --- a/PAKS.md +++ b/PAKS.md @@ -1,6 +1,6 @@ -# NextUI paks +# nexterUI paks -A pak is a folder with a `.pak` extension containing a shell script named `launch.sh`. That is the entire contract. NextUI launches the script; everything else is up to you. +A pak is a folder with a `.pak` extension containing a shell script named `launch.sh`. That is the entire contract. nexterUI launches the script; everything else is up to you. There are two kinds: @@ -11,6 +11,8 @@ There are two kinds: Both are just folders with a `launch.sh`. The only thing that makes one an emulator and the other a tool is which directory it sits in. +> **Compatibility.** nexterUI is a fork of [NextUI](https://github.com/LoveRetro/NextUI) and keeps the pak format unchanged. Paks built for NextUI work here as-is, and everything in this document applies to both. + **Contents** - [Where paks live](#where-paks-live) @@ -48,11 +50,11 @@ Both are just folders with a `launch.sh`. The only thing that makes one an emula │ ├── logs/ │ └── auto.sh legacy boot script └── shared/ - ├── minuisettings.txt all NextUI settings + ├── minuisettings.txt all nexterUI settings └── .minui/ recents, resume state ``` -Never install a pak into `.system/`. That directory is deleted and recreated by every NextUI update. +Never install a pak into `.system/`. That directory is deleted and recreated by every nexterUI update. ### Platform folders @@ -79,7 +81,7 @@ esac This is the single most important thing to understand, and it explains nearly every limitation further down. -**NextUI's launcher exits before your pak starts.** +**nexterUI's launcher exits before your pak starts.** The real init process is the shell loop in `.system//paks/MinUI.pak/launch.sh`: @@ -149,16 +151,16 @@ Daemons started at boot survive across pak launches: | `LOGS_PATH` | `$USERDATA_PATH/logs` | | | `HOOKS_PATH` | `$USERDATA_PATH/.hooks` | see [HOOKS.md](HOOKS.md) | | `DATETIME_PATH` | `$SHARED_USERDATA_PATH/datetime.txt` | | -| `IS_NEXT` | `yes` | present on NextUI, absent on stock MinUI | +| `IS_NEXT` | `yes` | set on nexterUI and NextUI, absent on stock MinUI | | `TRIMUI_MODEL` | e.g. `Trimui Brick` | Trimui platforms only | `HOME` is set to `$USERDATA_PATH` on device platforms. It is **not** exported on `desktop`, so set it yourself if your pak depends on it. -Use `IS_NEXT` to detect NextUI when writing a pak that also targets stock MinUI: +Use `IS_NEXT` to detect nexterUI or NextUI when writing a pak that also targets stock MinUI. Nothing in the environment distinguishes the fork from upstream — both set it: ```sh if [ "$IS_NEXT" = "yes" ]; then - # NextUI-only features + # not available on stock MinUI fi ``` @@ -222,7 +224,7 @@ A `map.txt` in a listed directory renames entries for display. One `filename