mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-24 18:01:56 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e914cb3041 | ||
|
|
af161af2a4 |
No files matched your search
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Distinguish an unreadable gamescope panel list from a confirmed empty list in
|
||||
Desktop Capture and Control (issue #73). Failed discovery now asks users to
|
||||
check SteamVR and report the exact message and SteamOS version, rather than
|
||||
claiming no panel is open. This does not establish a new capture fix.
|
||||
|
||||
- Add an opt-in desktop low-battery alert without starting a family session.
|
||||
Existing status reads confirm low charge and drain; recovery hysteresis and a
|
||||
ten-minute cooldown limit repeat alerts. Off on page open/headset switch;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Panel discovery investigation, 2026-10-24
|
||||
|
||||
Base: v0.4.16 (`040581a`). Scope: issue #73, Desktop Capture and Control.
|
||||
|
||||
## Evidence and change
|
||||
|
||||
Issue #73 reports SteamOS 0.4.5 with its desktop visibly running, without a
|
||||
Frame Control version or steps. The maintainer's 2026-10-12 comment identifies
|
||||
the duplicate `loadPanels` UI function fixed in 0.4.3 and requests the exact
|
||||
remaining message and SteamOS build if it persists. There is no reporter
|
||||
follow-up. At this base, the functions are distinct and the existing Node
|
||||
regression checks their uniqueness and the Desktop GET request.
|
||||
|
||||
Read-only source inspection found a remaining diagnostic ambiguity:
|
||||
`read_props()` returns `{}` for failed/timed-out X queries and missing root
|
||||
properties. `panels()` treated that like a confirmed empty focusable-window
|
||||
list. With no detected Plasma process it claimed no panel was open; with Plasma
|
||||
it claimed SteamOS was not listing the desktop. Neither claim follows from an
|
||||
unreadable list.
|
||||
|
||||
The change checks for the presence of `GAMESCOPE_FOCUSABLE_WINDOWS` before
|
||||
those claims. An absent list now reports unknown panel state, suggests checking
|
||||
SteamVR and retrying/Headset view, and asks for the exact message and SteamOS
|
||||
version if persistent. Missing-tool and whole-run deadline explanations retain
|
||||
precedence. No new probes, retries, timeouts, input or capture routes are added.
|
||||
|
||||
## Real-device boundary
|
||||
|
||||
A preliminary read-only SSH inspection returned SteamOS 0.5.6, BUILD_ID
|
||||
`20261015.6141207`, battery 99%, discharging, and X0/X1 socket names. It made
|
||||
no headset changes. The shared lock was absent at that moment.
|
||||
|
||||
The subsequent atomic `mkdir /tmp/frame-test.lock` failed with `File exists`.
|
||||
The guarded command exited with status 3 before the session, X-property or
|
||||
panel-helper probes ran. The other task's lock was left alone. No application
|
||||
was installed, started, stopped or focused; no headset settings changed.
|
||||
|
||||
That implementation probe was blocked by the occupied shared device. The
|
||||
coordinator later supplied a separate narrow read-only observation on SteamOS
|
||||
0.5.6, BUILD_ID `20261015.6141207`, at battery 99%, with the shared lock absent
|
||||
and both `xprop` and `xwininfo` installed:
|
||||
|
||||
- In **Game Mode**, `DISPLAY=:0 xprop -root GAMESCOPE_FOCUSABLE_WINDOWS
|
||||
GAMESCOPE_FOCUSED_WINDOW` returned both properties **present but empty**.
|
||||
- On `DISPLAY=:1`, both properties were reported **not found**.
|
||||
|
||||
This observation distinguishes a confirmed empty list on `:0` from absent
|
||||
properties on `:1`. It does **not** reproduce #73's Desktop Mode report on
|
||||
SteamOS 0.4.5, and it does **not** test this PR's code on the headset. It is a
|
||||
coordinator-supplied observation, separate from the blocked implementation probe
|
||||
above; no new headset probe was run for this follow-up.
|
||||
|
||||
Remaining limit: there is no reproduction establishing a further
|
||||
capture/discovery fix for the reported Desktop Mode/build combination. This PR
|
||||
improves a demonstrated diagnostic error; it must not close #73 as a verified
|
||||
new capture fix.
|
||||
|
||||
## Offline verification
|
||||
|
||||
Focused Python regressions distinguish absent, focus-only, failed/timed-out X
|
||||
queries, confirmed empty and desktop-running lists. Existing tests cover
|
||||
missing tools, alternate displays, successful discovery and the 12-second
|
||||
deadline. The production page's Node behavior test verifies that both Capture
|
||||
and Control preserve the unreadable-list advice. These use fake X/HTTP/DOM
|
||||
boundaries and do not prove real SteamOS 0.4.5 capture or physical input.
|
||||
@@ -285,6 +285,13 @@ How (**verified 2026-09-29**, SteamOS 0.4.1, build 20260925.6191901):
|
||||
[#73](https://github.com/saphid/frame-control/issues/73) (SteamOS 0.4.5, desktop
|
||||
open but not found). The likely cause there was the page itself: a second
|
||||
`loadPanels` (the panel switcher's) replaced the Desktop view's, fixed in #84.
|
||||
If the `GAMESCOPE_FOCUSABLE_WINDOWS` property cannot be read, discovery now
|
||||
reports that the panel list is unreadable and whether a panel is open is
|
||||
unknown. A failed X connection or missing property is not evidence that the
|
||||
desktop is closed. Check that SteamVR is running, retry or use Headset view;
|
||||
if it persists, report the exact message and SteamOS version. The confirmed
|
||||
empty-list advice is retained when the property is present but empty.
|
||||
[2026-10-24 investigation and verification limits](evidence/panel-discovery-2026-10-24.md).
|
||||
- Tested from the iPhone app (Simulator): a tap on the Desktop view focused a
|
||||
text box in the panel and the text field typed into it; a trackpad move went
|
||||
exactly (+40, +25).
|
||||
|
||||
@@ -40,6 +40,15 @@ function page(answer) {
|
||||
if (!p.noPanelWhy().startsWith("The headset's desktop is running")) fail("why for capture: " + p.noPanelWhy());
|
||||
if (!p.ctrlTarget().why.startsWith("The headset's desktop is running")) fail("why for control: " + p.ctrlTarget().why);
|
||||
}
|
||||
// An unreadable list is unknown: both Capture and Control retain that distinction.
|
||||
{
|
||||
const why = "Frame Control couldn't read gamescope's panel list, so it can't tell whether an app panel is open. " +
|
||||
"Check that SteamVR is running in the headset, then try again or use the Headset view. " +
|
||||
"If this persists, report your SteamOS version and this exact message.";
|
||||
const p = page(() => ({ panels: [], focus: null, why }));
|
||||
await p.loadPanels();
|
||||
if (p.noPanelWhy() !== why || p.ctrlTarget().why !== why) fail("unreadable list reason lost");
|
||||
}
|
||||
// An older headset script without "why": the generic advice.
|
||||
{
|
||||
const p = page(() => ({ panels: [], focus: null }));
|
||||
|
||||
@@ -355,6 +355,8 @@ class FindingPanels(unittest.TestCase):
|
||||
t.subprocess.run = lambda *a, **k: subprocess.CompletedProcess(a, 0, out, "")
|
||||
self.assertEqual(self.real_read_props(":0"), {"GAMESCOPE_FOCUSABLE_WINDOWS": [1, 0, 2, 3, 769, 4],
|
||||
"GAMESCOPE_FOCUSED_WINDOW": [0]})
|
||||
t.subprocess.run = lambda *a, **k: subprocess.CompletedProcess(a, 0, "GAMESCOPE_FOCUSABLE_WINDOWS(CARDINAL) = \n", "")
|
||||
self.assertEqual(self.real_read_props(":0"), {"GAMESCOPE_FOCUSABLE_WINDOWS": []})
|
||||
t.subprocess.run = lambda *a, **k: subprocess.CompletedProcess(a, 0, "GAMESCOPE_FOCUSED_WINDOW: not found.\n", "")
|
||||
self.assertEqual(self.real_read_props(":0"), {})
|
||||
|
||||
@@ -392,17 +394,52 @@ class FindingPanels(unittest.TestCase):
|
||||
self.assertIn("Headset view", out["why"])
|
||||
|
||||
def test_desktop_running_but_not_listed(self):
|
||||
self.props[(":0", "GAMESCOPE_FOCUSABLE_WINDOWS")] = []
|
||||
self.t.desktop_running = lambda: True
|
||||
out = self.t.panels()
|
||||
self.assertEqual(out["panels"], [])
|
||||
self.assertIn("desktop is running", out["why"])
|
||||
|
||||
def test_nothing_open(self):
|
||||
self.props[(":0", "GAMESCOPE_FOCUSABLE_WINDOWS")] = []
|
||||
out = self.t.panels()
|
||||
self.assertEqual(out, {"focus": None, "focus_display": None, "panels": [],
|
||||
"why": "No app panel is open in the headset. Open the desktop or an app in "
|
||||
"the headset, then try again."})
|
||||
|
||||
def test_absent_list_is_unknown_even_when_desktop_is_running(self):
|
||||
for desktop in (False, True):
|
||||
with self.subTest(desktop=desktop):
|
||||
self.t.desktop_running = lambda: desktop
|
||||
self.t._ROOT.clear()
|
||||
out = self.t.panels()
|
||||
self.assertEqual(out["panels"], [])
|
||||
self.assertIn("couldn't read gamescope's panel list", out["why"])
|
||||
self.assertIn("can't tell whether", out["why"])
|
||||
self.assertIn("Headset view", out["why"])
|
||||
self.assertNotIn("No app panel is open", out["why"])
|
||||
|
||||
def test_focus_property_without_panel_list_is_unknown(self):
|
||||
self.props[(":0", "GAMESCOPE_FOCUSED_WINDOW")] = [0]
|
||||
self.assertIn("couldn't read gamescope's panel list", self.t.panels()["why"])
|
||||
|
||||
def test_failed_xprop_does_not_claim_no_panels_are_open(self):
|
||||
t = self.t
|
||||
t.read_props = self.real_read_props
|
||||
saved = t.subprocess.run
|
||||
self.addCleanup(setattr, t.subprocess, "run", saved)
|
||||
for failure in ("refused", "timeout"):
|
||||
with self.subTest(failure=failure):
|
||||
def run(*args, **kwargs):
|
||||
if failure == "timeout":
|
||||
raise subprocess.TimeoutExpired(args[0], kwargs["timeout"])
|
||||
return subprocess.CompletedProcess(args[0], 1, "", "Can't open display")
|
||||
t.subprocess.run = run
|
||||
t._ROOT.clear()
|
||||
out = t.panels()
|
||||
self.assertIn("couldn't read gamescope's panel list", out["why"])
|
||||
self.assertNotIn("No app panel is open", out["why"])
|
||||
|
||||
def test_missing_x_tools_are_named(self):
|
||||
self.plasma_on(":0")
|
||||
self.t.shutil.which = lambda tool: None if tool == "xwininfo" else "/usr/bin/" + tool
|
||||
|
||||
+8
-3
@@ -271,7 +271,7 @@ def panels(limit=None):
|
||||
out["why"] = ("The headset took too long to list its panels (its X tools didn't answer in time). "
|
||||
+ ("Some may be missing. " if found else "") + "Try again, or use the Headset view.")
|
||||
elif not found:
|
||||
out["why"] = why_none(unfound)
|
||||
out["why"] = why_none(unfound, props)
|
||||
return out
|
||||
|
||||
|
||||
@@ -295,13 +295,18 @@ def desktop_running():
|
||||
return False
|
||||
|
||||
|
||||
def why_none(listed):
|
||||
def why_none(listed, props):
|
||||
"""Why panels() found nothing, in words the person at the computer can act on.
|
||||
listed: how many of the windows gamescope lists aren't X11 windows on any display."""
|
||||
listed: how many of the windows gamescope lists aren't X11 windows on any display.
|
||||
props: the root properties actually read; an absent list is not an empty list."""
|
||||
missing = [tool for tool in ("xprop", "xwininfo") if not shutil.which(tool)]
|
||||
if missing:
|
||||
return (f"This SteamOS build has no {' or '.join(missing)}, which Frame Control uses to find the "
|
||||
"headset's panels. Use the Headset view instead, and please report your SteamOS version.")
|
||||
if "GAMESCOPE_FOCUSABLE_WINDOWS" not in props:
|
||||
return ("Frame Control couldn't read gamescope's panel list, so it can't tell whether an app panel is open. "
|
||||
"Check that SteamVR is running in the headset, then try again or use the Headset view. "
|
||||
"If this persists, report your SteamOS version and this exact message.")
|
||||
if listed:
|
||||
# gamescope lists windows, but none is an X11 window on any display: a Wayland
|
||||
# window (a desktop drawn straight to gamescope, say) can't be found or captured this way.
|
||||
|
||||
Reference in new issue
Block a user