Press any button to continue
Running arbitrary scripts on an emulation handheld
You’ve probably heard of retro handhelds, or ‘Game Dads’—little gameboy-like things that run emulators for old consoles. I mentioned that I had one a year ago, and in fact later I picked up the Anbernic “SP” model when that came out. It’s not the nostalgia, because I never had the original devices they’re trying to mimic, I just like the games (and, in the SP’s case, the clamshell form factor).
The particular machines I have run Linux on little ARM CPUs,1 and there are thriving communities of developers who make whole alternative operating systems for them that are vastly superior to what comes pre-installed. It’s pretty neat, even if it’s a little scuzzy that the manufacturers effectively profit off of this unpaid work.
But the power of Linux, especially a Linux you chose and installed yourself, is you can make modifications and run your own stuff. The curse of Linux though, especially if you have any inclination, is that you eventually find yourself compelled to do so.
Before I explain the why though, let me talk about what.
As mentioned, I’m using a device called the Anbernic RG35XX SP. This confusingly named device is not the same as the RG35XX I demonstrated ADB on last year (I promise I’m not collecting the things). The operating system is muOS 2410.3 AW BANANA,2 which comes with a folder of scripts called the “Task Toolkit” that we can sneak our own into—but I think most of these operating systems can at very least launch shell scripts from their ROM folders.
To the extent that this is a tutorial, it’s aimed at people who already have a basic idea about Bash scripting but aren’t sure how to translate it into something that will work on a handheld. For an example, we’ll write a script that prints the current year:
#! /bin/bash
CURYEAR=$(date +%Y)
echo "$CURYEAR"
This is a perfectly ordinary Bash script that will run more-or-less anywhere, albeit an overly verbose and not terribly useful one. The first line is the Shebang, which lets the OS know that this is a Bash script and not something else; the next runs the date
command to get the year, and saves it as the variable CURYEAR
; and the final line prints this to the screen. If we put this script (as WhatYear.sh
) in /mnt/mmc/MUOS/task/
and run it through the interface, we get… a brief flash of black and are then back in the menu. Not what we wanted at all!

This is the biggest actual problem with scripting like this: we can write scripts like any normal Linux, our issues come in the form of the UI. How do you actually get the information that your script exposes, or confirm that it succeeded at all? It could save its result as a file, and you could read that through another app, but that’s way too much trouble.
Instead, the obvious trick is the sleep
command—just add sleep 5
to the end of the script and it will display the output for five seconds and then close. Sweet!

But we can do better. You may remember (I certainly do) the Press any key to continue...
prompt displayed in a DOS window on Windows 98 or XP or similar before it closed: rather than waiting for a fixed amount of time this construct exits to wait for user input before exiting, which is exactly what we want here. To get the same behaviour in Bash we can replace sleep 5
with read -p "Press any key to continue... " -n1 -s -t 10
.

The -t 10
part of this command invocation is a timeout,3 which will cause it to end and the script to progress after ten seconds no-matter what we do. If we were to run the script like this on our particular device we would discover that the timeout was very useful, because the on-device gamepad doesn’t count for the input here. Oops!
In theory the read
command can take input from somewhere else with the -u
flag, but I can’t get that to work in this context. Instead, the command I’ve found works is timeout 10s evtest /dev/input/event1 | read
, though I’m not a hundred percent on why it works.
/dev/input/event1
is the ‘event file’ for the inbuilt gamepad,4 while evtest
gets information from the specified input device, including a stream of events as they come in. Piping this to read
seems to work as a ‘wait until something happens’ system, and I’m not going to a look a gift horse in the mouth. The timeout 10s
bit works just as you’d expect, letting the script continue (and therefore end) after ten seconds no-matter what.

There are other things we can do: ANSI escape sequences let us colour text, while the muOS task list lets us include help text and an icon by specifying them in the first few lines. The full script now looks like:
#! /bin/bash
# HELP: Show the current year
# ICON: sdcard
CURYEAR=$(date +%Y)
echo -e "\033[32;40m$CURYEAR\033[0m"
echo "Press any button to continue"
timeout 10s evtest /dev/input/event1 | read
Beautiful!

