Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ VENDORED_LIBS =
endif
CXXFLAGS = -std=c++14 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\"
LDLIBS = $(SDL_LIBS)
SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp src/framework/list_screen.cpp src/framework/panel_dialog.cpp
SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp src/framework/list_screen.cpp src/framework/panel_dialog.cpp src/framework/close_on_exec.cpp
OBJECTS = $(SOURCES:.cpp=.o)
ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/save_manager.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o

Expand Down
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This is my continuation of [CompCom's Options Menu](https://github.com/CompCom/O

## Install

**From my Mod Hub (recommended):** in hakchi open **Manage repositories**, add `https://defkorns.github.io/hakchi-repo/` and install **Options Menu** from it. Updates show up there automatically.
**From my Mod Hub (recommended):** in hakchi choose **Modules → Manage mod repositories** and add `https://defkorns.github.io/hakchi-repo/`. Then open **Modules → DefKorns Store** and install **Options Menu** from the *Options Menu* tab. Updates show up there automatically.

**By hand:** download `options_menu.hmod` from [Releases](https://github.com/DefKorns/OptionsMenu/releases), put it in hakchi's `user_mods` folder (or drag and drop it onto the hakchi window), then install it from hakchi (**Modules → Install extra modules**).

Expand Down Expand Up @@ -49,7 +49,7 @@ The footer always shows the buttons for the current screen.
| RetroArch › | RetroArch settings, backups and BIOS files. Shown when RetroArch is installed |
| Network › | IP address, Wi-Fi scan and Wi-Fi config backups |
| Save Manager › | Back up, restore and clean up your saves. Needs a USB/SD drive |
| Language › | Menu language; the menu restarts in the new language |
| Language › | Menu language: *Automatic* follows the console's own language, or pick one. The menu restarts in the new language |
| Advanced Options › | Controller, diagnostics and system tools |

\*Don't shut down or restart from the menu while RetroArch is running: exit RetroArch first, or you may lose saves.
Expand Down Expand Up @@ -94,7 +94,7 @@ Restore entries only show up when there's a backup to restore.

### Save Manager

Needs a USB/SD drive with your saves on it. One screen lists the actions on the left; the right panel explains the selected one and shows how many saves are on the console, in the USB backup, left over from removed games and moved aside. Every action asks for confirmation (**Start + Select**).
Needs a USB/SD drive with your saves on it. One screen lists the actions on the left; the right panel explains the selected one and shows how many saves are on the console, in the USB backup, left over from removed games and moved aside. Every action asks for confirmation: **Start + Select** for the ones that overwrite or move saves, **A** for bringing back the saves of removed games, which never overwrites anything.

| Action | What it does |
| --- | --- |
Expand All @@ -107,7 +107,7 @@ In the long lists (here and in the Module Uninstaller), **Left**/**Right** page

#### Game names

The two "Removed Games" lists show the names of the removed games instead of their codes (`CLV-H-ABCDE`). Every time the console starts, it remembers the names of the games installed at that moment in `game_names.txt` in your saves folder, and a name stays there after its game is removed. The backup keeps the file with your saves, and a restore adds its names without replacing newer ones. Saves without a known name keep showing their code. Getting the names never changes or moves a save.
The two "Removed Games" lists show the names of the removed games instead of their codes (`CLV-H-ABCDE`). Every time the console starts, it remembers the names of the games installed at that moment in `game_names.txt` in your saves folder, and a name stays there after its game is removed. The backup keeps the file with your saves, and a restore adds its names without replacing newer ones. Saves without a known name keep showing their code, two games with the same name show their code too, and empty save folders aren't listed. Getting the names never changes or moves a save.

Games removed before this version was installed were never seen on the console, so their names can only come from your Hakchi CE library. The console can't read your PC, so the PC sends them. With the console on and reachable by Hakchi CE (USB or Wi-Fi), run [`pc/push_game_names.ps1`](pc/push_game_names.ps1) from a PowerShell window:

Expand Down Expand Up @@ -184,7 +184,7 @@ One file per entry, named `cNNNN_Name`; entries are sorted by name. Each line is
| Field | Description |
| --- | --- |
| `COMMAND_NAME` | Label shown, or a translation key |
| `COMMAND_TYPE` | `INTERNAL` shows the command's output in the menu's console; `EXTERNAL` runs it with the menu paused; `DOWNLOAD` runs it on a progress screen with a bar (fed by the downloader's `--progress` lines on stdout) and a B to cancel, closing on its own when the command exits 0 |
| `COMMAND_TYPE` | `INTERNAL` shows the command's output in the menu's console; `EXTERNAL` runs it with the menu paused; `DOWNLOAD` runs it on a progress screen with a bar (fed by the downloader's `--progress` lines on stdout) and a B to cancel, closing on its own when the command exits 0; `CHOICE` turns the entry into a value picker (see the `CHOICE_*` fields) |
| `COMMAND_STR` | Single-line command to run. Use a script for anything longer |
| `RESTART_UI` | External commands only. `FALSE` leaves the UI paused; resume it yourself with `/bin/sh /etc/options_menu/scripts/ResumeUI.sh` |
| `IGNORE_INTERRUPT` | `TRUE`: B doesn't interrupt an internal command while it runs |
Expand All @@ -197,11 +197,33 @@ One file per entry, named `cNNNN_Name`; entries are sorted by name. Each line is
| `DELETE_CONFIRM_KEY` | Translation key for that confirmation; defaults to a generic "Delete this item?" |
| `DELETE_BLOCKED_KEY` | Translation key: holding B on the entry shows this message with a Back badge instead of asking to delete it, for entries that can't be removed right now |
| `PREVIEW_IMAGE` | PNG shown in the detail panel, scaled to fit |
| `CHOICE_LIST` | `CHOICE` only. Command printing one `value<TAB>label<TAB>preview` line per option; the label can be a translation key and the preview is optional |
| `CHOICE_GET` | `CHOICE` only. Prints the stored value; empty means *Automatic* |
| `CHOICE_SET` | `CHOICE` only. Stores the value picked, with `%value%` replaced by it (empty for *Automatic*) |
| `CHOICE_AUTO` | `CHOICE` only. Translation key of the *Automatic* option, shown when no value is stored |
| `CHOICE_RESOLVED` | `CHOICE` only. Prints the value *Automatic* stands for, so its label and preview can be shown |

`COMMAND_STR`, `DELETE_STR` and `STATE_STR` can use `%options_path%` (the folder with the `options` binary) and `%script_dir%` (the current `--scriptPath`).

The shared framework also reads a few grid-only fields (`PREVIEW_NEAREST`, `PREVIEW_SQUARE`, `PREVIEW_GRID_COLS`, `PREVIEW_FIT_CONTAIN`, `PREVIEW_HIDE_LABEL`). The menu itself always shows a list; they're for apps built on the shared framework that use a grid layout. `PREVIEW_IMAGE_X/Y/WIDTH/HEIGHT` from the original format are accepted and ignored.

### Script helpers

Your scripts can `source /etc/options_menu/scripts/om_functions` for the helpers the menu's own scripts use. The main ones:

| Function | What it does |
| --- | --- |
| `translate <key>` | Prints the string for the menu's current language |
| `omSplash <key...>` | Draws one translated line per key, full screen |
| `omLoading [key...]` | Same, with a moving bar, until your script exits; "Loading" without keys |
| `omDownload <out> <url> [fallback...]` | Downloads a file, printing progress for a `DOWNLOAD` entry; exit code 3 means no free space |
| `omDownloadAll <list> <handler> [keep]` | Downloads a list of files over one connection |
| `omUnpack <tar.gz> <dir>` | Unpacks an archive if it fits, then removes it |
| `omReloadMenu` / `omReloadMenuLater` | Stops the UI and reloads the current Home folder, keeping your splash text on screen |
| `enableCmds` / `disableCmds <name...>` | Show or hide entries by renaming `$enable_<name>` / `$disable_<name>` |

Since 2.1.0, scripts started from the menu no longer hold `/dev/fb0`, so they can call `chmenu` (or anything else that runs `uistop`) without being killed. On 2.0.0 they still hold it: hand such a command to hakchi's `remote-exec` instead, by writing it to `/var/exec.flag`.

### Translations

Strings live in `/etc/options_menu/language/<code>.lang` as `KEY=Text`; the chosen language is in `/etc/options_menu/language.cfg`. `en-US` is always loaded first, so missing keys fall back to English.
Expand Down
2 changes: 1 addition & 1 deletion mod/etc/options_menu/network/scripts/backup-wifi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ restoreWifi() {

case "$2" in
backup)
omSplash WIFI_BACKUP_SPLASH_LINE1 WIFI_BACKUP_SPLASH_LINE2
omLoading WIFI_BACKUP_SPLASH_LINE1 WIFI_BACKUP_SPLASH_LINE2
backupWifi
echo "WIFI config backed up to $network"
sleep 1
Expand Down
12 changes: 10 additions & 2 deletions mod/etc/options_menu/scripts/om_functions
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ omSplash() {
"$om_scripts/gen_splash" /tmp/om_splash.png "$@" && decodepng /tmp/om_splash.png >/dev/fb0
}

# omLoading [key...]: omSplash plus a moving bar until the calling script exits; "Loading" without keys
omLoading() {
om_loading_keys="$*"
"$om_scripts/gen_splash" --loading "$@" 2>/dev/null
}

# omFreeKb <dir>
omFreeKb() {
df -Pk "$1" | awk 'NR==2 {print $4}'
Expand Down Expand Up @@ -245,18 +251,20 @@ omUnpack() {
# omReloadMenuLater [command...]: the menu holds /dev/fb0 and uistop kills every holder,
# so remote-exec runs omReloadMenu once the caller has exited
omReloadMenuLater() {
echo "source $om_scripts/om_functions; omReloadMenu $*" >/var/exec.flag.tmp
echo "source $om_scripts/om_functions; om_loading_keys=\"$om_loading_keys\"; omReloadMenu $*" >/var/exec.flag.tmp
mv /var/exec.flag.tmp /var/exec.flag
}

# omReloadMenu [command...]: stops the UI, runs the command, then reloads the current folder;
# omReloadMenu [command...]: stops the UI, shows Loading, runs the command, then reloads the current folder;
# chmenu refuses the folder already shown unless its state says otherwise
omReloadMenu() {
local menu_state="/var/saves/$modname/menu" current_menu="000"
rm -f /tmp/options.flag
uistop
uiresume
timeout="" waittimeout 1500 '! lsof -n | grep -qF "/dev/fb0"' "framebuffer"
# shellcheck disable=SC2086
omLoading ${om_loading_keys:-LOADING}
[ $# -gt 0 ] && "$@"
[ -f "$menu_state" ] && current_menu="$(cat "$menu_state")"
echo reload >"$menu_state"
Expand Down
4 changes: 2 additions & 2 deletions mod/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ The Options Menu is a custom, easily extendable menu for your console, letting o
- **Power:** Hibernate/Standby, Shutdown, Restart
- **RetroArch:** back up/restore settings (NAND or USB), restore defaults, transfer BIOS files, clean up overrides/remaps/BIOS, toggle load screens
- **Network:** show IP, reconnect, search for SSIDs, back up/restore your wifi config (NAND or USB)
- **Save Manager:** back up/restore saves to USB, and move aside (or bring back) saves of games no longer on the console
- **Language:** English, Français, Deutsch, Español, Italiano, Nederlands, Português, Русский, 日本語
- **Save Manager:** back up/restore saves to USB, and move aside (or bring back) saves of games no longer on the console, listed by game name
- **Language:** follows the console's own language, or pick one of English, Français, Deutsch, Español, Italiano, Nederlands, Português, Русский, 日本語
- **Advanced:**
- Controller: change the menu button combo, autofire (speed, X/Y as turbo A/B), Home menu combo, Start on the 2nd controller
- Diagnostics: system info, temperature, top, benchmark, RetroArch debugger, file structure and kernel log dumps to USB
Expand Down
21 changes: 21 additions & 0 deletions release-notes/v2.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Options Menu v2.1.0

- **Menu language follows the console:** Language → Options Menu language now defaults to *Automatic*, which uses the console's own language. Pick a language there to override it.
- **Clearer confirmations:** non-destructive actions confirm with **A**; deleting or overwriting still needs **Start+Select**. Save Manager and Module Uninstaller dialogs share the same look.
- **Save Manager:** saves of games with the same name are tagged with their game code, and empty saves are hidden.
- **Downloads:** add-ons can show a download screen with a progress bar and the free space left, and the file being downloaded is shown by its name only (`2ks_star_wars` instead of `SNES.2ks_star_wars.tar.g`).
- **Waiting screens:** add-ons can show their "please wait" messages with a moving bar, and reloading the Home menu shows it instead of a black screen.
- **More robust:** if the menu is closed by the system (SIGTERM/SIGHUP), the console's UI comes back instead of staying paused. Scripts started from the menu no longer hold the screen, so `chmenu` and folder switches don't kill them halfway.

## Why 2.1.0

This release adds features without breaking anything that already works, so it's a minor version bump (2.0.0 → 2.1.0), not a patch:

- **New for add-ons:** the `DOWNLOAD` and `CHOICE` command types, the `DELETE_BLOCKED_KEY` dialog, and new `om_functions` helpers (`omSplash`, `omLoading`, `omUnpack`, `omReloadMenu`, `omReloadMenuLater`, `omLangCode`, `omLangList`). Add-ons that use them need 2.1.0 or newer.
- **Nothing removed for installed add-ons:** command files and the `om_functions` helpers from 2.0.0 work as before, so add-ons made for 2.0.0 keep working.
- **For C++ add-on developers:** the framework grew `ScreenChrome`, `PanelDialog`, `NoteBox`, list-row and text-layout helpers, and `close_on_exec`. `ModernChrome` is gone (use `ScreenChrome`) and the code builds with `-std=c++14`. Add-ons compile the framework in, so this only matters when you update your `vendor/OptionsMenu`.

## Which file?

- `options_menu.hmod`: use this one.
- `options_menu_compat.hmod`: only if another hmod replaces the console's `libstdc++` (e.g. RetroArch 1.8.4 Xtreme SC) and the menu doesn't open with the normal build.
9 changes: 8 additions & 1 deletion src/command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

#include "command.h"
#include "framework/close_on_exec.h"
#include "framework/controller.h"
#include "framework/draw_helpers.h"
#include "framework/list_screen.h"
Expand All @@ -34,6 +35,7 @@ static CommandProcess StartCommand(const std::string & command)
int fds[2];
if(pipe(fds) != 0)
return { nullptr, -1 };
MarkFdsCloseOnExec();
const pid_t pid = fork();
if(pid < 0)
{
Expand Down Expand Up @@ -279,7 +281,10 @@ void Command::RunCommand(SDL_Context & sdl_context, Controller * controller, Scr
static std::vector<std::string> ReadLines(const std::string & command)
{
std::vector<std::string> lines;
FILE * pipe = command.empty() ? nullptr : popen(command.c_str(), "r");
if(command.empty())
return lines;
MarkFdsCloseOnExec();
FILE * pipe = popen(command.c_str(), "r");
if(!pipe)
return lines;
std::string line;
Expand Down Expand Up @@ -342,6 +347,7 @@ void Command::SetChoice(const std::string & value)
size_t pos;
while((pos = set.find("%value%")) != std::string::npos)
set.replace(pos, 7, value);
MarkFdsCloseOnExec();
system(set.c_str());
choiceValue = value;
choiceResolved = value.empty() ? choiceAutoResolved : value;
Expand All @@ -361,6 +367,7 @@ void Command::UpdateState()
if(stateCommand.empty())
return;

MarkFdsCloseOnExec();
FILE * pipe = popen(stateCommand.c_str(), "r");
if(!pipe)
return;
Expand Down
2 changes: 2 additions & 0 deletions src/download_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

#include "download_screen.h"
#include "framework/close_on_exec.h"
#include "framework/draw_helpers.h"
#include "framework/list_screen.h"
#include "framework/text_layout.h"
Expand Down Expand Up @@ -45,6 +46,7 @@ namespace
int fds[2];
if(pipe(fds) != 0)
return;
MarkFdsCloseOnExec();
pid_ = fork();
if(pid_ < 0)
{
Expand Down
41 changes: 41 additions & 0 deletions src/framework/close_on_exec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#include "close_on_exec.h"

#include <cstdlib>
#include <memory>
#include <dirent.h>
#include <fcntl.h>

void MarkFdsCloseOnExec()
{
std::unique_ptr<DIR, decltype(&closedir)> dir(opendir("/proc/self/fd"), &closedir);
if(!dir)
return;
const int dirFd = dirfd(dir.get());
while(const dirent * entry = readdir(dir.get()))
{
char * end = nullptr;
const long fd = std::strtol(entry->d_name, &end, 10);
if(end == entry->d_name || *end != '\0' || fd < 3 || fd == dirFd)
continue;
const int flags = fcntl(static_cast<int>(fd), F_GETFD);
if(flags >= 0 && !(flags & FD_CLOEXEC))
fcntl(static_cast<int>(fd), F_SETFD, flags | FD_CLOEXEC);
}
}
25 changes: 25 additions & 0 deletions src/framework/close_on_exec.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#ifndef CLOSE_ON_EXEC_H_
#define CLOSE_ON_EXEC_H_

// call right before system()/popen()/fork()+exec: a spawned script holding /dev/fb0
// gets killed by uistop, which TERMs every process lsof shows on the framebuffer
void MarkFdsCloseOnExec();

#endif
Loading
Loading