mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 02:00:06 +02:00
Add user-local desktop launcher and record Frame menu test
This commit is contained in:
1 parent
26113906f0
commit
417f81cb09
6 files changed
+122
-2
No files matched your search
@@ -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
|
Installation is user-local, retains rollback, refuses active-app upgrades and
|
||||||
foreign files, and does not launch or register automatically. Registration uses
|
foreign files, and does not launch or register automatically. Registration uses
|
||||||
OpenVR application key `local.frameyap.overlay`, not a Steam store AppID.
|
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`,
|
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
|
but **do not advertise or run it as a working public installation until a vetted
|
||||||
|
|||||||
@@ -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
@@ -89,7 +89,11 @@ input backend; default is `GAMESCOPE_WAYLAND_DISPLAY`, then `gamescope-0`.
|
|||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
||||||
Install root: `$XDG_DATA_HOME/frameyap` (default `~/.local/share/frameyap`);
|
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`.
|
passes a stable install-root lock identity and sets `PYTHONDONTWRITEBYTECODE=1`.
|
||||||
Runtime/check/registration modes and installer share an exclusive nonblocking
|
Runtime/check/registration modes and installer share an exclusive nonblocking
|
||||||
`.lock`; no upgrade/rollback/uninstall kills a running app or any other process.
|
`.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.
|
alone. Repeated registration/removal were exercised on Frame, autolaunch off.
|
||||||
Actual menu launch and cold-runtime behavior remain separate acceptance checks.
|
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
|
Before uninstall, explicitly `frameyap --unregister /absolute/manifest/path` and
|
||||||
verify success, then run `sh install.sh --uninstall --unregistered`. The latter is
|
verify success, then run `sh install.sh --uninstall --unregistered`. The latter is
|
||||||
an acknowledgement, not a hidden SteamVR edit. Only owned files are removed;
|
an acknowledgement, not a hidden SteamVR edit. Only owned files are removed;
|
||||||
|
|||||||
+20
@@ -230,6 +230,18 @@ def desired_manifest(launcher):
|
|||||||
"strings": {"en_us": {"name": "FrameYap"}}}]}
|
"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):
|
def check_owned_file(path, expected):
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
fail(f"refusing foreign symlink: {path}")
|
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()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
check_owned_file(launcher, desired_launcher(root))
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
|
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
||||||
return manifest, desired
|
return manifest, desired
|
||||||
|
|
||||||
|
|
||||||
def install_files(root, installed, launcher):
|
def install_files(root, installed, launcher):
|
||||||
owned_dir(launcher.parent)
|
owned_dir(launcher.parent)
|
||||||
manifest, desired = check_wrappers(root, launcher)
|
manifest, desired = check_wrappers(root, launcher)
|
||||||
|
desktop = desktop_path(root)
|
||||||
|
owned_dir(desktop.parent)
|
||||||
atomic_write(launcher, desired_launcher(root), 0o755)
|
atomic_write(launcher, desired_launcher(root), 0o755)
|
||||||
atomic_write(manifest, desired)
|
atomic_write(manifest, desired)
|
||||||
|
atomic_write(desktop, desired_desktop(launcher))
|
||||||
|
|
||||||
|
|
||||||
def receipt(path):
|
def receipt(path):
|
||||||
@@ -372,6 +388,8 @@ def uninstall(root, launcher):
|
|||||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
check_owned_file(launcher, desired_launcher(root))
|
||||||
|
desktop = desktop_path(root)
|
||||||
|
check_owned_file(desktop, desired_desktop(launcher))
|
||||||
versions = root / "versions"
|
versions = root / "versions"
|
||||||
if versions.exists():
|
if versions.exists():
|
||||||
if versions.is_symlink():
|
if versions.is_symlink():
|
||||||
@@ -398,6 +416,8 @@ def uninstall(root, launcher):
|
|||||||
launcher.unlink()
|
launcher.unlink()
|
||||||
if manifest.exists():
|
if manifest.exists():
|
||||||
manifest.unlink()
|
manifest.unlink()
|
||||||
|
if desktop.exists():
|
||||||
|
desktop.unlink()
|
||||||
print("FrameYap removed; config and saved models preserved. OpenVR unregister acknowledgement was required.")
|
print("FrameYap removed; config and saved models preserved. OpenVR unregister acknowledgement was required.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,18 @@ def desired_manifest(launcher):
|
|||||||
"strings": {"en_us": {"name": "FrameYap"}}}]}
|
"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):
|
def check_owned_file(path, expected):
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
fail(f"refusing foreign symlink: {path}")
|
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()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
check_owned_file(launcher, desired_launcher(root))
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
|
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
||||||
return manifest, desired
|
return manifest, desired
|
||||||
|
|
||||||
|
|
||||||
def install_files(root, installed, launcher):
|
def install_files(root, installed, launcher):
|
||||||
owned_dir(launcher.parent)
|
owned_dir(launcher.parent)
|
||||||
manifest, desired = check_wrappers(root, launcher)
|
manifest, desired = check_wrappers(root, launcher)
|
||||||
|
desktop = desktop_path(root)
|
||||||
|
owned_dir(desktop.parent)
|
||||||
atomic_write(launcher, desired_launcher(root), 0o755)
|
atomic_write(launcher, desired_launcher(root), 0o755)
|
||||||
atomic_write(manifest, desired)
|
atomic_write(manifest, desired)
|
||||||
|
atomic_write(desktop, desired_desktop(launcher))
|
||||||
|
|
||||||
|
|
||||||
def receipt(path):
|
def receipt(path):
|
||||||
@@ -361,6 +377,8 @@ def uninstall(root, launcher):
|
|||||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
check_owned_file(launcher, desired_launcher(root))
|
||||||
|
desktop = desktop_path(root)
|
||||||
|
check_owned_file(desktop, desired_desktop(launcher))
|
||||||
versions = root / "versions"
|
versions = root / "versions"
|
||||||
if versions.exists():
|
if versions.exists():
|
||||||
if versions.is_symlink():
|
if versions.is_symlink():
|
||||||
@@ -387,6 +405,8 @@ def uninstall(root, launcher):
|
|||||||
launcher.unlink()
|
launcher.unlink()
|
||||||
if manifest.exists():
|
if manifest.exists():
|
||||||
manifest.unlink()
|
manifest.unlink()
|
||||||
|
if desktop.exists():
|
||||||
|
desktop.unlink()
|
||||||
print("FrameYap removed; config and saved models preserved. OpenVR unregister acknowledgement was required.")
|
print("FrameYap removed; config and saved models preserved. OpenVR unregister acknowledgement was required.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"], str(launcher))
|
||||||
self.assertEqual(manifest["applications"][0]["binary_path_linux_arm"], str(launcher))
|
self.assertEqual(manifest["applications"][0]["binary_path_linux_arm"], str(launcher))
|
||||||
self.assertNotIn("binary_path", manifest["applications"][0])
|
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")
|
(root / "config-untouched").write_text("keep")
|
||||||
self.install("v1", a1, h1)
|
self.install("v1", a1, h1)
|
||||||
(self.stage / "bin/frameyap").write_text("next binary")
|
(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/v1/weights.bin").exists())
|
||||||
self.assertTrue((root / "saved-models/v2/weights.bin").exists())
|
self.assertTrue((root / "saved-models/v2/weights.bin").exists())
|
||||||
self.assertFalse(launcher.exists())
|
self.assertFalse(launcher.exists())
|
||||||
|
self.assertFalse(desktop.exists())
|
||||||
|
|
||||||
def test_digest_and_same_version_mismatch_leave_previous(self):
|
def test_digest_and_same_version_mismatch_leave_previous(self):
|
||||||
a, h = self.package("v1")
|
a, h = self.package("v1")
|
||||||
@@ -154,6 +158,20 @@ class InstallTests(unittest.TestCase):
|
|||||||
self.assertEqual(os.readlink(self.data / "frameyap/current"), "versions/v1")
|
self.assertEqual(os.readlink(self.data / "frameyap/current"), "versions/v1")
|
||||||
self.assertFalse((self.data / "frameyap/versions/v2").exists())
|
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):
|
def test_traversal_and_link_archives_rejected(self):
|
||||||
a, h = self.package("v1")
|
a, h = self.package("v1")
|
||||||
self.install("v1", a, h)
|
self.install("v1", a, h)
|
||||||
|
|||||||
Reference in new issue
Block a user