I’m sure it’s not escaped anyone’s attention that this example script we’ve been building is completely and utterly useless, and that I still haven’t explained why I want to write my own scripts for a device like this. Well you can do what you like, of course, but I’m interested in playing games on it—while at the same time sometimes I want to play those very same games on other things, specifically the PC hooked up to my TV. The canonical way to automatically share save files like this seems to be a tool called Syncthing, but Syncthing copies whole directories between machines and for a variety of reasons I don’t want that. All I need is to copy the save files for specific RPGs and the like, and to only do that copying when I specify for the game I specify. As a bonus I’d like to automatically backup saves before they are overwritten, and to have a UI I can use to check that I have the right version before I launch it.
To get this working I’ve created a separate folder on the SD card just for Syncthing (and another for those backups), and have written a small constellation of scripts to manage synchronising my saves with that folder—a similar set exists on the TV PC to complete the process at the other end, and in theory could also sit on another device like a SteamDeck (if I had one).

There is a fine line here between going into excruciating detail about what I’ve made, and making you imagine the rest of the owl. The key elements, besides the timeout prompt and the ANSI colours aforementioned, are -nt
to compare file ages, rsync
(or just cp
) to copy files around while preserving modification time, and refactoring repeatable parts into a second set of scripts that can be called by the main tasks.
For an example of the power of the latter, the script I’ve called ST All Diff
5—which gives information about the relative ages of the local and sync versions of each save game—can be as simple as these twenty lines:
#! /bin/bash
# HELP: Compare local and sync versions of save games
# ICON: sdcard
LOCALDIR="/mnt/sdcard/MUOS/save/file"
SYNCDIR="/mnt/sdcard/rpg-saves"
/mnt/mmc/scripts/fdate-diff.sh "Inheritors of the Oubliette (GBA)" \
"$LOCALDIR/GBA/mGBA/inheritors-of-the-oubliette-v1.2.srm" \
"$SYNCDIR/inheritors-of-the-oubliette-v1.2.srm"
timeout 1s evtest /dev/input/event1 | read
/mnt/mmc/scripts/fdate-diff.sh "Green Memories (GBA)" \
"$LOCALDIR/GBA/mGBA/green_memories_v147.srm" \
"$SYNCDIR/green_memories_v147.srm"
sleep 1
echo "Press any button to continue"
timeout 10s evtest /dev/input/event1 | read
That produces this output, which can easily be extended to as many games as needed.

As an aside, the games I’m using to demonstrate this system for you are both modern GBA homebrews available free on itch.io: Inheritors of the Oubliette is a 3d first-person dungeon crawler roguelike, while Green Memories is an action RPG about restoring a battered Earth—both seem pretty neat and worth much more than the $0 they’re charging. I’ll confess that neither are actually the game I’m playing right now, but they’re officially On The List.

I’ve put the full set of scripts in this zip file, though if you prefer to peruse them on github that’s also an option.6 A certain level of reworking will be required to use them in your own setup, even if your use-case is exactly the same as mine, but I think they’re fairly self-explanatory.
It’s inevitable that I’ll make more software for my handhelds, they’re just so neat. But in the meantime I’ve got some games to play—a lot of games.
As opposed to chunky ARM chips running Android in the price bracket above, and x86 SteamDeck-alikes above that.↩︎
The theme I’m using is CatOS Pink, credit to hxhiep, Pankeko, and Purrito Supreme for various parts of that. That much isn’t relevant to the scripts I’m going to demonstrate, however, only the menus.↩︎
An extremely niche note: if the
read
command exits due to the timeout rather than a keypress, it counts as an error for return code purposes. This means that if you are using it as some kind of mid-script pause, and the script hasset -e
at the start (to exit on error, rather than continuing execution—a useful trick) then it will immediately stop. This is probably not what you wanted.↩︎This does mean that a secondary gamepad will not work here, another good reason for a timeout.↩︎
‘ST’ for Syncthing and because this prefix groups all the scripts together near the end of the list.↩︎
Edit 11 Feb: I forgot to upload the scripts and add these links. Oops!↩︎