Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 88 additions & 53 deletions HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,121 @@

## The idea

Hooks are platform-specific, just like paks. The launcher reads them from:
Hooks are pak-scoped and self-registering. A Tools pak that wants to react to
a point in the system lifecycle drops one or more of these files right next
to its own `launch.sh`:

```
$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
Tools/<platform>/SomePak.pak/
launch.sh
boot.sh # optional -- run once at boot
pre-launch.sh # optional -- run before a launch, can cancel it
post-launch.sh # optional -- run after a launch exits
pre-sleep.sh # optional -- run before the device sleeps or powers off
post-resume.sh # optional -- run after the device wakes from sleep
```

On device, `USERDATA_PATH` resolves to:
`pak-hooks.sh` scans every installed Tools pak for these filenames and runs
whichever exist at the matching point in the lifecycle, in alphabetical
order by pak folder name. The file's presence *is* the registration: there
is no install step, no arming (no need to open the pak first), and removing
the pak removes its hooks with it.

```
/mnt/SDCARD/.userdata/$PLATFORM
```

So the actual hook directories on device are:

```
/mnt/SDCARD/.userdata/<platform>/.hooks/pre-launch.d/
/mnt/SDCARD/.userdata/<platform>/.hooks/post-launch.d/

```

Example installed hook path:

```
/mnt/SDCARD/.userdata/tg5040/.hooks/post-launch.d/shortcuts-resume.sh

```

If these directories don't exist, nothing happens and there is no overhead.
If no pak registers a given phase, nothing happens and there is no overhead.

## 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`, etc.) plus these launch-specific ones, for `pre-launch.sh`/`post-launch.sh`:

| 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) |

These can then be used by the underlying Pak to ingest information about the hook that just occurred.
`boot.sh`/`pre-sleep.sh`/`post-resume.sh` get none of these -- there's no
launch command to describe at those points. There's no `HOOK_PHASE` either:
your script's own filename (`pre-launch.sh`, `post-resume.sh`, ...) already
says which phase it's running for.

## Writing a hook script

A hook script is any executable `.sh` file in one of the hook directories. Scripts run in alphabetical order.

```sh
#!/bin/sh
# my-hook.sh — log every ROM launch
# pre-launch.sh — log every ROM launch

[ "$HOOK_TYPE" = "rom" ] || exit 0
echo "$(date): launched $HOOK_ROM_PATH" >> "$LOGS_PATH/launches.log"
```

This is how the built-in `Game Tracker.pak` hangs `gametimectl.elf
start/stop/stop_all/resume` off the lifecycle -- see its own
`pre-launch.sh`/`post-launch.sh`/`pre-sleep.sh`/`post-resume.sh`. Core code
(`api.c`, `nextui.c`) has no direct knowledge of gametimectl any more, it
only fires the generic `pak-hooks.sh` events.

## Rules

- Each script runs in a subshell. A crash or non-zero exit will not affect the launcher or other hooks.
- Each script runs in a subshell. A crash will not affect the launcher or other paks' 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.


## Example: sync after ROM exit

```sh
#!/bin/sh
# shortcuts-resume.sh — one-shot resume metadata sync after a ROM exits

[ "$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
```
- Only `pre-launch.sh` can cancel anything: a non-zero exit from **any**
registered pak's `pre-launch.sh` cancels the launch -- every pak that
registers one is assumed to need to agree before a game runs. `boot.sh`,
`post-launch.sh`, `pre-sleep.sh` and `post-resume.sh` are fire-and-forget;
their exit code is ignored. Sleep in particular isn't cancellable.
- Every registered `pre-launch.sh` runs in the same pass, in alphabetical
order by pak folder name, with **no ordering guarantee relative to
another pak's veto**. If your hook does something that must be undone
when a *different* pak vetoes the launch, that vetoing pak is responsible
for the cleanup itself -- see `Game Tracker.pak/pre-launch.sh`, which
always starts tracking unconditionally because it cannot know in advance
whether another pak will refuse the launch.
- Keep hooks fast. A slow hook delays the launch, sleep, or the return to the menu.
- Use a descriptive filename inside your own pak folder; collisions across
paks aren't possible since each hook lives inside its owning pak.
- If several of your phases share logic, don't merge them into one script
dispatched by an argument or env var -- that would force `pak-hooks.sh` to
invoke it for every phase just to find out which ones it actually handles,
losing the whole point of presence-based registration. Put the shared part
in a plain file next to your hooks and `source` it from each one instead:
`. "$(dirname "$0")/common.sh"`.

