diff --git a/HOOKS.md b/HOOKS.md index bd6583c83..e54a93227 100644 --- a/HOOKS.md +++ b/HOOKS.md @@ -1,76 +1,145 @@ -# About 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. + +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 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: + +| 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 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: + +**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 nexterUI 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..5ff02bbb1 100644 --- a/PAKS.md +++ b/PAKS.md @@ -1,107 +1,473 @@ -# About 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. nexterUI launches the script; everything else is up to you. + +There are two kinds: -A pak is just a folder with a ".pak" extension that contains a shell script named "launch.sh". +| 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. + +> **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) +- [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 -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. +``` +/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 nexterUI settings + └── .minui/ recents, resume state +``` + +Never install a pak into `.system/`. That directory is deleted and recreated by every nexterUI 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 +``` + +--- -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. +## How a pak runs + +This is the single most important thing to understand, and it explains nearly every limitation further down. -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. +**nexterUI's launcher exits before your pak starts.** -# The types of emulator pak +The real init process is the shell loop in `.system//paks/MinUI.pak/launch.sh`: -There are three basic types of emulator paks, which you chose depends on your goals and your desired level of NextUI integration. +```sh +while [ -f $EXEC_PATH ]; do + nextui.elf &> $LOGS_PATH/nextui.txt # the launcher UI runs here, then EXITS -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. + 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 +``` -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. +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 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. +### What follows from this -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. +- **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. -# Naming your emulator pak +### What stays running underneath -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. +Daemons started at boot survive across pak launches: -# Launching your core +| 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 | -Here's an example "launch.sh": +`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. - #!/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" +--- -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: +## Environment - CORES_PATH=$(dirname "$0") +`launch.sh` is invoked from the boot script, so it inherits everything that script exported. -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". +### Paths -That's it! Feel free to experiement with cores from the stock firmware, other compatible devices, or building your own. +| 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` | set on nexterUI and NextUI, absent on stock MinUI | +| `TRIMUI_MODEL` | e.g. `Trimui Brick` | Trimui platforms only | -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. +`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. -# Option defaults and button bindings +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: -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. +```sh +if [ "$IS_NEXT" = "yes" ]; then + # not available on stock MinUI +fi +``` -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": +### Search paths - 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 +PATH=$SYSTEM_PATH/bin:/usr/trimui/bin:$PATH +LD_LIBRARY_PATH=$SYSTEM_PATH/lib:/usr/trimui/lib:$LD_LIBRARY_PATH +``` -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: +Everything in `.system//bin` is callable by bare name — `minarch.elf`, `show2.elf`, `nextval.elf`, `syncsettings.elf`, `gametimectl.elf`, `governor.sh`, `run_hooks.sh`. - bind More Sun = L3 +### CPU governor -to +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 More Sun = NONE:L3 +```sh +sh "$SYSTEM_PATH/bin/governor.sh" "auto" +``` -# Brightness and Volume +--- -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: +## Tool paks - syncsettings.elf & - ./DinguxCommander +A tool pak is any pak in `/Tools//`. It appears in the Tools menu, named after its folder minus the `.pak` extension. -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: +Minimal example — `/Tools/tg5040/Hello.pak/launch.sh`: - while :; do - syncsettings.elf - done & - LOOP_PID=$! - - ./PPSSPPSDL --pause-menu-exit "$ROM_PATH" - - kill $LOOP_PID +```sh +#!/bin/sh -# Caveats +cd "$(dirname "$0")" +./hello.elf &> "$LOGS_PATH/hello.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 +`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. + +### Visibility + +An entry is hidden from every menu when its name: + +- starts with `.` +- ends with `.disabled` +- is exactly `map.txt` + +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 + +nexterUI 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 nexterUI 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 nexterUI'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 nexterUI 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 nexterUI brightness and volume | +| `nextval.elf` | read any nexterUI 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 nexterUI 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 nexterUI 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 + +- nexterUI 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 nexterUI project, and neither upstream NextUI nor MinUI supports them either. When a console or core is absent from the base or extras bundles that 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