feat: log the window properties gamescope checks before counting a window as focusable

This commit is contained in:
xXJsonDeruloXx committed 2026-10-07 06:45:16 -04:00
1 parent 408233b8e5
commit 933ba953a8
1 file changed
+28 -4
@@ -2,7 +2,10 @@
# The top-level windows of one Xwayland server, one line each, for the session log: what gamescope
# had to pick focus from. Each line has the window, whether it is mapped, override-redirect, its
# geometry, the app id it carries (STEAM_GAME, which gamescope focuses by), its WM_STATE, pid and
# class. Windows that are neither mapped nor tagged with an app id are left out - wine keeps many
# class. Then whatever is set of what else gamescope weighs before it counts a mapped window as
# focusable: an InputOnly class, _NET_WM_WINDOW_OPACITY (0 is never focusable), STEAM_OVERLAY,
# GAMESCOPE_EXTERNAL_OVERLAY, STEAM_INPUT_FOCUS and _NET_WM_STATE (skip taskbar and pager without
# fullscreen is left out of the focusable list the client reads). Windows that are neither mapped nor tagged with an app id are left out - wine keeps many
# hidden ones. The rootfs has xprop but no xwininfo, so this speaks the X protocol itself.
#
# droiddeck-xwindows :1
@@ -65,7 +68,13 @@ class X:
def attributes(self, win):
r = self.request(struct.pack("<BxHI", 3, 2, win))
return (r[26], r[27]) if r else None
return (r[26], r[27], struct.unpack_from("<H", r, 12)[0]) if r else None
def atom_name(self, atom):
r = self.request(struct.pack("<BxHI", 17, 2, atom))
if not r:
return str(atom)
return r[32:32 + struct.unpack_from("<H", r, 8)[0]].decode(errors="replace")
def geometry(self, win):
r = self.request(struct.pack("<BxHI", 14, 2, win))
@@ -91,7 +100,9 @@ def main():
except OSError as e:
print("== %s: %s" % (display, e))
return
atoms = {name: x.atom(name) for name in ("STEAM_GAME", "WM_STATE", "_NET_WM_PID", "WM_CLASS")}
atoms = {name: x.atom(name) for name in (
"STEAM_GAME", "WM_STATE", "_NET_WM_PID", "WM_CLASS", "_NET_WM_WINDOW_OPACITY", "STEAM_OVERLAY",
"GAMESCOPE_EXTERNAL_OVERLAY", "STEAM_INPUT_FOCUS", "_NET_WM_STATE")}
for win in x.children(x.root):
attrs = x.attributes(win)
if not attrs:
@@ -103,6 +114,19 @@ def main():
state = x.prop(win, atoms["WM_STATE"])
pid = x.prop(win, atoms["_NET_WM_PID"])
cls = x.prop(win, atoms["WM_CLASS"])
extra = []
if attrs[2] == 2:
extra.append("InputOnly")
for label, name in (("opacity", "_NET_WM_WINDOW_OPACITY"), ("overlay", "STEAM_OVERLAY"),
("external-overlay", "GAMESCOPE_EXTERNAL_OVERLAY"),
("input-focus", "STEAM_INPUT_FOCUS")):
value = x.prop(win, atoms[name])
if value:
extra.append("%s 0x%x" % (label, value[0]) if label == "opacity" else "%s %d" % (label, value[0]))
net_state = x.prop(win, atoms["_NET_WM_STATE"])
if net_state:
extra.append("net-state " + ",".join(
x.atom_name(a).replace("_NET_WM_STATE_", "") for a in net_state))
print("== %s window 0x%x: %s%s %s app %s state %s pid %s class %s" % (
display, win, MAP_STATE[attrs[0]] if attrs[0] < 3 else attrs[0],
" override-redirect" if attrs[1] else "",
@@ -111,7 +135,7 @@ def main():
WM_STATES.get(state[0], state[0]) if state else "-",
pid[0] if pid else "-",
cls.split(b"\0")[-2].decode(errors="replace") if cls and b"\0" in cls else "-",
))
) + "".join(" " + e for e in extra))
main()