## Caching

The list of paks with at least one registered hook is cached
(`/tmp/pak_hooks_cache.txt`) to avoid re-scanning every Tools pak on every
single launch or sleep. It's built fresh on the very first call of the
session -- in practice `pak-hooks.sh boot`, so it's always current right
after a reboot.

After that, it's only rebuilt when a **Tools pak** (not a rom) exits --
see `MinUI.pak/launch.sh`, right after `eval $CMD` when `$HOOK_TYPE` is
`pak`. A rom can't change what's under `Tools/`, so the overwhelming
majority of returns to the menu (finishing a game) skip the rescan
entirely; a pak that plausibly did change something there (Pak Store,
Files.pak, ...) triggers a refresh as soon as it closes, so a newly
installed/removed pak is picked up on the very next launch instead of
staying stale until the next reboot.

## Legacy hooks (deprecated)

Before `pak-hooks.sh`, hooks were plain scripts a pak copied into
`$USERDATA_PATH/.hooks/{boot,pre-launch,post-launch,pre-sleep,post-resume}.d/`
(usually the first time it was opened), run by `run_hooks.sh`. This
mechanism is **deprecated** and kept only so a pak that already dropped
scripts there doesn't silently break:

- It requires that install/arming step, and nothing removes the script if
the pak is later deleted -- it becomes an orphan.
- `pre-launch.d`/`post-launch.d` scripts here **cannot veto a launch**; only
a pak-scoped `pre-launch.sh` can.
- `run_hooks.sh` logs a warning to `$LOGS_PATH/hooks-deprecated.txt`
whenever it actually finds a script to run in one of these directories,
so a stale legacy hook doesn't go unnoticed.

New hooks should always be pak-scoped (`pak-hooks.sh`), never dropped into
`.hooks/`. Migrating an existing legacy hook just means moving the script
into your pak's own folder under the matching name (`pre-launch.sh`,
`post-launch.sh`, `pre-sleep.sh`, `post-resume.sh`, or `boot.sh`).
6 changes: 6 additions & 0 deletions skeleton/EXTRAS/Tools/desktop/Game Tracker.pak/post-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
# post-launch.sh -- close the play_activity row for the rom that just exited.

[ "$HOOK_TYPE" = "rom" ] || exit 0

gametimectl.elf stop "$HOOK_ROM_PATH"
4 changes: 4 additions & 0 deletions skeleton/EXTRAS/Tools/desktop/Game Tracker.pak/post-resume.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
# post-resume.sh -- reopen tracking for the last rom after the device wakes.

gametimectl.elf resume
13 changes: 13 additions & 0 deletions skeleton/EXTRAS/Tools/desktop/Game Tracker.pak/pre-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# pre-launch.sh -- start tracking play time for the rom about to launch.
#
# Scanned by pak-hooks.sh alongside every other registered pak's pre-launch.sh
# in the same pass, with no ordering guarantee relative to any pak that might
# still veto the launch -- so this always starts tracking unconditionally.
# Any pak that vetoes a launch (exits non-zero from its own pre-launch.sh)
# is responsible for calling `gametimectl.elf stop_all` itself -- see the
# ordering note in HOOKS.md.

[ "$HOOK_TYPE" = "rom" ] || exit 0

gametimectl.elf start "$HOOK_ROM_PATH"
5 changes: 5 additions & 0 deletions skeleton/EXTRAS/Tools/desktop/Game Tracker.pak/pre-sleep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# pre-sleep.sh -- close the open play_activity row before the device sleeps
# or powers off (fire-and-forget, cannot veto sleep).

