From 72352c59149f110a39c3a02d741adcc401b86512 Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:33:23 +1000 Subject: [PATCH] Add companion and headset panel switchers with feasibility evidence --- docs/img/panel-switcher.png | Bin 0 -> 74333 bytes docs/mac-in-headset.md | 15 +++ docs/panels.md | 137 +++++++++++++++++++ docs/testing.md | 17 +++ tests/test_panels.py | 102 ++++++++++++++ ui/frame_panels.py | 261 ++++++++++++++++++++++++++++++++++++ ui/index.html | 34 ++++- ui/server.py | 29 +++- 8 files changed, 592 insertions(+), 3 deletions(-) create mode 100644 docs/img/panel-switcher.png create mode 100644 tests/test_panels.py create mode 100644 ui/frame_panels.py diff --git a/docs/img/panel-switcher.png b/docs/img/panel-switcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ce02452be2cae47410c74a4d64f464b67564e91c GIT binary patch literal 74333 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 P-!J}p>6cQ2>%adGqyL{8 literal 0 HcmV?d00001 diff --git a/docs/mac-in-headset.md b/docs/mac-in-headset.md index 590a831..ed1b90a 100644 --- a/docs/mac-in-headset.md +++ b/docs/mac-in-headset.md @@ -468,3 +468,18 @@ versus on, medians of the runs, ms): window to the front first. - Ctrl stays Ctrl. On the Mac, copy is ⌘C, so use Meta+C on a keyboard paired with the Frame. + +## Switching panels and workspace limits + +**Tools → Panel switcher** lists open SteamVR panels, including Mac viewers. +Use **Show** to request focus or **Open in headset** for Frame Control's own +switcher panel. It uses SteamVR/gamescope and Chromium, with no third-party +overlay app. [Device checks and limits](panels.md#frame-controls-panel-switcher) +include the difference between a panel surviving a scene launch and staying +visible over it. + +Saved spatial layouts are blocked on this build: the public OpenVR transform +setter denies access to gamescope-owned panels. Reconnecting an existing viewer +is supported; restoring its room position after a reboot is not. We do not +save short-lived Mac window IDs or viewer access keys as if they were a durable +workspace. See [the feasibility evidence](panels.md#saved-spatial-layouts-blocked-on-the-current-panel-route). diff --git a/docs/panels.md b/docs/panels.md index b08720b..d9c47f7 100644 --- a/docs/panels.md +++ b/docs/panels.md @@ -123,3 +123,140 @@ a limit on the number of floating panels. keyboard) or virtual desktops arrange windows within the 1280×800 rectangle. - **Windows-only overlay tools** (Desktop+, OVR Toolkit, OVRdrop) do this for a PC's desktop in SteamVR. They don't run on the Frame's standalone Linux. + +## Frame Control's panel switcher + +**Verified 2026-09-28**, SteamOS 0.4.1, BUILD_ID `20260925.6191901`, +SteamVR 2.18.1: **Tools → Panel switcher** lists SteamVR's open main panels, +including panels that are currently hidden. **Show** asks SteamVR to bring one +forward. **Open in headset** opens the same switcher as its own panel; choose +it again from Steam's dashboard after switching away. Refresh updates the list. +This is a list, not thumbnail Exposé. + +![Frame Control's switcher rendered on the Frame](img/panel-switcher.png) + +This is our own Python/HTML implementation (`ui/frame_panels.py`), using the +Frame's shipped `vrcmd` OpenVR client and gamescope. The headset page uses +Chromium (Chromium XR when present, then system Chromium, then the existing +Chromium Flatpak). No XSOverlay, OVR Toolkit, WayVR or other overlay application +is needed. This dependency boundary also applies to future layout and panel +persistence work: platform APIs and bundled libraries are fine; another app +must not implement the feature for us. + +The companion runs the helper over SSH. Opening it in the headset installs a +copy under `~/.local/share/frame-control/panels/` and starts a loopback HTTP +server and an isolated Chromium profile. There is no startup service or global +setting change. Close the switcher to stop its server and browser. Other +Chromium profiles, Steam and SteamVR are left alone. If the window or runtime +closes, use **Open in headset** again. + +The page carries a random, per-process access key in its URL fragment, removes +it from the address bar, keeps it in tab session storage for page reloads, and +sends it in a header. Panel lists and actions need +that key; Host and Origin checks reject other sites. The key permits only +listing panels, requesting focus and closing this switcher. Like Mac viewer +launch tickets, it is initially readable by another process running as the +same Frame user. Panel titles are rendered as text, never HTML. The companion +retains its existing request guards. No Mac capture credentials cross this API. + +**Verified:** the real headset page rendered its panel list (image above), its +HTTP focus request changed `GAMESCOPE_FOCUSED_APP` to `2000999030`, a request +without the key returned HTTP 403, and Close stopped the helper and its browser. +Opening an already running switcher requests its focus rather than creating a +second one. The companion uses the same list/focus helper. **Unverified:** laser +selection while wearing the headset, physical placement, and non-XR Chromium. +The API reports that focus was *requested*: another action can take focus before +we observe the result. Closed panels are rejected after re-enumeration. + +## Saved spatial layouts: blocked on the current panel route + +**Verified 2026-09-28**, same build, using a temporary xterm panel with +`STEAM_GAME=2000999031` and `FnTable:IVROverlay_028` from +`/opt/steamvr/bin/linuxarm64/libopenvr_api.so`: + +| OpenVR call | Result | +|---|---| +| `FindOverlay("valve.steam.desktopgame.2000999031")` | Success | +| `GetOverlayWidthInMeters` | Success, 2.67 m | +| `SetOverlayWidthInMeters` (same width) | Success | +| `GetOverlayTransformType` | Success, type 5 (`VROverlayTransform_DashboardTab`) | +| `GetOverlayTransformAbsolute` | 18 (`WrongTransformType`) | +| `SetOverlayTransformAbsolute` (identity rotation, 1.2 m up, 1.5 m forward) | 12 (`PermissionDenied`); type remained 5 | + +The public interface names type 5 **DashboardTab**; SteamVR's dashboard code +places these panels through its scene graph. It owns the frame/docking +transforms. A successful width setter does not grant permission to restore the +position. `vrcmd --dock-overlay world ` dispatched a docking request but +the dashboard logged `Failed to get SGTransform in setInitialTransformForLocation. +Invalid transform ID`. This does not establish working world placement. + +**Inferred:** saving X11 pixel rectangles or Mac window IDs would not restore +this spatial arrangement. Mac window IDs also change when an application +reopens; viewer tickets and reconnect keys must not go into a layout file. +The base Mac stream reconnects after a network break, but that is different +from recreating windows and their room positions after a reboot. + +There is consequently no Save/Restore control yet. A durable layout needs a +working transform restore path, stable source identity, and a fresh capture +permission/ticket flow. The tested gamescope-owned overlay route denies that +transform operation. A future Frame Control-owned overlay renderer, or a +supported platform API for dashboard frame transforms, needs its own device +proof before building layout UI. This is a blocker for the current approach, +not a claim that all possible implementations are impossible. Reboot recovery +was not tested: the shared headset was not rebooted. + +## Panels during an immersive session + +**Verified 2026-09-28**, same build: our Chromium switcher panel remained in +OpenVR's overlay list before, during and after the Frame's shipped `helloxr -g +Vulkan` sample. During the test `vrcmd --stats` identified +`system.generated.openxr.helloxr.helloxr`, with 242 frame submissions. The test +ended only its own sample process; no SteamVR, Steam, power or global settings +were changed. The switcher was still selectable afterwards. + +This proves survival of that panel across an OpenXR scene session, **not** that +it stayed visibly composited over the scene: OpenVR reported it `not_visible` +before, during and after. **Verified:** calling `ShowOverlay` on our +*gamescope-owned* switcher overlay returns 12 (`PermissionDenied`). A helper +cannot force that panel visible using the public overlay call. Use the +switcher/dashboard to request access to it; we do not fight the runtime with a +repeated force-focus loop. + +**Verified in a second controlled run:** a live H.264 test-pattern stream from +this checkout's Mac helper, through its own SSH tunnel and a temporary Chromium +profile, survived the same OpenXR sample (257 scene-frame submissions). Its +panel `2000999032` changed from `visible` before launch to `not_visible` during +and after the scene. The Mac helper still reported the same `test` stream; +captured frames increased from 35 to 232, with 29.5 decoded/drawn fps afterwards. +The test did not capture personal Mac windows or inject Mac input. The sample, +viewer, temporary profile, tunnel and Mac helper were cleaned up. This proves +stream survival, and also shows why it must not be advertised as always visible. + +**Unverified:** persistent visible placement while playing a Steam-launched VR +game, Plasma desktop and real Mac-window behavior during that launch, and worn +headset input. Other threads were launching games and changing the runtime on +the shared device, so those transitions were not treated as controlled evidence. +A runtime/X-server restart can destroy the viewer windows; a network reconnect +cannot recreate them. No “always visible during games” guarantee is shipped. + +## Keyboard passthrough feasibility + +**Verified 2026-09-28**, same build, using `FnTable:IVRTrackedCamera_006`: +`HasCamera(0)` returned success and true. `GetCameraFrameSize` returned 100 +(`OperationFailed`), with zero dimensions, for all three public frame types +(distorted, undistorted and maximum-undistorted), including after acquiring the +video service. Acquisition returned success and a handle; release returned 101 +(`InvalidHandle`). The probe shut down its OpenVR client afterwards. No camera +frames were captured and no camera settings were changed. + +**Documented:** the public OpenVR camera interface provides camera frame sizes, +intrinsics, projections and streaming handles; these are prerequisites for a +spatially aligned camera cutout. See Valve's +[OpenVR C API](https://github.com/ValveSoftware/openvr/blob/master/headers/openvr_capi.h). + +**Inferred:** camera presence alone does not establish access to camera pixels. +The failed frame-size path blocks a keyboard cutout in our current panel +implementation. We have not established a keyboard detector or a calibrated +camera-to-panel mapping. Built-in full-room passthrough is not proof of a +public, selectively masked camera stream. No keyboard cutout is offered, and +no third-party camera/overlay app is substituted for it. diff --git a/docs/testing.md b/docs/testing.md index b06577d..a36b7b7 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -148,3 +148,20 @@ For example, on 2026-09-27 the smoke test found that Steam's `create-shortcut` refuses ids with a hyphen (`missing/invalid arguments`), which the fake had accepted. The fake now refuses them the same way, and Frame Control makes ids Steam accepts. + +## Panel switcher + +`tests/test_panels.py` supplies fake-Frame `vrcmd --overlays` output, checks +main-panel filtering (including hidden panels), revalidates closed panels before +focus, and drives the headset helper's real loopback HTTP server to test access +keys, Host/Origin guards, malformed requests, offline errors and Close. It runs +in the normal unit suite without OpenVR or a headset. The fixture format comes +from SteamVR 2.18.1, BUILD_ID `20260925.6191901`; it does not simulate rendering. + +On the Frame, run `python3 -` over SSH with `ui/frame_panels.py` on stdin to +list panels. `--focus ` rechecks the list and requests focus. In Frame +Control, **Tools → Panel switcher → Open in headset** exercises installation, +Chromium rendering and the same helper through HTTP. Close the switcher after +testing. [The recorded device checks](panels.md#frame-controls-panel-switcher) +cover actual focus, HTTP guards and an OpenXR sample transition, and separately +identify the unverified Steam-game, spatial layout, reboot and laser behaviors. diff --git a/tests/test_panels.py b/tests/test_panels.py new file mode 100644 index 0000000..6cfb241 --- /dev/null +++ b/tests/test_panels.py @@ -0,0 +1,102 @@ +"""Fake-Frame panel responses and the headset page's real HTTP guards.""" +import http.client +import json +from pathlib import Path +import sys +import threading +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'ui')) +import frame_panels as panels + +# Shape verified with vrcmd on SteamVR 2.18.1 / BUILD_ID 20260925.6191901. +OVERLAYS = """---- OVERLAYS ---- +'valve.steam.desktopgame.12' -- 'Alex's ', 1920x1080 visible VROverlayType_Dashboard_Main +'valve.steam.desktopgame.12.thumb' -- 'Thumb', not_visible VROverlayType_Dashboard_Thumbnail +'valve.steam.desktopgame.12.layer1' -- 'Layer', visible VROverlayType_Subview +'system.pointer' -- 'Pointer', visible VROverlayType_Basic +'valve.steam.desktopgame.13' -- 'Other', not_visible VROverlayType_Dashboard_Main +""" + + +class Panels(unittest.TestCase): + def test_enumerates_only_main_panels_including_hidden(self): + rows = panels.parse_overlays(OVERLAYS) + self.assertEqual(len(rows), 2) + self.assertEqual(rows[0]['title'], "Alex's ") + self.assertTrue(rows[0]['visible']) + self.assertFalse(rows[1]['visible']) + + def test_unavailable_runtime_is_not_an_empty_workspace(self): + with self.assertRaises(panels.PanelError): + panels.parse_overlays('SteamVR not running') + self.assertEqual(panels.parse_overlays('---- OVERLAYS ----'), []) + + @patch.object(panels, 'run', return_value=OVERLAYS) + def test_focus_revalidates_and_dispatches_without_shell(self, run): + key = 'valve.steam.desktopgame.12' + self.assertEqual(panels.focus(key)['requested'], key) + self.assertEqual(run.call_args.args[0], [panels.VRCMD, '--showdashboard', key]) + + @patch.object(panels, 'run', return_value=OVERLAYS) + def test_closed_or_injected_panel_never_dispatches(self, run): + for key in (None, 12, 'x;touch /tmp/bad', '../other', 'missing'): + with self.assertRaises(panels.PanelError): + panels.focus(key) + self.assertEqual(run.call_count, 1) # only valid-looking 'missing' enumerates + + +class HeadsetHTTP(unittest.TestCase): + def setUp(self): + self.server = panels.HTTPServer(('127.0.0.1', 0), panels.Handler) + self.server.key = 'test-key' + self.server.closing = False + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.thread.start() + + def tearDown(self): + self.server.shutdown() + self.thread.join() + self.server.server_close() + + def request(self, path, body=None, headers=None): + c = http.client.HTTPConnection('127.0.0.1', self.server.server_port, timeout=5) + try: + c.request('POST' if body is not None else 'GET', path, body=body, headers=headers or {}) + r = c.getresponse() + return r.status, r.read().decode() + finally: + c.close() + + def test_page_is_public_but_contains_no_key_or_private_titles(self): + status, page = self.request('/') + self.assertEqual(status, 200) + self.assertNotIn('test-key', page) + self.assertIn('textContent=p.title', page) # titles never become HTML + self.assertEqual(self.request('/panels')[0], 403) + + @patch.object(panels, 'state', return_value={'panels': []}) + def test_auth_host_and_origin_checks(self, state): + auth = {'X-Panel-Key': 'test-key'} + self.assertEqual(self.request('/panels', headers=auth)[0], 200) + for extra in ({'Host': 'evil.test'}, {'Origin': 'https://evil.test'}, {'X-Panel-Key': 'wrong'}): + self.assertEqual(self.request('/panels', headers={**auth, **extra})[0], 403) + self.assertEqual(state.call_count, 1) + + @patch.object(panels, 'focus', return_value={'requested': 'panel'}) + def test_post_validation_and_close(self, focus): + auth = {'X-Panel-Key': 'test-key'} + for body in ('[]', '{broken', '0', '"text"', 'x' * 1025): + self.assertEqual(self.request('/focus', body, auth)[0], 400) + self.assertEqual(focus.call_count, 0) + self.assertEqual(self.request('/focus', '{"key":"panel"}', auth)[0], 200) + self.assertEqual(focus.call_args.args, ('panel',)) + self.assertEqual(self.request('/close', '{}', auth)[0], 200) + self.assertTrue(self.server.closing) + + @patch.object(panels, 'state', side_effect=panels.PanelError('offline')) + def test_offline_is_an_error_not_a_successful_empty_list(self, state): + status, body = self.request('/panels', headers={'X-Panel-Key': 'test-key'}) + self.assertEqual(status, 502) + self.assertEqual(json.loads(body), {'error': 'offline'}) diff --git a/ui/frame_panels.py b/ui/frame_panels.py new file mode 100644 index 0000000..4bf753c --- /dev/null +++ b/ui/frame_panels.py @@ -0,0 +1,261 @@ +"""Panel switcher. Runs on the Frame, either piped over SSH or installed with +--open for its loopback-only headset page. Uses Valve's shipped vrcmd and +Chromium, not an overlay app. Spatial layout limitations: docs/panels.md. +""" +import argparse +import hmac +import json +import os +from pathlib import Path +import re +import secrets +import signal +import subprocess +import sys +import time +from http.server import BaseHTTPRequestHandler, HTTPServer + +VRCMD = '/opt/steamvr/bin/linuxarm64/vrcmd' +PANEL_ID = 2000999030 +PANEL_KEY = 'valve.steam.desktopgame.' + str(PANEL_ID) +KEY = re.compile(r'[A-Za-z0-9_.:-]{1,200}\Z') + + +class PanelError(Exception): + pass + + +def run(args): + try: + p = subprocess.run(args, capture_output=True, text=True, timeout=10, + env={**os.environ, 'DISPLAY': ':0', 'LC_ALL': 'C.UTF-8'}) + except (OSError, subprocess.TimeoutExpired) as e: + raise PanelError('The panel service did not answer: ' + str(e)) + if p.returncode: + raise PanelError((p.stderr or p.stdout).strip()[-400:] or 'Panel command failed') + return p.stdout + + +def parse_overlays(text): + """Only main dashboard panels, never their thumbnails, layers or cursors. + vrcmd output verified on SteamVR 2.18.1, BUILD_ID 20260925.6191901. + """ + if '---- OVERLAYS ----' not in text: + raise PanelError('SteamVR did not return its panel list. Is the headset awake?') + panels = [] + for line in text.splitlines(): + m = re.fullmatch(r"'([^']+)' -- '(.*)', (.*?) VROverlayType_Dashboard_Main\s*", line) + if m and KEY.fullmatch(m[1]): + panels.append({'key': m[1], 'title': 'Panel switcher' if m[1] == PANEL_KEY else m[2] or m[1], + 'visible': 'not_visible' not in m[3]}) + return panels + + +def state(): + return {'panels': parse_overlays(run([VRCMD, '--overlays']))} + + +def focus(key): + if not isinstance(key, str) or not KEY.fullmatch(key): + raise PanelError('Choose an open panel.') + if key not in {p['key'] for p in state()['panels']}: + raise PanelError('That panel has closed. Refresh the list.') + run([VRCMD, '--showdashboard', key]) + # vrcmd acknowledges dispatch, not final focus (a game or the user can + # switch again). Do not report a focus success without observing it. + return {'requested': key, 'message': 'Asked SteamVR to show the panel.'} + + +PAGE = ''' +Panel switcher [fc-panels] + +

