feat: linux desktop preview and npm scripts

This commit is contained in:
Lpdev authored and GitHub committed 2026-10-17 20:09:36 +02:00
1 parent c742e20f89
commit bf3689a5ec
16 files changed
+352 -33

No files matched your search

+2
View File
@@ -3,6 +3,8 @@
.build/
dist/
node_modules/
/Overdrive.exfat
/update.zip
# Python
__pycache__/
+55 -11
View File
@@ -226,6 +226,52 @@ What a bundler throws away could not come back: most comments, the JSX syntax (i
</details>
<details>
<summary><b>Developing</b></summary>
<br>
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.
</details>
<details>
<summary><b>Building</b></summary>
<br>
@@ -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=<its bin directory>` 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
+30
View File
@@ -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"
}
}
+51
View File
@@ -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=<its bin directory> 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=<directory holding its clang and llvm-config>" >&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"
+50
View File
@@ -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."
+22
View File
@@ -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 <x> <y>` 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
+3 -3
View File
@@ -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 | — |
+3 -3
View File
@@ -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 |
+3
View File
@@ -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)
+56 -3
View File
@@ -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=<file> 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 <x> <y>` 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;
+42 -5
View File
@@ -8,14 +8,19 @@
#include "audio/mixer.hpp"
#include <dirent.h>
#include <mach/mach.h>
#include <spawn.h>
#include <sys/mount.h>
#include <sys/statvfs.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#ifdef __APPLE__
#include <mach/mach.h>
#include <sys/mount.h>
#include <sys/sysctl.h>
#else
#include <mntent.h>
#include <sys/sysinfo.h>
#endif
#include <cerrno>
#include <cstdio>
#include <cstdlib>
@@ -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<std::int64_t>(info.freeram + info.bufferram) * static_cast<std::int64_t>(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<host_info64_t>(&stats), &count) != KERN_SUCCESS)
return -1;
return static_cast<std::int64_t>(stats.free_count + stats.inactive_count) * static_cast<std::int64_t>(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<std::int64_t>(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<char*>(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;
+15
View File
@@ -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;
};
}
+6 -3
View File
@@ -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):
+4
View File
@@ -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 <key|@ref> move focus by focusKey or by a ref from the last snapshot
ps5_drive.py click <x> <y> a left mouse click at logical pixels: focuses the element
there and confirms it
ps5_drive.py shot <path.bmp> the frame on screen, 960×540
ps5_drive.py script "<steps>" 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 <dir>")
+3 -1
View File
@@ -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"])
+7 -4
View File
@@ -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"))