Add i3 focus tracking and harden X11 handoff

This commit is contained in:
Storm Dragon
2026-07-30 18:53:56 -04:00
parent f4ee5e4468
commit c39f3bda47
17 changed files with 1379 additions and 76 deletions
@@ -11,6 +11,7 @@ import cthulhu as cthulhu_package
from cthulhu import cthulhu_state
from cthulhu import compositor_state_adapter
from cthulhu import compositor_state_i3
from cthulhu import compositor_state_types
from cthulhu import compositor_state_wayland
@@ -74,7 +75,7 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
self.assertTrue(callable(selectedBackend.activate_calls[0]))
self.assertEqual(adapter.get_snapshot().backend_name, "selected")
def test_default_workspace_backends_include_wayland_then_null_backend(self) -> None:
def test_default_workspace_backends_include_i3_wayland_then_null_backend(self) -> None:
adapter = compositor_state_adapter.CompositorStateAdapter()
backend_types = [type(backend) for backend in adapter._workspaceBackends]
@@ -82,11 +83,68 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
self.assertEqual(
backend_types,
[
compositor_state_i3.I3WorkspaceBackend,
compositor_state_wayland.WaylandSharedProtocolsBackend,
compositor_state_wayland.NullWorkspaceBackend,
],
)
def test_workspace_payload_records_i3_foreground_context(self) -> None:
backend = FakeWorkspaceBackend(True, "i3-ipc")
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend])
adapter.activate()
backend.activate_calls[-1](
compositor_state_types.WORKSPACE_STATE_CHANGED,
{"8"},
"i3-window-focus",
{
"focused_window_id": 0x200000C,
"focused_window_title": "9 devel",
"focused_workspace_empty": False,
},
)
snapshot = adapter.get_snapshot()
self.assertEqual(snapshot.active_workspace_ids, frozenset({"8"}))
self.assertEqual(snapshot.focused_window_id, 0x200000C)
self.assertEqual(snapshot.focused_window_title, "9 devel")
self.assertFalse(snapshot.focused_workspace_empty)
def test_confirmed_empty_workspace_clears_stale_accessible_context(self) -> None:
backend = FakeWorkspaceBackend(True, "i3-ipc")
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend])
adapter.activate()
adapter._snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
active_window_token="42:Browser",
locus_of_focus_token="42:Web content",
active_window_pid=42,
locus_of_focus_pid=42,
active_window_name="Browser",
locus_of_focus_name="Web content",
)
backend.activate_calls[-1](
compositor_state_types.WORKSPACE_STATE_CHANGED,
{"9"},
"i3-workspace-focus",
{
"focused_window_id": None,
"focused_window_title": "",
"focused_workspace_empty": True,
},
)
snapshot = adapter.get_snapshot()
self.assertEqual(snapshot.active_window_token, "")
self.assertEqual(snapshot.locus_of_focus_token, "")
self.assertEqual(snapshot.active_window_pid, -1)
self.assertEqual(snapshot.locus_of_focus_pid, -1)
self.assertEqual(snapshot.active_window_name, "")
self.assertEqual(snapshot.locus_of_focus_name, "")
def test_wayland_backend_is_unavailable_without_wayland_session(self) -> None:
backend = compositor_state_wayland.WaylandSharedProtocolsBackend()
@@ -0,0 +1,181 @@
import sys
import types
import unittest
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cthulhu import compositor_state_i3
from cthulhu import compositor_state_types
class FakeNode:
def __init__(
self,
*,
node_type="con",
name="",
window=None,
window_title=None,
focused=False,
children=None,
):
self.type = node_type
self.name = name
self.window = window
self.window_title = window_title
self.focused = focused
self.nodes = list(children or [])
self.floating_nodes = []
self.parent = None
for child in self.nodes:
child.parent = self
def __iter__(self):
pending = list(self.nodes) + list(self.floating_nodes)
while pending:
node = pending.pop(0)
yield node
pending.extend(node.nodes)
pending.extend(node.floating_nodes)
def workspaces(self):
return [node for node in self if node.type == "workspace"]
def find_focused(self):
if self.focused:
return self
return next((node for node in self if node.focused), None)
def workspace(self):
node = self
while node is not None and node.type != "workspace":
node = node.parent
return node
class FakeWorkspaceReply:
def __init__(self, name, focused):
self.name = name
self.focused = focused
class FakeConnection:
def __init__(self, tree, workspaces):
self.tree = tree
self.workspaceReplies = workspaces
def get_tree(self):
return self.tree
def get_workspaces(self):
return self.workspaceReplies
class I3WorkspaceBackendRegressionTests(unittest.TestCase):
def test_query_reports_focused_window_id_title_and_nonempty_workspace(self):
focusedWindow = FakeNode(
name="AT-SPI misses this title",
window=0x200000C,
window_title="Accurate i3 title",
focused=True,
)
workspace = FakeNode(
node_type="workspace",
name="8",
children=[focusedWindow],
)
root = FakeNode(node_type="root", children=[workspace])
connection = FakeConnection(root, [FakeWorkspaceReply("8", True)])
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
backend._connection = connection
workspaceIds, payload = backend._query_context()
self.assertEqual(workspaceIds, {"8"})
self.assertEqual(payload["focused_window_id"], 0x200000C)
self.assertEqual(payload["focused_window_title"], "Accurate i3 title")
self.assertFalse(payload["focused_workspace_empty"])
def test_query_reports_confirmed_empty_focused_workspace(self):
workspace = FakeNode(
node_type="workspace",
name="9",
focused=True,
)
root = FakeNode(node_type="root", children=[workspace])
connection = FakeConnection(root, [FakeWorkspaceReply("9", True)])
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
backend._connection = connection
workspaceIds, payload = backend._query_context()
self.assertEqual(workspaceIds, {"9"})
self.assertIsNone(payload["focused_window_id"])
self.assertEqual(payload["focused_window_title"], "")
self.assertTrue(payload["focused_workspace_empty"])
def test_refresh_emits_started_state_and_finished_with_foreground_payload(self):
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
backend._active = True
backend._emitSignal = mock.Mock()
payload = {
"focused_window_id": None,
"focused_window_title": "",
"focused_workspace_empty": True,
}
with mock.patch.object(backend, "_query_context", return_value=({"9"}, payload)):
self.assertFalse(backend._refresh_context("workspace-focus", transition=True))
self.assertEqual(
backend._emitSignal.call_args_list,
[
mock.call(
compositor_state_types.DESKTOP_TRANSITION_STARTED,
{"9"},
"workspace-focus",
payload,
),
mock.call(
compositor_state_types.WORKSPACE_STATE_CHANGED,
{"9"},
"workspace-focus",
payload,
),
mock.call(
compositor_state_types.DESKTOP_TRANSITION_FINISHED,
{"9"},
"workspace-focus",
payload,
),
],
)
def test_query_failure_emits_unknown_instead_of_preserving_stale_i3_context(self):
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
backend._active = True
backend._emitSignal = mock.Mock()
backend._lastContext = (frozenset({"8"}), 0x200000C, "Browser", False)
with (
mock.patch.object(backend, "_query_context", side_effect=OSError("IPC disconnected")),
mock.patch.object(compositor_state_i3.debug, "printMessage"),
):
self.assertFalse(backend._refresh_context("window-focus", transition=True))
backend._emitSignal.assert_called_once_with(
compositor_state_types.WORKSPACE_STATE_CHANGED,
set(),
"window-focus: i3-context-unknown",
{
"focused_window_id": None,
"focused_window_title": "",
"focused_workspace_empty": None,
},
)
if __name__ == "__main__":
unittest.main()
@@ -91,6 +91,75 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
self.assertFalse(cthulhu_state.pauseAtspiChurn)
self.assertFalse(self.manager._churnSuppressed)
def test_confirmed_empty_i3_workspace_clears_stale_web_context(self) -> None:
staleScript = mock.Mock()
defaultScript = mock.Mock()
focusManager = mock.Mock()
self.manager.app.scriptManager.get_active_script.return_value = staleScript
self.manager.app.scriptManager.get_default_script.return_value = defaultScript
snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
active_workspace_ids=frozenset({"9"}),
focused_workspace_empty=True,
)
with mock.patch.object(
event_manager.focus_manager,
"get_manager",
return_value=focusManager,
):
self.manager._handle_compositor_signal(
compositor_state_types.CompositorStateEvent(
compositor_state_types.DESKTOP_TRANSITION_FINISHED,
reason="workspace-focus",
snapshot=snapshot,
)
)
staleScript.presentationInterrupt.assert_called_once_with()
focusManager.clear_state.assert_called_once_with("confirmed empty i3 workspace")
self.manager.app.scriptManager.set_active_script.assert_called_once_with(
defaultScript,
"focus: confirmed-empty-i3-workspace",
)
self.assertTrue(self.manager._desktopContextConfirmedEmpty)
def test_atspi_event_is_ignored_while_i3_workspace_is_confirmed_empty(self) -> None:
self.manager._desktopContextConfirmedEmpty = True
event = FakeEvent("object:state-changed:focused", source=object(), detail1=1)
self.manager._get_scriptForEvent = mock.Mock()
with (
mock.patch.object(event_manager.debug, "printObjectEvent"),
mock.patch.object(event_manager.debug, "printMessage"),
):
self.manager._processObjectEvent(event)
self.manager._get_scriptForEvent.assert_not_called()
def test_nonempty_i3_workspace_schedules_focus_resync_after_empty_workspace(self) -> None:
self.manager._desktopContextConfirmedEmpty = True
snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
active_workspace_ids=frozenset({"8"}),
focused_window_id=0x200000C,
focused_workspace_empty=False,
)
with mock.patch.object(event_manager.GLib, "idle_add") as idleAdd:
self.manager._handle_compositor_signal(
compositor_state_types.CompositorStateEvent(
compositor_state_types.WORKSPACE_STATE_CHANGED,
reason="window-focus",
snapshot=snapshot,
)
)
self.assertFalse(self.manager._desktopContextConfirmedEmpty)
idleAdd.assert_called_once_with(self.manager._sync_focus_on_startup)
def test_stale_context_event_is_obsolete_while_churn_is_paused(self) -> None:
self.manager._churnSuppressed = True
self.manager._prioritizedContextToken = "current"
@@ -6,9 +6,20 @@ from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cthulhu import input_event_manager
from cthulhu import compositor_state_types
class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
def setUp(self):
self.compositorSnapshot = input_event_manager.cthulhu_state.compositorSnapshot
input_event_manager.cthulhu_state.compositorSnapshot = None
self.addCleanup(
setattr,
input_event_manager.cthulhu_state,
"compositorSnapshot",
self.compositorSnapshot,
)
def test_active_x11_window_differs_from_cached_atspi_window_by_pid(self):
manager = input_event_manager.InputEventManager()
cachedWindow = object()
@@ -599,7 +610,6 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
def test_xterm_match_reuses_cached_metadata_while_ewmh_window_id_is_unchanged(self):
manager = input_event_manager.InputEventManager()
activeWindow = object()
with (
mock.patch.object(
@@ -609,12 +619,7 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
),
mock.patch.object(
manager,
"_get_active_x11_window",
return_value=activeWindow,
) as getActiveWindow,
mock.patch.object(
manager,
"_x11_window_xterm_match",
"_x11_window_id_xterm_match",
return_value=False,
) as matcher,
):
@@ -623,8 +628,165 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
self.assertFalse(firstResult)
self.assertFalse(secondResult)
getActiveWindow.assert_called_once_with(ewmhWindowId=0x1400010)
matcher.assert_called_once_with(activeWindow)
matcher.assert_called_once_with(0x1400010)
def test_xterm_match_prefers_i3_focused_window_id_over_stale_ewmh_id(self):
manager = input_event_manager.InputEventManager()
snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
focused_window_id=0x2600003,
focused_workspace_empty=False,
)
with (
mock.patch.object(input_event_manager.cthulhu_state, "compositorSnapshot", snapshot),
mock.patch.object(
manager,
"_get_ewmh_active_x11_window_id",
return_value=0x200000C,
) as getEwmhWindowId,
mock.patch.object(
manager,
"_x11_window_id_xterm_match",
return_value=False,
) as matcher,
):
result = manager._active_x11_window_xterm_match()
self.assertFalse(result)
matcher.assert_called_once_with(0x2600003)
getEwmhWindowId.assert_not_called()
def test_i3_exact_xid_initializes_x11_before_matching_xterm(self):
manager = input_event_manager.InputEventManager()
snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
focused_window_id=0x200000C,
focused_workspace_empty=False,
)
window = mock.Mock()
window.get_wm_class.return_value = ("xterm", "XTerm")
window.get_wm_name.return_value = "devel"
window.get_full_property.return_value = None
display = mock.Mock()
display.create_resource_object.return_value = window
def initializeX11():
manager._x11Display = display
manager._x11AnyPropertyType = object()
return True
with (
mock.patch.object(input_event_manager.cthulhu_state, "compositorSnapshot", snapshot),
mock.patch.object(
manager,
"_ensure_x11_display",
side_effect=initializeX11,
) as ensureX11,
mock.patch.object(manager, "_get_ewmh_active_x11_window_id") as getEwmhWindowId,
):
result = manager._active_x11_window_xterm_match()
self.assertTrue(result)
ensureX11.assert_called_once_with()
display.create_resource_object.assert_called_once_with("window", 0x200000C)
getEwmhWindowId.assert_not_called()
def test_confirmed_empty_i3_workspace_is_definitely_not_xterm(self):
manager = input_event_manager.InputEventManager()
snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
focused_workspace_empty=True,
)
with (
mock.patch.object(input_event_manager.cthulhu_state, "compositorSnapshot", snapshot),
mock.patch.object(manager, "_get_ewmh_active_x11_window_id") as getEwmhWindowId,
mock.patch.object(manager, "_x11_window_id_xterm_match") as matcher,
):
result = manager._active_x11_window_xterm_match()
self.assertFalse(result)
getEwmhWindowId.assert_not_called()
matcher.assert_not_called()
def test_unknown_i3_context_falls_back_to_generic_ewmh_window_id(self):
manager = input_event_manager.InputEventManager()
snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
focused_window_id=None,
focused_workspace_empty=None,
)
with (
mock.patch.object(input_event_manager.cthulhu_state, "compositorSnapshot", snapshot),
mock.patch.object(
manager,
"_get_ewmh_active_x11_window_id",
return_value=0x2600003,
),
mock.patch.object(
manager,
"_x11_window_id_xterm_match",
return_value=False,
) as matcher,
):
result = manager._active_x11_window_xterm_match()
self.assertFalse(result)
matcher.assert_called_once_with(0x2600003)
def test_ewmh_brave_window_does_not_use_stale_wnck_xterm_metadata(self):
manager = input_event_manager.InputEventManager()
with (
mock.patch.object(
manager,
"_get_ewmh_active_x11_window_id",
return_value=0x2600003,
),
mock.patch.object(
manager,
"_x11_window_id_xterm_match",
return_value=False,
) as directMatcher,
mock.patch.object(manager, "_get_active_x11_window") as getWnckWindow,
mock.patch.object(manager, "_x11_window_xterm_match") as wnckMatcher,
):
result = manager._active_x11_window_xterm_match()
self.assertFalse(result)
directMatcher.assert_called_once_with(0x2600003)
getWnckWindow.assert_not_called()
wnckMatcher.assert_not_called()
def test_direct_x11_metadata_identifies_brave_as_non_xterm(self):
manager = input_event_manager.InputEventManager()
with mock.patch.object(
manager,
"_get_x11_window_metadata",
return_value=(["brave-origin", "Brave-origin", "New Tab - Brave Origin"], 1234),
):
result = manager._x11_window_id_xterm_match(0x2600003)
self.assertFalse(result)
def test_direct_x11_metadata_identifies_xterm(self):
manager = input_event_manager.InputEventManager()
with mock.patch.object(
manager,
"_get_x11_window_metadata",
return_value=(["xterm", "XTerm", "devel"], 1234),
):
result = manager._x11_window_id_xterm_match(0x200000C)
self.assertTrue(result)
def test_xterm_focus_change_suspends_grabs_without_keyboard_event(self):
manager = input_event_manager.InputEventManager()
@@ -687,9 +849,30 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
result = manager._poll_xterm_grab_recovery()
self.assertTrue(result)
self.assertEqual(manager._xtermRecoveryPollCount, 1)
restore.assert_called_once_with()
suspend.assert_not_called()
def test_xterm_match_records_last_raw_ewmh_window_id(self):
manager = input_event_manager.InputEventManager()
with (
mock.patch.object(
manager,
"_get_ewmh_active_x11_window_id",
return_value=0x1400010,
),
mock.patch.object(
manager,
"_x11_window_id_xterm_match",
return_value=False,
),
):
result = manager._active_x11_window_xterm_match()
self.assertFalse(result)
self.assertEqual(manager._lastEwmhReadWindowId, 0x1400010)
def test_xterm_focus_poll_survives_transient_matcher_failure(self):
manager = input_event_manager.InputEventManager()
manager._device = object()
+19 -1
View File
@@ -82,12 +82,23 @@ class RuntimeStateSnapshotTests(unittest.TestCase):
manager._scriptWithSuspendedGrabsForXterm = object()
manager._xtermFocusHandlerId = 17
manager._xtermRecoverySourceId = 23
manager._xtermRecoveryPollCount = 42
manager._xtermHandoffHistory = [
"ewmh:0x200000c->0x1400010",
"restore:success",
]
manager._lastEwmhReadWindowId = 0x1400010
manager._lastEwmhActiveWindowId = 0x1400010
manager._lastEwmhXtermMatch = False
manager._last_input_event = object()
manager._last_non_modifier_key_event = None
snapshot = manager.get_debug_snapshot()
with mock.patch.object(
manager,
"_xterm_recovery_source_is_present",
return_value=True,
):
snapshot = manager.get_debug_snapshot()
self.assertTrue(snapshot["device_active"])
self.assertTrue(snapshot["pointer_watcher_active"])
@@ -97,6 +108,13 @@ class RuntimeStateSnapshotTests(unittest.TestCase):
self.assertTrue(snapshot["suspended_xterm_script_present"])
self.assertTrue(snapshot["xterm_focus_monitor_active"])
self.assertTrue(snapshot["xterm_recovery_timer_active"])
self.assertTrue(snapshot["xterm_recovery_source_present"])
self.assertEqual(42, snapshot["xterm_recovery_poll_count"])
self.assertEqual(
"ewmh:0x200000c->0x1400010 | restore:success",
snapshot["xterm_handoff_history"],
)
self.assertEqual("0x1400010", snapshot["last_ewmh_read_window_id"])
self.assertEqual("0x1400010", snapshot["last_ewmh_active_window_id"])
self.assertFalse(snapshot["last_ewmh_xterm_match"])
self.assertFalse(snapshot["last_non_modifier_key_event_present"])
+3 -3
View File
@@ -1858,7 +1858,7 @@ class WebDynamicContentRecoveryRegressionTests(unittest.TestCase):
setLocusOfFocus.assert_called_once_with(event, source, False)
testScript.utilities.setCaretContext.assert_called_once_with(source, 0)
def test_focused_link_replaces_stale_section_context_after_caret_nav(self):
def test_delayed_focused_link_does_not_rewind_section_context_after_caret_nav(self):
testScript = self._make_dynamic_script()
document = object()
section = object()
@@ -1883,8 +1883,8 @@ class WebDynamicContentRecoveryRegressionTests(unittest.TestCase):
result = web_script.Script.onFocusedChanged(testScript, event)
self.assertTrue(result)
setLocusOfFocus.assert_called_once_with(event, link, False)
testScript.utilities.setCaretContext.assert_called_once_with(link, 0)
setLocusOfFocus.assert_not_called()
testScript.utilities.setCaretContext.assert_not_called()
testScript.utilities.searchForCaretContext.assert_not_called()
def test_browse_mode_sticky_blocks_web_app_descendant_focus_claim(self):
testScript = self._make_dynamic_script()
+167 -1
View File
@@ -34,6 +34,7 @@ class WindowTitleFallbackRegressionTests(unittest.TestCase):
activeWindow = mock.Mock(id=200)
with (
mock.patch.object(plugin, "_ensure_x11_display", return_value=True),
mock.patch.object(plugin, "_get_active_window", return_value=activeWindow),
mock.patch.object(plugin, "_get_current_title", return_value="XTerm"),
mock.patch.object(plugin, "_schedule_fallback_title") as scheduleFallback,
@@ -47,7 +48,10 @@ class WindowTitleFallbackRegressionTests(unittest.TestCase):
plugin._pollSourceId = 1
plugin._lastActiveWindowId = 100
with mock.patch.object(plugin, "_get_active_window", return_value=None):
with (
mock.patch.object(plugin, "_ensure_x11_display", return_value=True),
mock.patch.object(plugin, "_get_active_window", return_value=None),
):
self.assertTrue(plugin._poll_window_title())
self.assertEqual(plugin._lastActiveWindowId, 100)
@@ -58,6 +62,7 @@ class WindowTitleFallbackRegressionTests(unittest.TestCase):
activeWindow = mock.Mock()
with (
mock.patch.object(plugin, "_ensure_x11_display", return_value=True),
mock.patch.object(plugin, "_get_active_window", return_value=activeWindow),
mock.patch.object(plugin, "_get_current_title", return_value="Terminal"),
):
@@ -69,11 +74,172 @@ class WindowTitleFallbackRegressionTests(unittest.TestCase):
activeWindow = mock.Mock()
with (
mock.patch.object(plugin, "_ensure_x11_display", return_value=True),
mock.patch.object(plugin, "_get_active_window", return_value=activeWindow),
mock.patch.object(plugin, "_get_current_title", return_value="Game Window"),
):
self.assertEqual(plugin.get_fallback_title("Wine Desktop"), "Game Window")
def test_fallback_prefers_i3_title_and_window_id(self):
plugin = WindowTitleReader()
plugin.app = mock.Mock()
snapshot = mock.Mock(
backend_name="i3-ipc",
focused_workspace_empty=False,
focused_window_id=0x200000C,
focused_window_title="Accurate i3 title",
)
plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot
with mock.patch.object(plugin, "_get_active_window") as getX11Window:
self.assertEqual(plugin.get_fallback_title("Incomplete title"), "Accurate i3 title")
getX11Window.assert_not_called()
def test_i3_wine_desktop_title_uses_existing_child_window_resolution(self):
plugin = WindowTitleReader()
plugin.app = mock.Mock()
snapshot = mock.Mock(
backend_name="i3-ipc",
focused_workspace_empty=False,
focused_window_id=0x2400007,
focused_window_title="Wine Desktop",
)
desktopWindow = mock.Mock()
plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot
plugin._display = mock.Mock()
plugin._display.create_resource_object.return_value = desktopWindow
with (
mock.patch.object(plugin, "_ensure_x11_display", return_value=True),
mock.patch.object(
plugin,
"_get_window_title",
return_value="Wine Desktop",
),
mock.patch.object(
plugin,
"_get_wine_desktop_title",
return_value="Actual Game Window",
) as getWineTitle,
):
self.assertEqual(
plugin.get_fallback_title("Wine Desktop"),
"Actual Game Window",
)
plugin._display.create_resource_object.assert_called_once_with("window", 0x2400007)
getWineTitle.assert_called_once_with(desktopWindow)
def test_wine_title_resolution_ignores_unrelated_x11_focus(self):
plugin = WindowTitleReader()
desktopWindow = mock.Mock(id=10)
unrelatedFocus = mock.Mock(id=20)
with (
mock.patch.object(plugin, "_get_focus_window", return_value=unrelatedFocus),
mock.patch.object(
plugin,
"_window_is_descendant_of",
return_value=False,
) as isDescendant,
mock.patch.object(plugin, "_get_window_title", return_value="Unrelated Terminal"),
mock.patch.object(
plugin,
"_find_child_title",
return_value="Actual Game Window",
) as findChildTitle,
):
self.assertEqual(
plugin._get_wine_desktop_title(desktopWindow),
"Actual Game Window",
)
isDescendant.assert_called_once_with(unrelatedFocus, desktopWindow)
findChildTitle.assert_called_once_with(desktopWindow)
def test_wine_child_title_filter_rejects_infrastructure_windows(self):
plugin = WindowTitleReader()
helperWindow = mock.Mock()
helperWindow.get_wm_class.return_value = (
"cthulhu-wine-access.exe",
"cthulhu-wine-access.exe",
)
gameWindow = mock.Mock()
gameWindow.get_wm_class.return_value = ("game.exe", "game.exe")
self.assertFalse(plugin._is_usable_wine_child_title(gameWindow, "Default IME"))
self.assertFalse(plugin._is_usable_wine_child_title(helperWindow, "NVDA"))
self.assertTrue(plugin._is_usable_wine_child_title(gameWindow, "Actual Game Window"))
def test_confirmed_empty_i3_workspace_does_not_fall_back_to_stale_x11_title(self):
plugin = WindowTitleReader()
plugin.app = mock.Mock()
snapshot = mock.Mock(
backend_name="i3-ipc",
focused_workspace_empty=True,
focused_window_id=None,
focused_window_title="",
)
plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot
with mock.patch.object(plugin, "_get_active_window") as getX11Window:
self.assertEqual(plugin.get_fallback_title("Browser"), "")
getX11Window.assert_not_called()
def test_unknown_i3_context_falls_back_to_generic_x11_title(self):
plugin = WindowTitleReader()
plugin.app = mock.Mock()
snapshot = mock.Mock(
backend_name="i3-ipc",
focused_workspace_empty=None,
focused_window_id=None,
focused_window_title="",
)
plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot
plugin._pollSourceId = 1
activeWindow = mock.Mock()
with (
mock.patch.object(plugin, "_ensure_x11_display", return_value=True) as ensureX11,
mock.patch.object(plugin, "_get_active_window", return_value=activeWindow),
mock.patch.object(plugin, "_get_current_title", return_value="Generic X title"),
):
self.assertEqual(plugin.get_fallback_title("Incomplete"), "Generic X title")
ensureX11.assert_called_once_with()
def test_known_i3_xid_with_missing_title_uses_exact_x11_title(self):
plugin = WindowTitleReader()
plugin.app = mock.Mock()
snapshot = mock.Mock(
backend_name="i3-ipc",
focused_workspace_empty=False,
focused_window_id=0x2600003,
focused_window_title="",
)
activeWindow = mock.Mock()
plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot
plugin._display = mock.Mock()
plugin._display.create_resource_object.return_value = activeWindow
with (
mock.patch.object(plugin, "_ensure_x11_display", return_value=True),
mock.patch.object(
plugin,
"_get_current_title",
return_value="Exact X11 title",
) as getCurrentTitle,
):
self.assertEqual(
plugin.get_fallback_title("Incomplete"),
"Exact X11 title",
)
plugin._display.create_resource_object.assert_called_once_with("window", 0x2600003)
getCurrentTitle.assert_called_once_with(activeWindow)
def test_present_title_uses_fallback_instead_of_atspi_title(self):
presenter = where_am_i_presenter.WhereAmIPresenter()
script = mock.Mock()
@@ -19,6 +19,28 @@ class WineAccessControllerContractTests(unittest.TestCase):
match.group(0),
)
def test_helper_uses_windows_subsystem_without_a_console_window(self):
cmakePath = Path(__file__).resolve().parents[1] / "wine-access" / "CMakeLists.txt"
source = cmakePath.read_text(encoding="utf-8")
self.assertIn(
"target_link_options(cthulhu-wine-access PRIVATE -mwindows)",
source,
)
def test_compatibility_window_stays_hidden_zero_sized_and_nonactivating(self):
sourcePath = Path(__file__).resolve().parents[1] / "wine-access" / "main.cpp"
source = sourcePath.read_text(encoding="utf-8")
self.assertIn("WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW", source)
self.assertIsNotNone(re.search(
r"CreateWindowExW\(WS_EX_NOACTIVATE \| WS_EX_TOOLWINDOW,.*?"
r"L\"NVDA\", 0, 0, 0, 0, 0,",
source,
re.DOTALL,
))
self.assertIn("ShowWindow(window, SW_HIDE)", source)
if __name__ == "__main__":
unittest.main()