Panel switcher

Choose a panel to show it. Open this panel again from Steam's dashboard.

+ +

+''' + + +class Handler(BaseHTTPRequestHandler): + def setup(self): + super().setup() + self.connection.settimeout(5) + + def log_message(self, *args): + pass # never log the page's access key + + def reply(self, code, value, html=False): + data = value.encode() if html else json.dumps(value).encode() + self.send_response(code) + self.send_header('Content-Type', 'text/html; charset=utf-8' if html else 'application/json') + self.send_header('Content-Length', str(len(data))) + self.send_header('Cache-Control', 'no-store') + self.send_header('X-Content-Type-Options', 'nosniff') + self.send_header('Referrer-Policy', 'no-referrer') + self.send_header('Content-Security-Policy', "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'") + self.end_headers() + self.wfile.write(data) + + def allowed(self): + host = '127.0.0.1:' + str(self.server.server_port) + origin = self.headers.get('Origin') + return (self.headers.get('Host') == host and + (origin is None or origin == 'http://' + host) and + hmac.compare_digest(self.headers.get('X-Panel-Key', '').encode(), self.server.key.encode())) + + def do_GET(self): + if self.path == '/': + return self.reply(200, PAGE, html=True) # no data or access key in the page + if not self.allowed(): + return self.reply(403, {'error': 'Open the switcher from Frame Control.'}) + try: + if self.path == '/panels': + return self.reply(200, state()) + self.reply(404, {'error': 'Not found'}) + except PanelError as e: + self.reply(502, {'error': str(e)}) + + def do_POST(self): + if not self.allowed(): + return self.reply(403, {'error': 'Forbidden'}) + try: + size = int(self.headers.get('Content-Length', '0')) + if not 0 < size <= 1024: + raise ValueError('Invalid request size') + body = json.loads(self.rfile.read(size)) + if not isinstance(body, dict): + raise ValueError('Expected an object') + if self.path == '/focus': + return self.reply(200, focus(body.get('key'))) + if self.path == '/close': + self.server.closing = True + return self.reply(200, {'closed': True}) + self.reply(404, {'error': 'Not found'}) + except (ValueError, PanelError) as e: + self.reply(400, {'error': str(e)}) + + +def serve(): + """Own only our Chromium profile and process group. No changes to Steam, + SteamVR, other Chromium sessions, or global power settings. + """ + import fcntl # only on the Frame; module/tests also import on Windows + folder = Path.home() / '.local/share/frame-control/panels' + folder.mkdir(parents=True, exist_ok=True, mode=0o700) + with (folder / 'lock').open('w') as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + print(json.dumps(focus(PANEL_KEY)), flush=True) + return + chrome = Path.home() / 'chromium-xr/chrome' + if chrome.is_file(): + command = [str(chrome)] + profile = folder / 'chromium' + elif Path('/usr/bin/chromium').is_file(): + command = ['/usr/bin/chromium'] + profile = folder / 'chromium' + elif subprocess.run(['flatpak', 'info', 'org.chromium.Chromium'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10).returncode == 0: + command = ['flatpak', 'run', 'org.chromium.Chromium'] + profile = Path.home() / '.var/app/org.chromium.Chromium/data/frame-panel-switcher' + else: + raise PanelError('The headset switcher needs Chromium. The companion switcher still works.') + server = HTTPServer(('127.0.0.1', 0), Handler) + server.key = secrets.token_urlsafe(32) + server.closing = False + server.timeout = .5 + url = 'http://127.0.0.1:%d/#%s' % (server.server_port, server.key) + env = {**os.environ, 'DISPLAY': ':0'} + env.pop('WAYLAND_DISPLAY', None) + browser = subprocess.Popen([*command, '--ozone-platform=x11', + '--user-data-dir=' + str(profile), + '--no-first-run', '--no-default-browser-check', + '--password-store=basic', '--window-size=1200,800', '--app=' + url], + env=env, start_new_session=True, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + def stop(signum, frame): + server.closing = True + signal.signal(signal.SIGTERM, stop) + win = None + try: + deadline = time.monotonic() + 30 + while not win and time.monotonic() < deadline and browser.poll() is None: + server.handle_request() # Chromium must fetch the page before it has a title + for line in run(['xwininfo', '-root', '-children']).splitlines(): + m = re.match(r'\s*(0x[0-9a-fA-F]+) .*\[fc-panels\]', line) + if m: + win = m[1] + break + if not win: + raise PanelError('The switcher window did not appear within 30 seconds.') + run(['xprop', '-id', win, '-f', 'STEAM_GAME', '32c', '-set', 'STEAM_GAME', str(PANEL_ID)]) + print(json.dumps({'message': 'Opened the panel switcher in the headset.'}), flush=True) + # Caller reads exactly one line, then disconnects; no more stdout. + while not server.closing and browser.poll() is None: + server.handle_request() + # Closing the last app window need not exit Chromium. + if win not in run(['xwininfo', '-root', '-children']): + break + finally: + server.server_close() + if browser.poll() is None: + os.killpg(browser.pid, signal.SIGTERM) + try: + browser.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(browser.pid, signal.SIGKILL) + browser.wait() + + +def open_switcher(): + # This command runs from an installed path, never from the SSH stdin copy. + proc = subprocess.Popen([sys.executable, str(Path(__file__).resolve()), '--serve'], + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + text=True, start_new_session=True) + line = proc.stdout.readline() + proc.stdout.close() + if not line: + raise PanelError('The headset switcher could not start.') + result = json.loads(line) + if 'error' in result: + raise PanelError(result['error']) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--focus') + parser.add_argument('--open', action='store_true') + parser.add_argument('--serve', action='store_true') + args = parser.parse_args() + try: + if args.serve: + serve() + else: + print(json.dumps(open_switcher() if args.open else focus(args.focus) if args.focus else state())) + except (PanelError, OSError) as e: + print(json.dumps({'error': str(e)}), flush=True) + return 1 + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/ui/index.html b/ui/index.html index d9491e4..cc87cf5 100644 --- a/ui/index.html +++ b/ui/index.html @@ -650,6 +650,15 @@ +
+

Panel switcher

+ +
+
Refresh to see open panels.
+
Choose a panel to show it in SteamVR. The headset switcher stays available from Steam's dashboard. + Saved spatial layouts are not available yet.
+
+