Add user-local desktop launcher and record Frame menu test

This commit is contained in:
baketnk committed 2026-09-24 12:11:43 -04:00
1 parent 26113906f0
commit 417f81cb09
6 files changed
+122 -2

No files matched your search

+4 -1
View File
@@ -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
+19
View File
@@ -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.
+41 -1
View File
@@ -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;
+20
View File
@@ -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.")
+20
View File
@@ -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.")
+18
View File
@@ -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)