diff --git a/.gitignore b/.gitignore
index f73cbc7..31d7fea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
.build/
dist/
node_modules/
+/Overdrive.exfat
+/update.zip
# Python
__pycache__/
diff --git a/README.md b/README.md
index 57780d2..87b69dc 100644
--- a/README.md
+++ b/README.md
@@ -226,6 +226,52 @@ What a bundler throws away could not come back: most comments, the JSX syntax (i
+
+Developing
+
+
+Everything runs from the repository root through npm. Once after cloning, `npm install` installs
+every dependency through its `postinstall` (`packaging/setup.sh`): on Linux the apt packages for the
+desktop preview, the PS5 build and the tests (it asks for `sudo` once), on macOS the Homebrew
+formulae; then Pillow and the framework's npm packages.
+
+```sh
+git clone git@github.com:inglbiwalid/overdrive.git
+cd overdrive
+npm install # once; asks for sudo on Linux
+npm run dev # the app in a window on your PC, rebuilt on every save under app/
+```
+
+| Command | What it does |
+| --- | --- |
+| `npm install` | Installs every dependency, once after cloning |
+| `npm run dev` | Opens the app in the desktop preview and rebuilds it on every save (state resets on restart) |
+| `npm run preview` | Opens the desktop preview once, without watching |
+| `npm test` | The framework's checks; needed after changes under `ps5-react/`, not for `app/` only |
+| `npm run build` | The PS5 app, then `Overdrive.exfat` and `update.zip` (see Building) |
+| `npm run build:app` | The PS5 app only, in `ps5-react/dist/PPSA99058` |
+
+The preview is the console's UI on your PC, so it takes a controller or the keyboard, and a mouse
+as a developer's shortcut. The first `npm run dev` downloads and compiles the framework's pinned
+dependencies, which takes a while; later starts are quick.
+
+| Action | Keyboard | Mouse | PS5 |
+| --- | --- | --- | --- |
+| Move focus | Arrow keys | Wheel (up/down) | D-pad |
+| Select | Enter or Space | Left click on the element | ✕ |
+| Back | Backspace | Right click | ○ |
+| Sections | Q / E | — | L1 / R1 |
+| Favorite | F | — | □ |
+| Quit the preview | Escape | — | PS button |
+
+The app's code is under `app/` (see "What's in this repository"); a change there needs no rebuild
+of the framework. Keep new user-facing strings in every file of `app/i18n/locales/`. On Linux the
+preview is compiled with clang (`CC=clang CXX=clang++`, set by the npm scripts), since GCC stops on
+a bounds warning in `archive_preflight.cpp`. Linux support for the preview is this fork's addition;
+upstream ps5-react supports macOS only.
+
+
+
Building
@@ -234,21 +280,19 @@ Builds run on `ubuntu-24.04` with clang 18, only from **Run workflow**: a push b
Each run puts `Overdrive.exfat` and `update.zip` in a draft release named after
`app/version.js`, which only maintainers can see.
-The same build locally, on any Linux with the packages the workflow installs (`clang-18`,
-`llvm-18`, `cmake`, `ninja-build`, `nodejs`, `npm`, `zlib1g-dev`, `build-essential`,
-`pkg-config`, `unzip`, `curl`, `exfatprogs`, and Pillow from `ps5-react/requirements.txt`):
+The same build locally, from the repository root, after `npm install` (see Developing):
```sh
-cd ps5-react
-python3 tools/build_ps5.py --app-dir ../app --llvm /usr/bin
-sudo ../packaging/make-exfat.sh dist/PPSA99058 ../Overdrive.exfat
+npm run build # the app, then Overdrive.exfat and update.zip here
+npm run build:app # the app only, in ps5-react/dist/PPSA99058
```
-The build downloads its pinned dependencies into `ps5-react/.deps` and writes the app folder, a
-`.zip` and a `.receipt.json` into `ps5-react/dist`. It verifies the result itself
-(`tools/verify.py`), so a green build means the payload structure and signatures check out.
-`--llvm /usr/bin` points it at the system clang; upstream defaults to the Homebrew LLVM path
-because macOS is its supported development environment.
+`packaging/build.sh` behind them finds LLVM 18 where apt or Homebrew put it (set
+`LLVM=` otherwise), builds through `ps5-react/tools/build_ps5.py`, then runs
+`packaging/make-exfat.sh` under `sudo`, since the image is mounted to be filled. The build
+downloads its pinned dependencies into `ps5-react/.deps` and writes the app folder, a `.zip` and a
+`.receipt.json` into `ps5-react/dist`. It verifies the result itself (`tools/verify.py`), so a
+green build means the payload structure and signatures check out.
The image is a plain exFAT volume whose root is the app folder, with 64 KiB clusters and the
label `OVERDRIVE`. To release a new version, raise the version in `app/version.js` and
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..dfeb862
--- /dev/null
+++ b/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "overdrive",
+ "private": true,
+ "description": "Overdrive, a PS5 game store. The scripts wrap the ps5-react build for this app.",
+ "//": [
+ "JSON has no comments; npm ignores this key.",
+ "",
+ "npm install Run once after cloning. Through postinstall it installs every dependency: the",
+ " apt or Homebrew packages for the preview, the PS5 build and the tests (asks for",
+ " sudo on Linux), Pillow, and the framework's npm packages. See packaging/setup.sh.",
+ "build Builds the PS5 app, then packages Overdrive.exfat and update.zip here, like the",
+ " GitHub workflow. Needs LLVM 18, cmake, ninja, nasm, exfatprogs; asks for sudo",
+ " to mount the image while filling it. See packaging/build.sh.",
+ "build:app The PS5 build only, into ps5-react/dist/PPSA99058. No sudo.",
+ "preview Opens the app in the desktop preview window on this PC. No PS5 toolchain needed.",
+ "dev Same window, rebuilt and restarted on every save under app/. State resets.",
+ "test The framework's checks; run after changing rendering or input in ps5-react/.",
+ "",
+ "CC=clang CXX=clang++: the preview is compiled with clang on every platform, since GCC",
+ "stops on a bounds warning in archive_preflight.cpp."
+ ],
+ "scripts": {
+ "postinstall": "bash packaging/setup.sh",
+ "build": "bash packaging/build.sh",
+ "build:app": "bash packaging/build.sh --no-package",
+ "preview": "cd ps5-react && CC=clang CXX=clang++ python3 tools/cli.py preview --app-dir ../app",
+ "dev": "cd ps5-react && CC=clang CXX=clang++ python3 tools/cli.py preview --app-dir ../app --watch",
+ "test": "cd ps5-react && CC=clang CXX=clang++ npm test"
+ }
+}
diff --git a/packaging/build.sh b/packaging/build.sh
new file mode 100755
index 0000000..4944a35
--- /dev/null
+++ b/packaging/build.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# Builds the PS5 app and packages it as Overdrive.exfat, the same steps as
+# .github/workflows/build.yml, on this machine. Run it through npm from the
+# repository root:
+#
+# npm run build build the app and package the image
+# npm run build:app build the app only (ps5-react/dist/PPSA99058)
+#
+# The build is pinned to LLVM 18. It is looked for where apt and Homebrew put
+# it; set LLVM= to point elsewhere. Packaging needs root
+# (make-exfat.sh mounts the image to fill it), so it asks through sudo.
+set -euo pipefail
+
+root="$(cd "$(dirname "$0")/.." && pwd)"
+package=1
+[ "${1:-}" = "--no-package" ] && package=0
+
+llvm="${LLVM:-}"
+if [ -z "$llvm" ]; then
+ for candidate in /usr/lib/llvm-18/bin /opt/homebrew/opt/llvm@18/bin /usr/local/opt/llvm@18/bin; do
+ if [ -x "$candidate/clang" ]; then llvm="$candidate"; break; fi
+ done
+fi
+if [ -z "$llvm" ] || [ ! -x "$llvm/clang" ]; then
+ echo "error: LLVM 18 not found. Install it (apt: clang-18 llvm-18 llvm-18-dev; brew: llvm@18)" >&2
+ echo " or set LLVM=" >&2
+ exit 1
+fi
+
+missing=()
+for tool in cmake ninja nasm curl unzip zip; do
+ command -v "$tool" >/dev/null || missing+=("$tool")
+done
+if [ "$package" = 1 ] && ! command -v mkfs.exfat >/dev/null; then missing+=("mkfs.exfat (exfatprogs)"); fi
+if [ "${#missing[@]}" -gt 0 ]; then
+ echo "error: missing tools: ${missing[*]}" >&2
+ echo " apt: cmake ninja-build nasm curl unzip zip exfatprogs" >&2
+ exit 1
+fi
+
+echo "→ building the PS5 app with LLVM at $llvm"
+(cd "$root/ps5-react" && python3 tools/build_ps5.py --app-dir "$root/app" --llvm "$llvm")
+[ "$package" = 1 ] || exit 0
+
+echo "→ packaging Overdrive.exfat (sudo: the image is mounted to fill it)"
+sudo bash "$root/packaging/make-exfat.sh" "$root/ps5-react/dist/PPSA99058" "$root/Overdrive.exfat" OVERDRIVE
+sudo chown "$(id -u):$(id -g)" "$root/Overdrive.exfat"
+# The in-app updater fetches one name, for an image install: the image alone at the root of update.zip.
+rm -f "$root/update.zip"
+zip -q -X -j "$root/update.zip" "$root/Overdrive.exfat"
+ls -l "$root/Overdrive.exfat" "$root/update.zip"
diff --git a/packaging/setup.sh b/packaging/setup.sh
new file mode 100755
index 0000000..d0b29eb
--- /dev/null
+++ b/packaging/setup.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+# Installs everything a fresh clone needs to preview, test and build Overdrive.
+# npm install runs it as postinstall, once, from the repository root:
+#
+# npm install
+#
+# Linux (apt): asks for sudo once to install the packages for the desktop
+# preview (SDL2, OpenGL, curl, libarchive, OpenSSL, clang), the PS5 build
+# (LLVM 18, ninja, nasm, exfatprogs) and the tests (bsdtar), then Pillow for
+# the asset baking and the framework's npm packages.
+# macOS (Homebrew): the same through brew and pip.
+set -euo pipefail
+
+root="$(cd "$(dirname "$0")/.." && pwd)"
+
+case "$(uname -s)" in
+Linux)
+ command -v apt-get >/dev/null || { echo "error: only apt-based Linux is scripted; see README 'Build it yourself' for the package list" >&2; exit 1; }
+ echo "→ apt packages (sudo)"
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential cmake clang ninja-build nasm pkg-config git curl unzip zip zstd \
+ libsdl2-dev libgl-dev libcurl4-openssl-dev libarchive-dev libarchive-tools liblzma-dev libssl-dev zlib1g-dev \
+ clang-18 llvm-18 llvm-18-dev exfatprogs \
+ nodejs npm python3 python3-pil
+ ;;
+Darwin)
+ command -v brew >/dev/null || { echo "error: Homebrew is needed: https://brew.sh" >&2; exit 1; }
+ echo "→ Homebrew formulae"
+ brew install cmake sdl2 llvm@18 nasm ninja curl libarchive xz openssl@3 python node
+ echo "→ Pillow"
+ python3 -m pip install -r "$root/ps5-react/requirements.txt"
+ ;;
+*)
+ echo "error: unsupported OS $(uname -s)" >&2
+ exit 1
+ ;;
+esac
+
+echo "→ framework npm packages"
+(cd "$root/ps5-react" && npm ci)
+
+echo "→ checking"
+python3 -c "import PIL; print('Pillow', PIL.__version__)"
+for tool in cmake clang ninja nasm node mkfs.exfat; do
+ printf '%-10s %s\n' "$tool" "$(command -v "$tool" || echo MISSING)"
+done
+echo
+echo "Ready. Next: npm run preview (desktop window), npm run build (Overdrive.exfat)."
+echo "The first preview and the first build download and compile the pinned dependencies; that takes a while."
diff --git a/ps5-react/CHANGELOG.md b/ps5-react/CHANGELOG.md
index 09db687..c661524 100644
--- a/ps5-react/CHANGELOG.md
+++ b/ps5-react/CHANGELOG.md
@@ -1,4 +1,26 @@
# Changelog
+## Unreleased — desktop preview on Linux, with a mouse
+
+- The desktop preview builds and runs on Linux: `sysinfo`, `/proc/self/mounts` and `xdg-open` stand
+ in for the Mach, BSD and Launch Services calls, FFmpeg and `rar-extract` take TLS and SHA-256 from
+ the system OpenSSL, and the JS stack guard is 4 MiB there. macOS keeps SecureTransport and
+ VideoToolbox; the PS5 build is unchanged. Build with `CC=clang CXX=clang++`: GCC rejects a
+ bounds warning in `archive_preflight.cpp`.
+- A mouse drives the preview as a developer's pointer: a left click focuses the smallest navigable
+ element under it and confirms, a right click is `back`, and the wheel is `up`/`down`.
+ `ps5_drive.py click ` does the same in the snapshot's logical pixels. The click goes
+ through `globalThis.__ps5ReactHit(nx, ny)`, a hit-test over the focus manager's nodes that takes
+ the point as fractions of the screen; the desktop host maps the window point through the frame's
+ letterbox first.
+- None of this reaches the console. The Linux stand-ins, the mouse handling and the `click` command
+ live in `native/desktop/` and `tools/`, which the PS5 build never compiles; `ffmpeg.ps5()` and the
+ PS5 `rar-extract` link line are untouched. The one shared file is the JS bundle: `__ps5ReactHit` is
+ defined only when the host sets `globalThis.__ps5ReactPointer` before the bundle loads, which the
+ desktop host does and the PS5 host does not, so on the console the hook is never created and
+ nothing calls it. Its source text (about 600 bytes of the 2.4 MB bundle) still ships, as the
+ existing `__ps5ReactInspect` and `__ps5ReactFocus` dev hooks do; stripping it would need a
+ target-aware bundle step, a change to the PS5 build path this entry deliberately avoids.
+
## Unreleased — far focus scrolls skip ahead
diff --git a/ps5-react/README.md b/ps5-react/README.md
index 4668479..ee78785 100644
--- a/ps5-react/README.md
+++ b/ps5-react/README.md
@@ -88,9 +88,9 @@ Refresh.** The first run fetches and builds pinned dependencies when needed.
| Action | Desktop preview | PS5 |
| --- | --- | --- |
-| Move focus | Arrow keys | D-pad |
-| Activate focused element | Enter | Cross |
-| Navigate back | Backspace | Circle |
+| Move focus | Arrow keys, mouse wheel | D-pad |
+| Activate focused element | Enter, left click on it | Cross |
+| Navigate back | Backspace, right click | Circle |
| Options (the `options` action) | O | Options |
| Exit application | Escape | PS button |
| Stop the development watcher | Ctrl+C in the terminal | — |
diff --git a/ps5-react/docs/NAVIGATION.md b/ps5-react/docs/NAVIGATION.md
index a307e46..03bf79f 100644
--- a/ps5-react/docs/NAVIGATION.md
+++ b/ps5-react/docs/NAVIGATION.md
@@ -93,9 +93,9 @@ frames.
| Action | Desktop | PS5 |
| --- | --- | --- |
-| `up`, `down`, `left`, `right` | Arrow keys | D-pad, left stick |
-| `confirm` | Enter | Cross (X) |
-| `back` | Backspace | Circle |
+| `up`, `down`, `left`, `right` | Arrow keys (`up`/`down` also the mouse wheel) | D-pad, left stick |
+| `confirm` | Enter, or a left click (it focuses the element under the pointer first) | Cross (X) |
+| `back` | Backspace, right click | Circle |
| `l1`, `r1` | Q, E | L1, R1 |
| `l2`, `r2` | Z, C | L2, R2 (past half travel on the desktop) |
| `triangle`, `square` | T, F | Triangle, Square |
diff --git a/ps5-react/native/desktop/CMakeLists.txt b/ps5-react/native/desktop/CMakeLists.txt
index 5806251..d3c5388 100644
--- a/ps5-react/native/desktop/CMakeLists.txt
+++ b/ps5-react/native/desktop/CMakeLists.txt
@@ -61,6 +61,9 @@ endforeach()
if(APPLE)
target_link_libraries(ps5-react-preview PRIVATE "-framework VideoToolbox" "-framework CoreFoundation" "-framework CoreMedia"
"-framework CoreVideo" "-framework CoreServices" "-framework Security")
+else()
+ # FFmpeg's https/tls protocols use the system OpenSSL on Linux.
+ target_link_libraries(ps5-react-preview PRIVATE OpenSSL::SSL OpenSSL::Crypto)
endif()
# tools/test_video.py: the player in real time on a short clip, with a stand-in mixer.
add_executable(ps5-react-video-test EXCLUDE_FROM_ALL ../../tools/tests/video_client.cpp ../shared/video_player.cpp ../shared/video_subtitles.cpp ../shared/audio_sink.cpp video_decoder.cpp)
diff --git a/ps5-react/native/desktop/host.cpp b/ps5-react/native/desktop/host.cpp
index 02327a0..096827f 100644
--- a/ps5-react/native/desktop/host.cpp
+++ b/ps5-react/native/desktop/host.cpp
@@ -242,17 +242,24 @@ struct Host {
cfg.screen_width = width; cfg.screen_height = height; cfg.screen_scale = 2;
cfg.memory_limit = kJsMemoryLimit;
cfg.malloc_functions = js_heap_functions();
+#ifdef __APPLE__
cfg.max_stack_size = 1024 * 1024;
+#else
+ // x86-64 Linux builds use deeper native frames per JS call; the main thread has 8 MiB.
+ cfg.max_stack_size = 4 * 1024 * 1024;
+#endif
cfg.install_host_globals = ps5_react_install_host_api;
runtime_started = er_runtime_init(&cfg);
if (!runtime_started) return false;
// PS5_REACT_PROFILE= samples JavaScript; the folded stacks are written there on exit.
if (std::getenv("PS5_REACT_PROFILE")) profiler.start(JS_GetRuntime(er_runtime_context()), 1000);
- if (controlled) {
- // Read when the bundle loads: Text reports its content to the inspector only then.
+ {
+ // Read when the bundle loads: the pointer hit-test exists only where a mouse can call it, and
+ // Text reports its content to the inspector only under the control channel.
JSContext* ctx = er_runtime_context();
JSValue global = JS_GetGlobalObject(ctx);
- JS_SetPropertyStr(ctx, global, "__ps5ReactInspecting", JS_TRUE);
+ JS_SetPropertyStr(ctx, global, "__ps5ReactPointer", JS_TRUE);
+ if (controlled) JS_SetPropertyStr(ctx, global, "__ps5ReactInspecting", JS_TRUE);
JS_FreeValue(ctx, global);
}
er_register_assets();
@@ -312,6 +319,39 @@ struct Host {
return call_input("__ps5ReactTouch", 2, args);
}
+ // A mouse click at window point (x, y): focuses the element under it, as a developer's pointer. The
+ // frame is letterboxed in the window, so the point maps through that scale and offset.
+ bool click(int x, int y) {
+ int ww = 0, wh = 0;
+ SDL_GetWindowSize(window, &ww, &wh);
+ if (ww <= 0 || wh <= 0) return false;
+ const float scale = std::min(float(ww) / width, float(wh) / height);
+ return hit((x - (ww - width * scale) / 2) / scale, (y - (wh - height * scale) / 2) / scale);
+ }
+
+ // Focuses the navigable element under logical point (fx, fy); whether one took focus.
+ bool hit(float fx, float fy) {
+ if (fx < 0 || fy < 0 || fx >= width || fy >= height) return false;
+ gc_scheduler.input(now_us());
+ JSContext* ctx = er_runtime_context();
+ JSValue global = JS_GetGlobalObject(ctx);
+ JSValue fn = JS_GetPropertyStr(ctx, global, "__ps5ReactHit");
+ // As fractions: the runtime's layout units need not be the render's pixels.
+ JSValue args[2] = {JS_NewFloat64(ctx, fx / width), JS_NewFloat64(ctx, fy / height)};
+ JSValue result = JS_Call(ctx, fn, global, 2, args);
+ const bool hit = !JS_IsException(result) && JS_ToBool(ctx, result) == 1;
+ if (JS_IsException(result)) {
+ JSValue error = JS_GetException(ctx);
+ const char* message = JS_ToCString(ctx, error);
+ std::fprintf(stderr, "Click failed: %s\n", message ? message : "JS exception");
+ if (message) JS_FreeCString(ctx, message);
+ JS_FreeValue(ctx, error);
+ }
+ JS_FreeValue(ctx, result); JS_FreeValue(ctx, args[0]); JS_FreeValue(ctx, args[1]);
+ JS_FreeValue(ctx, fn); JS_FreeValue(ctx, global);
+ return hit;
+ }
+
// A development hook's result as text: strings as they are, anything else through JSON.
std::string call_js(const char* name, const char* arg) {
JSContext* ctx = er_runtime_context();
@@ -369,6 +409,12 @@ struct Host {
Control::reply(client, call_js("__ps5ReactInspect", nullptr));
} else if (verb == "focus") {
Control::reply(client, call_js("__ps5ReactFocus", arg.c_str()) == "true" ? "ok" : "not found: " + arg);
+ } else if (verb == "click") {
+ // `click ` in logical pixels: a left mouse click there.
+ int x = 0, y = 0;
+ const bool found = std::sscanf(arg.c_str(), "%d %d", &x, &y) == 2 && hit(float(x), float(y));
+ if (found && !dispatch(action_name(HostAction::confirm))) return false;
+ Control::reply(client, found ? "ok" : "nothing at " + arg);
} else if (verb == "script") {
// Runs input-script steps from now on, e.g. a touchpad swipe.
char error[512] = {};
@@ -438,6 +484,13 @@ struct Host {
} else if ((event.type == SDL_CONTROLLERTOUCHPADDOWN || event.type == SDL_CONTROLLERTOUCHPADMOTION ||
event.type == SDL_CONTROLLERTOUCHPADUP) && event.ctouchpad.touchpad == 0 && event.ctouchpad.finger == 0) {
if (!touch(event.type != SDL_CONTROLLERTOUCHPADUP, event.ctouchpad.x, event.ctouchpad.y)) return false;
+ } else if (event.type == SDL_MOUSEBUTTONDOWN) {
+ // A developer's mouse stands in for the pad: left selects what is under it, right goes back.
+ if (event.button.button == SDL_BUTTON_LEFT) {
+ if (click(event.button.x, event.button.y)) action = action_name(HostAction::confirm);
+ } else if (event.button.button == SDL_BUTTON_RIGHT) action = action_name(HostAction::back);
+ } else if (event.type == SDL_MOUSEWHEEL && event.wheel.y != 0) {
+ action = action_name(event.wheel.y > 0 ? HostAction::up : HostAction::down);
} else if (event.type == SDL_CONTROLLERAXISMOTION) {
const auto trigger = [&](bool& down, const char* name) {
if (crossed(down, event.caxis.value / 32767.0f)) action = name;
diff --git a/ps5-react/native/desktop/host_platform.cpp b/ps5-react/native/desktop/host_platform.cpp
index cf51746..45641c7 100644
--- a/ps5-react/native/desktop/host_platform.cpp
+++ b/ps5-react/native/desktop/host_platform.cpp
@@ -8,14 +8,19 @@
#include "audio/mixer.hpp"
#include
-#include
#include
-#include
#include
#include
#include
-#include
#include
+#ifdef __APPLE__
+#include
+#include
+#include
+#else
+#include
+#include
+#endif
#include
#include
#include
@@ -26,8 +31,12 @@ extern char** environ;
namespace {
SDL_GameController* controller = nullptr;
-void sysctl_string(const char* name, char* out, std::size_t size) {
+void sysctl_string([[maybe_unused]] const char* name, char* out, [[maybe_unused]] std::size_t size) {
+#ifdef __APPLE__
if (sysctlbyname(name, out, &size, nullptr, 0) != 0) *out = '\0';
+#else
+ *out = '\0';
+#endif
}
// "pt_BR.UTF-8" → "pt-BR"; the C and POSIX locales name no language.
@@ -43,6 +52,11 @@ void environment_language(char* out, std::size_t size) {
}
std::int64_t free_memory() {
+#ifndef __APPLE__
+ struct sysinfo info;
+ if (sysinfo(&info) != 0) return -1;
+ return static_cast(info.freeram + info.bufferram) * static_cast(info.mem_unit);
+#else
vm_statistics64_data_t stats;
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
vm_size_t page = 0;
@@ -50,6 +64,7 @@ std::int64_t free_memory() {
host_statistics64(mach_host_self(), HOST_VM_INFO64, reinterpret_cast(&stats), &count) != KERN_SUCCESS)
return -1;
return static_cast(stats.free_count + stats.inactive_count) * static_cast(page);
+#endif
}
} // namespace
@@ -70,6 +85,21 @@ bool arrange_relaunch(char*, std::size_t) {
}
int list_mounts(MountEntry* mounts, int max) {
+#ifndef __APPLE__
+ FILE* table = setmntent("/proc/self/mounts", "r");
+ if (!table) return -1;
+ int used = 0;
+ while (used < max) {
+ const mntent* entry = getmntent(table);
+ if (!entry) break;
+ std::snprintf(mounts[used].device, sizeof mounts[used].device, "%s", entry->mnt_fsname);
+ std::snprintf(mounts[used].path, sizeof mounts[used].path, "%s", entry->mnt_dir);
+ std::snprintf(mounts[used].type, sizeof mounts[used].type, "%s", entry->mnt_type);
+ ++used;
+ }
+ endmntent(table);
+ return used;
+#else
struct statfs* found = nullptr;
const int count = getmntinfo(&found, MNT_NOWAIT);
if (count <= 0) return -1;
@@ -80,6 +110,7 @@ int list_mounts(MountEntry* mounts, int max) {
std::snprintf(mounts[i].type, sizeof mounts[i].type, "%s", found[i].f_fstypename);
}
return used;
+#endif
}
bool disk_usage(const char* real_path, double& total, double& free) {
@@ -120,11 +151,13 @@ void device_info(DeviceInfo& info) {
char version[24];
sysctl_string("kern.osproductversion", version, sizeof version);
if (*version) std::snprintf(info.firmware, sizeof info.firmware, "macOS %s", version);
+#ifdef __APPLE__
std::uint64_t frequency = 0;
std::size_t size = sizeof frequency;
// Apple silicon does not publish a CPU frequency; it stays unknown there.
if (sysctlbyname("hw.cpufrequency", &frequency, &size, nullptr, 0) == 0)
info.cpu_frequency = static_cast(frequency);
+#endif
info.free_memory = free_memory();
rusage usage;
if (getrusage(RUSAGE_SELF, &usage) == 0)
@@ -152,10 +185,14 @@ bool notify(const char* message, const char* sub_message) {
bool open_url(const char* url) {
if (std::strncmp(url, "http://", 7) && std::strncmp(url, "https://", 8)) return false;
+#ifdef __APPLE__
char open[] = "open";
+#else
+ char open[] = "xdg-open";
+#endif
char* argv[] = {open, const_cast(url), nullptr};
pid_t pid = 0;
- if (posix_spawnp(&pid, "open", nullptr, nullptr, argv, environ) != 0) return false;
+ if (posix_spawnp(&pid, open, nullptr, nullptr, argv, environ) != 0) return false;
int status = 0;
// `open` hands the URL to Launch Services and exits at once.
return waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0;
diff --git a/ps5-react/runtime/js/focus/inspect.js b/ps5-react/runtime/js/focus/inspect.js
index 611ded5..aa9cad1 100644
--- a/ps5-react/runtime/js/focus/inspect.js
+++ b/ps5-react/runtime/js/focus/inspect.js
@@ -41,4 +41,19 @@ export function installInspector(manager) {
};
}));
globalThis.__ps5ReactFocus = key => manager.focus(key);
+ // A pointer at (nx, ny), fractions of the screen: focuses the smallest navigable element under it,
+ // as a mouse click on the desktop preview would. Returns whether one took focus. Only the desktop
+ // host has a pointer (it sets the flag before the bundle loads); the PS5 gets no hook.
+ if (!globalThis.__ps5ReactPointer) return;
+ globalThis.__ps5ReactHit = (nx, ny) => {
+ const x = nx * screen.width, y = ny * screen.height;
+ let best = null, bestArea = Infinity;
+ for (const node of manager.nodes) {
+ const rect = screenRect(node);
+ if (!rect || inert(node.scope) || x < rect.x || y < rect.y || x >= rect.x + rect.width || y >= rect.y + rect.height) continue;
+ const area = rect.width * rect.height;
+ if (area < bestArea) { best = node; bestArea = area; }
+ }
+ return best ? manager.focus(best) : false;
+ };
}
diff --git a/ps5-react/tools/ffmpeg.py b/ps5-react/tools/ffmpeg.py
index 44977de..ddc885b 100644
--- a/ps5-react/tools/ffmpeg.py
+++ b/ps5-react/tools/ffmpeg.py
@@ -10,6 +10,7 @@ import hashlib
import os
from pathlib import Path
import shutil
+import sys
import tarfile
from common import DEPS, LOCK, ROOT, fetch, run
@@ -76,10 +77,12 @@ def _build(name, arguments, env=None):
def desktop():
- """macOS: TLS through SecureTransport, h264/hevc through VideoToolbox when the decoder asks."""
+ """macOS: TLS through SecureTransport, h264/hevc through VideoToolbox when the decoder asks.
+ Linux: TLS through the system OpenSSL, software decoding only."""
+ platform = (["--enable-securetransport", "--enable-videotoolbox", "--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox"]
+ if sys.platform == "darwin" else ["--enable-openssl"])
return _build("desktop", ["--cc=clang", "--enable-static", "--disable-shared", "--pkg-config=false",
- *COMPONENTS, "--enable-securetransport", "--enable-videotoolbox",
- "--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox"])
+ *COMPONENTS, *platform])
def ps5(sdk, network_ports, env):
diff --git a/ps5-react/tools/ps5_drive.py b/ps5-react/tools/ps5_drive.py
index 84e4099..9cfef24 100644
--- a/ps5-react/tools/ps5_drive.py
+++ b/ps5-react/tools/ps5_drive.py
@@ -23,6 +23,8 @@
hidden-layer elements; --json prints the raw records
ps5_drive.py diff what changed on screen since the last snapshot or diff
ps5_drive.py focus move focus by focusKey or by a ref from the last snapshot
+ ps5_drive.py click a left mouse click at logical pixels: focuses the element
+ there and confirms it
ps5_drive.py shot the frame on screen, 960×540
ps5_drive.py script "" input-script steps from now on (docs/ANIMATION.md), e.g.
"swipe:right:0.3" for a touchpad swipe
@@ -175,6 +177,8 @@ def main():
return diff()
if args[0] == "focus" and len(args) == 2:
return print(send(f"focus {resolve(args[1])}"))
+ if args[0] == "click" and len(args) == 3: # logical px, as snapshot prints them; the host takes render px
+ return print(send(f"click {round(float(args[1]) * SCALE)} {round(float(args[2]) * SCALE)}"))
print(send(" ".join(args)))
except OSError as error:
raise SystemExit(f"No preview on {SOCKET} ({error}); run: ps5_drive.py start --app-dir ")
diff --git a/ps5-react/tools/rar_worker.py b/ps5-react/tools/rar_worker.py
index 8c7f004..ece029c 100644
--- a/ps5-react/tools/rar_worker.py
+++ b/ps5-react/tools/rar_worker.py
@@ -6,6 +6,7 @@ program of its own. UnRAR's license allows its use to extract RAR archives but i
so it is never linked into the engine; the engine starts this program and reads its reports.
"""
import subprocess
+import sys
import tarfile
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
@@ -99,4 +100,5 @@ def build(output, compiler, flags=(), libraries=(), env=None):
def build_host(output):
"""rar-extract for the desktop preview and the archive tests."""
- return build(output, ["clang++"], ["-pthread"])
+ # macOS hashes with CommonCrypto; elsewhere the driver uses OpenSSL's EVP.
+ return build(output, ["clang++"], ["-pthread"], [] if sys.platform == "darwin" else ["-lcrypto"])
diff --git a/ps5-react/tools/test_archives.py b/ps5-react/tools/test_archives.py
index 8a12c5b..0ee1ac1 100644
--- a/ps5-react/tools/test_archives.py
+++ b/ps5-react/tools/test_archives.py
@@ -1,6 +1,6 @@
"""Real local archive extraction with the same native worker used on PS5."""
from pathlib import Path
-import binascii,hashlib,io,json,lzma,os,random,struct,subprocess,tarfile,tempfile,time,zipfile
+import binascii,hashlib,io,json,lzma,os,random,struct,subprocess,sys,tarfile,tempfile,time,zipfile
from rar_worker import build_host
ROOT=Path(__file__).resolve().parents[1]
DATA=bytes(range(256))*1024
@@ -116,9 +116,12 @@ def uudecode(source,destination):
def main():
with tempfile.TemporaryDirectory(prefix="ps5-react-archives-") as tmp:
root=Path(tmp);binary=root/"archive-client"
- prefix=subprocess.check_output(["brew","--prefix","libarchive"],text=True).strip()
- xz=subprocess.check_output(["brew","--prefix","xz"],text=True).strip()
- crypto=subprocess.check_output(["brew","--prefix","openssl@3"],text=True).strip()
+ # Homebrew keeps these keg-only on macOS; Linux distributions install them under /usr.
+ if sys.platform=="darwin":
+ prefix=subprocess.check_output(["brew","--prefix","libarchive"],text=True).strip()
+ xz=subprocess.check_output(["brew","--prefix","xz"],text=True).strip()
+ crypto=subprocess.check_output(["brew","--prefix","openssl@3"],text=True).strip()
+ else: prefix=xz=crypto="/usr"
sources=[ROOT/"tools/tests/archive_client.cpp",ROOT/"native/shared/archives.cpp",ROOT/"native/shared/archive_preflight.cpp",ROOT/"native/shared/rar5_password.cpp",ROOT/"native/shared/archive_writer.cpp",ROOT/"native/desktop/rar_worker_process.cpp"]
# RAR volumes go to rar-extract, as in the app; the other formats stay with libarchive.
os.environ["PS5_REACT_RAR_WORKER"]=str(build_host(root/"rar-extract"))