gametimectl.elf stop_all
6 changes: 6 additions & 0 deletions skeleton/EXTRAS/Tools/tg5040/Game Tracker.pak/post-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
# post-launch.sh -- close the play_activity row for the rom that just exited.

[ "$HOOK_TYPE" = "rom" ] || exit 0

gametimectl.elf stop "$HOOK_ROM_PATH"
4 changes: 4 additions & 0 deletions skeleton/EXTRAS/Tools/tg5040/Game Tracker.pak/post-resume.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
# post-resume.sh -- reopen tracking for the last rom after the device wakes.

gametimectl.elf resume
13 changes: 13 additions & 0 deletions skeleton/EXTRAS/Tools/tg5040/Game Tracker.pak/pre-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# pre-launch.sh -- start tracking play time for the rom about to launch.
#
# Scanned by pak-hooks.sh alongside every other registered pak's pre-launch.sh
# in the same pass, with no ordering guarantee relative to any pak that might
# still veto the launch -- so this always starts tracking unconditionally.
# Any pak that vetoes a launch (exits non-zero from its own pre-launch.sh)
# is responsible for calling `gametimectl.elf stop_all` itself -- see the
# ordering note in HOOKS.md.

[ "$HOOK_TYPE" = "rom" ] || exit 0

gametimectl.elf start "$HOOK_ROM_PATH"
5 changes: 5 additions & 0 deletions skeleton/EXTRAS/Tools/tg5040/Game Tracker.pak/pre-sleep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# pre-sleep.sh -- close the open play_activity row before the device sleeps
# or powers off (fire-and-forget, cannot veto sleep).

gametimectl.elf stop_all
6 changes: 6 additions & 0 deletions skeleton/EXTRAS/Tools/tg5050/Game Tracker.pak/post-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
# post-launch.sh -- close the play_activity row for the rom that just exited.

[ "$HOOK_TYPE" = "rom" ] || exit 0

gametimectl.elf stop "$HOOK_ROM_PATH"
4 changes: 4 additions & 0 deletions skeleton/EXTRAS/Tools/tg5050/Game Tracker.pak/post-resume.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
# post-resume.sh -- reopen tracking for the last rom after the device wakes.

gametimectl.elf resume
13 changes: 13 additions & 0 deletions skeleton/EXTRAS/Tools/tg5050/Game Tracker.pak/pre-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# pre-launch.sh -- start tracking play time for the rom about to launch.
#
# Scanned by pak-hooks.sh alongside every other registered pak's pre-launch.sh
# in the same pass, with no ordering guarantee relative to any pak that might
# still veto the launch -- so this always starts tracking unconditionally.
# Any pak that vetoes a launch (exits non-zero from its own pre-launch.sh)
# is responsible for calling `gametimectl.elf stop_all` itself -- see the
# ordering note in HOOKS.md.

[ "$HOOK_TYPE" = "rom" ] || exit 0

gametimectl.elf start "$HOOK_ROM_PATH"
5 changes: 5 additions & 0 deletions skeleton/EXTRAS/Tools/tg5050/Game Tracker.pak/pre-sleep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# pre-sleep.sh -- close the open play_activity row before the device sleeps
# or powers off (fire-and-forget, cannot veto sleep).

gametimectl.elf stop_all
66 changes: 66 additions & 0 deletions skeleton/SYSTEM/desktop/bin/pak-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/sh
# pak-hooks.sh - self-registering, pak-scoped lifecycle hooks
#
# The sole hook mechanism in NextUI: scans installed Tools paks for a
# boot.sh / pre-launch.sh / post-launch.sh / pre-sleep.sh / post-resume.sh
# file sitting right next to their launch.sh. Presence of the file *is* the
# registration: no install step, no arming, and removing the pak removes
# its hooks with it.
#
# Usage: pak-hooks.sh rebuild-cache
# pak-hooks.sh boot (fire-and-forget, exit code ignored)
# pak-hooks.sh pre-launch (exit != 0 vetoes the launch)
# pak-hooks.sh post-launch
# pak-hooks.sh pre-sleep (fire-and-forget, exit code ignored)
# pak-hooks.sh post-resume (fire-and-forget, exit code ignored)
#
# The list of qualifying paks is cached to avoid re-scanning every Tools pak
# on every single launch/sleep. The cache is rebuilt when returning to the
# main menu (see nextui.c), not at call time, so a freshly installed/removed
# pak is picked up on the very next visit to the menu instead of going stale
# until the next reboot. At boot, no menu visit has happened yet, so the
# first call below builds the cache fresh.

