From 417f81cb09d11882729894003ef3947ff5ed9e98 Mon Sep 17 00:00:00 2001 From: baketnk Date: Thu, 24 Sep 2026 12:11:43 -0400 Subject: [PATCH] Add user-local desktop launcher and record Frame menu test --- README.md | 5 ++++- TODO.md | 19 +++++++++++++++++ docs/packaging.md | 42 +++++++++++++++++++++++++++++++++++++- install.sh | 20 ++++++++++++++++++ scripts/install_payload.py | 20 ++++++++++++++++++ tests/test_installer.py | 18 ++++++++++++++++ 6 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 TODO.md diff --git a/README.md b/README.md index b3b705f..3c18003 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,10 @@ This is the **local-artifact command shape**, not an available public download. Installation is user-local, retains rollback, refuses active-app upgrades and foreign files, and does not launch or register automatically. Registration uses OpenVR application key `local.frameyap.overlay`, not a Steam store AppID. -Autolaunch is opt-in. See [packaging and lifecycle](docs/packaging.md). +Autolaunch is opt-in. An installed desktop entry can be selected manually as a +non-Steam shortcut. A basic launch from Steam's Non-Steam section opened the +panel on one Frame; registration alone did not show an entry in the first checked +dashboard menu. See [packaging and lifecycle](docs/packaging.md). A pinned GitHub one-command route is implemented in `install.sh --version TAG`, but **do not advertise or run it as a working public installation until a vetted diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..500c246 --- /dev/null +++ b/TODO.md @@ -0,0 +1,19 @@ +# FrameYap TODO + +- [ ] Resolve the Redux inference runtime license before further inference or a + bundled release. Obtain written permission covering use and redistribution of + M87 Labs Kestrel kernels (including the CPU payload), or scope an independently + licensed runtime for the same weights. Inventory the other runtime dependencies' + notices/licenses before publishing. The CC-BY-4.0 model weights do not grant + permission to use or redistribute the proprietary runtime. See + [third-party notes](docs/third-party.md). +- [x] Validate a basic menu launch on Frame: OpenVR registration did **not** show + FrameYap in the first checked dashboard menu; the user found FrameYap in Steam's + **Non-Steam** section and reported that selecting it showed a panel and Quit + worked. The server log showed a `--run` overlay connection followed by exit; + no FrameYap process remained. The installed native-only package had no bundled + runtime or model. This does not validate transcription, recording, text delivery, + cold startup, or which shortcut discovery mechanism Steam used. +- [ ] Verify the exact missing-runtime panel message and Steam's shortcut + persistence across a normal restart (without restarting sessions just for the + test). Confirm no runtime/model environment override before future live checks. diff --git a/docs/packaging.md b/docs/packaging.md index c7f510e..6c283a5 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -89,7 +89,11 @@ input backend; default is `GAMESCOPE_WAYLAND_DISPLAY`, then `gamescope-0`. ## Lifecycle Install root: `$XDG_DATA_HOME/frameyap` (default `~/.local/share/frameyap`); -launcher: `~/.local/bin/frameyap`; existing config is untouched. The launcher +launcher: `~/.local/bin/frameyap`; desktop entry: +`$XDG_DATA_HOME/applications/frameyap.desktop` (default +`~/.local/share/applications/frameyap.desktop`). The desktop entry points to the +user-local launcher; the installer never edits Steam's library or registers a +Steam shortcut. Existing config is untouched. The launcher passes a stable install-root lock identity and sets `PYTHONDONTWRITEBYTECODE=1`. Runtime/check/registration modes and installer share an exclusive nonblocking `.lock`; no upgrade/rollback/uninstall kills a running app or any other process. @@ -113,6 +117,42 @@ Registration checks actual OpenVR installed state rather than trusting Add's ret alone. Repeated registration/removal were exercised on Frame, autolaunch off. Actual menu launch and cold-runtime behavior remain separate acceptance checks. +## SteamVR dashboard launcher check (opt-in on Frame) + +Registration creates an **OpenVR app entry**, not a Steam store/library shortcut. +On one Frame, registration succeeded with autolaunch off but **no FrameYap entry +was visible in the first checked dashboard menu**. The user then found FrameYap +under Steam's **Non-Steam** section and reported that selecting it opened the +panel and Quit closed it. The VR server logged a `--run` overlay connection and +exit; no owned process remained. This establishes a basic menu-driven launch on +that device, **not** that OpenVR registration alone creates a Steam shortcut or +that the new desktop entry was the sole discovery path. The manifest points to +`~/.local/bin/frameyap`, which starts `--run` if launched. A native-only install +without a model or authorized runtime should show an **Unavailable** overlay +rather than record or run inference. Only perform this check on that native-only +installation: verify that `current/runtime/bin/python3` and `current/model` are +absent first, and do not press Record, Insert or Enter. +The installer also provides a desktop entry; where Steam's UI supports adding a +non-Steam app, the user may select that entry or browse to the installed launcher. +Shortcut discovery/persistence after a normal restart is not yet verified. Do +not hand-edit Steam's shortcut database. + +1. With SteamVR running, register using the command above (do not pass + `--autostart`). Leave any other apps and sessions alone. +2. In the headset, check Steam's **Non-Steam** section or the **+** application + picker for FrameYap. If absent, stop and report the menu checked; registration + alone does not add a Steam library shortcut. If present, select it, check the + FrameYap panel's status, and use **Quit** to close. +3. Report separately whether the menu entry was visible, the click started an + app, the panel was visible, and Quit worked. A registration success or CLI + `--check-overlay` success alone does **not** establish menu-driven launch. + +This check does not validate microphone capture, transcription, controller input, +text delivery or cold SteamVR startup. With a configured inference runtime, the +menu launch attempts to load the model; defer that test until runtime licensing is +resolved. If an environment/configuration unexpectedly supplies a runtime/model, +do not perform this check. + Before uninstall, explicitly `frameyap --unregister /absolute/manifest/path` and verify success, then run `sh install.sh --uninstall --unregistered`. The latter is an acknowledgement, not a hidden SteamVR edit. Only owned files are removed; diff --git a/install.sh b/install.sh index 8e07e03..afacd7f 100755 --- a/install.sh +++ b/install.sh @@ -230,6 +230,18 @@ def desired_manifest(launcher): "strings": {"en_us": {"name": "FrameYap"}}}]} +def desktop_path(root): + return root.parent / "applications/frameyap.desktop" + + +def desired_desktop(launcher): + # Desktop Entry Exec quotes are not shell quotes; escape reserved characters. + executable = (str(launcher).replace("\\", "\\\\").replace('"', '\\"') + .replace("$", "\\$").replace("`", "\\`").replace("%", "%%")) + return ("[Desktop Entry]\nType=Application\nName=FrameYap\n" + f'Exec="{executable}"\nTerminal=false\nCategories=Utility;\n').encode() + + def check_owned_file(path, expected): if path.is_symlink(): fail(f"refusing foreign symlink: {path}") @@ -246,14 +258,18 @@ def check_wrappers(root, launcher): desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode() check_owned_file(launcher, desired_launcher(root)) check_owned_file(manifest, desired) + check_owned_file(desktop_path(root), desired_desktop(launcher)) return manifest, desired def install_files(root, installed, launcher): owned_dir(launcher.parent) manifest, desired = check_wrappers(root, launcher) + desktop = desktop_path(root) + owned_dir(desktop.parent) atomic_write(launcher, desired_launcher(root), 0o755) atomic_write(manifest, desired) + atomic_write(desktop, desired_desktop(launcher)) def receipt(path): @@ -372,6 +388,8 @@ def uninstall(root, launcher): desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode() check_owned_file(manifest, desired) check_owned_file(launcher, desired_launcher(root)) + desktop = desktop_path(root) + check_owned_file(desktop, desired_desktop(launcher)) versions = root / "versions" if versions.exists(): if versions.is_symlink(): @@ -398,6 +416,8 @@ def uninstall(root, launcher): launcher.unlink() if manifest.exists(): manifest.unlink() + if desktop.exists(): + desktop.unlink() print("FrameYap removed; config and saved models preserved. OpenVR unregister acknowledgement was required.") diff --git a/scripts/install_payload.py b/scripts/install_payload.py index 3d38b89..8c2ec45 100644 --- a/scripts/install_payload.py +++ b/scripts/install_payload.py @@ -219,6 +219,18 @@ def desired_manifest(launcher): "strings": {"en_us": {"name": "FrameYap"}}}]} +def desktop_path(root): + return root.parent / "applications/frameyap.desktop" + + +def desired_desktop(launcher): + # Desktop Entry Exec quotes are not shell quotes; escape reserved characters. + executable = (str(launcher).replace("\\", "\\\\").replace('"', '\\"') + .replace("$", "\\$").replace("`", "\\`").replace("%", "%%")) + return ("[Desktop Entry]\nType=Application\nName=FrameYap\n" + f'Exec="{executable}"\nTerminal=false\nCategories=Utility;\n').encode() + + def check_owned_file(path, expected): if path.is_symlink(): fail(f"refusing foreign symlink: {path}") @@ -235,14 +247,18 @@ def check_wrappers(root, launcher): desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode() check_owned_file(launcher, desired_launcher(root)) check_owned_file(manifest, desired) + check_owned_file(desktop_path(root), desired_desktop(launcher)) return manifest, desired def install_files(root, installed, launcher): owned_dir(launcher.parent) manifest, desired = check_wrappers(root, launcher) + desktop = desktop_path(root) + owned_dir(desktop.parent) atomic_write(launcher, desired_launcher(root), 0o755) atomic_write(manifest, desired) + atomic_write(desktop, desired_desktop(launcher)) def receipt(path): @@ -361,6 +377,8 @@ def uninstall(root, launcher): desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode() check_owned_file(manifest, desired) check_owned_file(launcher, desired_launcher(root)) + desktop = desktop_path(root) + check_owned_file(desktop, desired_desktop(launcher)) versions = root / "versions" if versions.exists(): if versions.is_symlink(): @@ -387,6 +405,8 @@ def uninstall(root, launcher): launcher.unlink() if manifest.exists(): manifest.unlink() + if desktop.exists(): + desktop.unlink() print("FrameYap removed; config and saved models preserved. OpenVR unregister acknowledgement was required.") diff --git a/tests/test_installer.py b/tests/test_installer.py index 0c9d3d8..7311ec3 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -77,6 +77,9 @@ class InstallTests(unittest.TestCase): self.assertEqual(manifest["applications"][0]["binary_path_linux"], str(launcher)) self.assertEqual(manifest["applications"][0]["binary_path_linux_arm"], str(launcher)) self.assertNotIn("binary_path", manifest["applications"][0]) + desktop = self.data / "applications/frameyap.desktop" + self.assertIn(f'Exec="{launcher}"', desktop.read_text()) + self.assertIn("Terminal=false", desktop.read_text()) (root / "config-untouched").write_text("keep") self.install("v1", a1, h1) (self.stage / "bin/frameyap").write_text("next binary") @@ -101,6 +104,7 @@ class InstallTests(unittest.TestCase): self.assertTrue((root / "saved-models/v1/weights.bin").exists()) self.assertTrue((root / "saved-models/v2/weights.bin").exists()) self.assertFalse(launcher.exists()) + self.assertFalse(desktop.exists()) def test_digest_and_same_version_mismatch_leave_previous(self): a, h = self.package("v1") @@ -154,6 +158,20 @@ class InstallTests(unittest.TestCase): self.assertEqual(os.readlink(self.data / "frameyap/current"), "versions/v1") self.assertFalse((self.data / "frameyap/versions/v2").exists()) + def test_foreign_desktop_entry_is_preserved(self): + a, h = self.package("v1") + desktop = self.data / "applications/frameyap.desktop" + desktop.parent.mkdir(parents=True) + desktop.write_text("foreign shortcut\n") + with self.assertRaisesRegex(ValueError, "foreign file"): + self.install("v1", a, h) + self.assertEqual(desktop.read_text(), "foreign shortcut\n") + self.assertFalse((self.data / "frameyap/current").exists()) + + def test_desktop_exec_escapes_field_codes(self): + entry = installer.desired_desktop(Path('/home/steam%user/.local/bin/frameyap')).decode() + self.assertIn('Exec="/home/steam%%user/.local/bin/frameyap"', entry) + def test_traversal_and_link_archives_rejected(self): a, h = self.package("v1") self.install("v1", a, h)