: "${SDCARD_PATH:=/var/tmp/nextui/sdcard}"
: "${PLATFORM:=desktop}"
CACHE="/tmp/pak_hooks_cache.txt"

rebuild_cache() {
: > "$CACHE"
for pak in "$SDCARD_PATH"/Tools/"$PLATFORM"/*.pak; do
[ -d "$pak" ] || continue
if [ -x "$pak/boot.sh" ] || [ -x "$pak/pre-launch.sh" ] \
|| [ -x "$pak/post-launch.sh" ] || [ -x "$pak/pre-sleep.sh" ] \
|| [ -x "$pak/post-resume.sh" ]; then
echo "$pak" >> "$CACHE"
fi
done
}

run_phase() {
PHASE="$1" # boot | pre-launch | post-launch | pre-sleep | post-resume
SCRIPT_NAME="$PHASE.sh"
# no HOOK_PHASE env var here: the script's own filename already tells it
# which phase it's running for, so a separate variable would just repeat
# that. Share logic across phases by sourcing a common file instead.

[ -f "$CACHE" ] || rebuild_cache

VETOED=0
while IFS= read -r pak; do
[ -n "$pak" ] || continue
[ -x "$pak/$SCRIPT_NAME" ] || continue
if ! "$pak/$SCRIPT_NAME"; then
VETOED=1
fi
done < "$CACHE"

# only pre-launch actually vetoes anything; every other phase is
# fire-and-forget and callers don't check this exit code
exit $VETOED
}

case "$1" in
rebuild-cache) rebuild_cache ;;
boot|pre-launch|post-launch|pre-sleep|post-resume) run_phase "$1" ;;
esac
17 changes: 16 additions & 1 deletion skeleton/SYSTEM/desktop/bin/run_hooks.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#!/bin/sh
# run_hooks.sh - shared hook runner for NextUI
# run_hooks.sh - DEPRECATED legacy hook runner for $USERDATA_PATH/.hooks/
#
# Superseded by pak-hooks.sh: self-registering, no arming step, no orphaned
# scripts left behind when a pak is deleted. See HOOKS.md. Kept only so a
# pak that already dropped scripts into .hooks/ doesn't silently stop
# working -- do not build anything new against this mechanism.
#
# Usage: run_hooks.sh <dir-name> [--sync-only]
#
# dir-name: directory name under $USERDATA_PATH/.hooks/ (e.g. pre-launch.d, boot.d)
Expand All @@ -14,10 +20,19 @@ SYNC_ONLY="${2:-}"
: "${SDCARD_PATH:=/var/tmp/nextui/sdcard}"
: "${PLATFORM:=desktop}"
: "${USERDATA_PATH:=$SDCARD_PATH/.userdata/$PLATFORM}"
: "${LOGS_PATH:=$USERDATA_PATH/logs}"

HOOK_DIR="$USERDATA_PATH/.hooks/$DIR_NAME"
[ -d "$HOOK_DIR" ] || exit 0

# only warn when there's actually a script to run here -- an unused,
# never-populated .hooks/ directory shouldn't spam the log
for _probe in "$HOOK_DIR"/*.sh; do
[ -f "$_probe" ] || continue
echo "$(date): DEPRECATED - $HOOK_DIR/$(basename "$_probe") uses the legacy .hooks/ mechanism (run_hooks.sh). Migrate to a pak-scoped hook (pak-hooks.sh) -- see HOOKS.md." >> "$LOGS_PATH/hooks-deprecated.txt"
break
done

case "$DIR_NAME" in
pre-*) export HOOK_PHASE="pre" ;;
post-*) export HOOK_PHASE="post" ;;
Expand Down
Loading