Tighten up relase and reclaim clode for terminals.

This commit is contained in:
Storm Dragon
2026-08-01 03:51:31 -04:00
parent ed80fb0ea7
commit fcd10069ae
8 changed files with 116 additions and 0 deletions
+9
View File
@@ -63,6 +63,15 @@ class CompositorStateAdapter:
def get_snapshot(self) -> DesktopContextSnapshot: def get_snapshot(self) -> DesktopContextSnapshot:
return self._snapshot return self._snapshot
def refresh_workspace_context(self, reason: str) -> DesktopContextSnapshot:
"""Refreshes and returns the selected backend's current workspace context."""
backend = self._workspaceBackend
refreshContext = getattr(backend, "refresh_context", None)
if callable(refreshContext):
refreshContext(reason)
return self._snapshot
def get_session_type(self) -> str: def get_session_type(self) -> str:
return get_session_type() return get_session_type()
+5
View File
@@ -92,6 +92,11 @@ class I3WorkspaceBackend:
self._emitSignal = None self._emitSignal = None
self._lastContext = None self._lastContext = None
def refresh_context(self, reason: str) -> None:
"""Refreshes the cached i3 focus context without emitting a transition."""
self._refresh_context(reason, transition=False)
def _ensure_connection(self) -> bool: def _ensure_connection(self) -> bool:
if self._connection is not None: if self._connection is not None:
return True return True
+3
View File
@@ -174,6 +174,7 @@ class EventManager:
debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating keyboard handling', True) debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating keyboard handling', True)
self._inputEventManager = input_event_manager.get_manager() self._inputEventManager = input_event_manager.get_manager()
self._inputEventManager.set_compositor_state_adapter(self._compositorStateAdapter)
self._inputEventManager.start_key_watcher() self._inputEventManager.start_key_watcher()
cthulhu_state.device = self._inputEventManager._device cthulhu_state.device = self._inputEventManager._device
self._keyHandlingActive = True self._keyHandlingActive = True
@@ -277,6 +278,8 @@ class EventManager:
self._compositorStateAdapter.remove_listener(self._handle_compositor_signal) self._compositorStateAdapter.remove_listener(self._handle_compositor_signal)
self._compositorStateAdapter = adapter self._compositorStateAdapter = adapter
if self._inputEventManager is not None:
self._inputEventManager.set_compositor_state_adapter(adapter)
if adapter is not None and hasattr(adapter, "add_listener"): if adapter is not None and hasattr(adapter, "add_listener"):
adapter.add_listener(self._handle_compositor_signal) adapter.add_listener(self._handle_compositor_signal)
+14
View File
@@ -134,6 +134,12 @@ class InputEventManager:
self._xtermRecoverySourceId: int = 0 self._xtermRecoverySourceId: int = 0
self._xtermRecoveryPollCount: int = 0 self._xtermRecoveryPollCount: int = 0
self._xtermHandoffHistory: List[str] = [] self._xtermHandoffHistory: List[str] = []
self._compositorStateAdapter: Optional[Any] = None
def set_compositor_state_adapter(self, adapter: Optional[Any]) -> None:
"""Stores the compositor adapter used to refresh XTerm handoff state."""
self._compositorStateAdapter = adapter
def activate_device(self) -> Atspi.Device: def activate_device(self) -> Atspi.Device:
"""Creates and returns the AT-SPI device used by this manager.""" """Creates and returns the AT-SPI device used by this manager."""
@@ -847,9 +853,17 @@ class InputEventManager:
snapshot = cthulhu_state.compositorSnapshot snapshot = cthulhu_state.compositorSnapshot
usingI3Context = snapshot is not None and snapshot.backend_name == "i3-ipc" usingI3Context = snapshot is not None and snapshot.backend_name == "i3-ipc"
if usingI3Context and self._lastEwmhXtermMatch is True:
adapter = self._compositorStateAdapter
refreshContext = getattr(adapter, "refresh_workspace_context", None)
if callable(refreshContext):
snapshot = refreshContext("xterm-handoff-check")
usingI3Context = snapshot is not None and snapshot.backend_name == "i3-ipc"
if usingI3Context and snapshot.focused_workspace_empty is True: if usingI3Context and snapshot.focused_workspace_empty is True:
self._record_xterm_handoff_action("i3:confirmed-empty-workspace") self._record_xterm_handoff_action("i3:confirmed-empty-workspace")
self._lastEwmhReadWindowId = None self._lastEwmhReadWindowId = None
self._lastEwmhActiveWindowId = None
self._lastEwmhXtermMatch = False
return False return False
if usingI3Context and snapshot.focused_window_id is not None: if usingI3Context and snapshot.focused_window_id is not None:
@@ -42,6 +42,7 @@ class FakeWorkspaceBackend:
self.name = name self.name = name
self.activate_calls = [] self.activate_calls = []
self.deactivate_calls = [] self.deactivate_calls = []
self.refresh_calls = []
def is_available(self, session_type: str | None = None) -> bool: def is_available(self, session_type: str | None = None) -> bool:
return self.available return self.available
@@ -52,6 +53,9 @@ class FakeWorkspaceBackend:
def deactivate(self, emit_signal=None) -> None: def deactivate(self, emit_signal=None) -> None:
self.deactivate_calls.append(emit_signal) self.deactivate_calls.append(emit_signal)
def refresh_context(self, reason: str) -> None:
self.refresh_calls.append(reason)
class CompositorStateAdapterRegressionTests(unittest.TestCase): class CompositorStateAdapterRegressionTests(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
@@ -111,6 +115,16 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase):
self.assertEqual(snapshot.focused_window_title, "9 devel") self.assertEqual(snapshot.focused_window_title, "9 devel")
self.assertFalse(snapshot.focused_workspace_empty) self.assertFalse(snapshot.focused_workspace_empty)
def test_refresh_workspace_context_queries_selected_backend(self) -> None:
backend = FakeWorkspaceBackend(True, "i3-ipc")
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend])
adapter.activate()
snapshot = adapter.refresh_workspace_context("xterm-handoff-check")
self.assertEqual(backend.refresh_calls, ["xterm-handoff-check"])
self.assertIs(snapshot, adapter.get_snapshot())
def test_confirmed_empty_workspace_clears_stale_accessible_context(self) -> None: def test_confirmed_empty_workspace_clears_stale_accessible_context(self) -> None:
backend = FakeWorkspaceBackend(True, "i3-ipc") backend = FakeWorkspaceBackend(True, "i3-ipc")
adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend]) adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend])
@@ -153,6 +153,26 @@ class I3WorkspaceBackendRegressionTests(unittest.TestCase):
], ],
) )
def test_public_refresh_emits_updated_context_without_transition(self) -> None:
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
backend._active = True
backend._emitSignal = mock.Mock()
payload = {
"focused_window_id": 0x2600003,
"focused_window_title": "Browser",
"focused_workspace_empty": False,
}
with mock.patch.object(backend, "_query_context", return_value=({"8"}, payload)):
backend.refresh_context("xterm-handoff-check")
backend._emitSignal.assert_called_once_with(
compositor_state_types.WORKSPACE_STATE_CHANGED,
{"8"},
"xterm-handoff-check",
payload,
)
def test_query_failure_emits_unknown_instead_of_preserving_stale_i3_context(self): def test_query_failure_emits_unknown_instead_of_preserving_stale_i3_context(self):
backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace()) backend = compositor_state_i3.I3WorkspaceBackend(i3ipc_module=types.SimpleNamespace())
backend._active = True backend._active = True
@@ -64,6 +64,16 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
adapter.add_listener.assert_called_once_with(self.manager._handle_compositor_signal) adapter.add_listener.assert_called_once_with(self.manager._handle_compositor_signal)
def test_set_compositor_state_adapter_updates_active_input_manager(self) -> None:
adapter = mock.Mock()
self.manager._inputEventManager = mock.Mock()
self.manager.set_compositor_state_adapter(adapter)
self.manager._inputEventManager.set_compositor_state_adapter.assert_called_once_with(
adapter
)
def test_pause_signal_updates_churn_state_and_resume_clears_it(self) -> None: def test_pause_signal_updates_churn_state_and_resume_clears_it(self) -> None:
snapshot = compositor_state_types.DesktopContextSnapshot(session_type="wayland") snapshot = compositor_state_types.DesktopContextSnapshot(session_type="wayland")
@@ -658,6 +658,43 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
matcher.assert_called_once_with(0x2600003) matcher.assert_called_once_with(0x2600003)
getEwmhWindowId.assert_not_called() getEwmhWindowId.assert_not_called()
def test_xterm_match_refreshes_i3_context_before_reusing_cached_match(self) -> None:
manager = input_event_manager.InputEventManager()
staleSnapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
focused_window_id=0x200000C,
focused_workspace_empty=False,
)
freshSnapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11",
backend_name="i3-ipc",
focused_window_id=0x2600003,
focused_workspace_empty=False,
)
adapter = mock.Mock()
def refresh_context(_reason: str) -> compositor_state_types.DesktopContextSnapshot:
input_event_manager.cthulhu_state.compositorSnapshot = freshSnapshot
return freshSnapshot
adapter.refresh_workspace_context.side_effect = refresh_context
input_event_manager.cthulhu_state.compositorSnapshot = staleSnapshot
manager._lastEwmhActiveWindowId = 0x200000C
manager._lastEwmhXtermMatch = True
manager.set_compositor_state_adapter(adapter)
with mock.patch.object(
manager,
"_x11_window_id_xterm_match",
return_value=False,
) as matcher:
result = manager._active_x11_window_xterm_match()
self.assertFalse(result)
adapter.refresh_workspace_context.assert_called_once_with("xterm-handoff-check")
matcher.assert_called_once_with(0x2600003)
def test_i3_exact_xid_initializes_x11_before_matching_xterm(self): def test_i3_exact_xid_initializes_x11_before_matching_xterm(self):
manager = input_event_manager.InputEventManager() manager = input_event_manager.InputEventManager()
snapshot = compositor_state_types.DesktopContextSnapshot( snapshot = compositor_state_types.DesktopContextSnapshot(
@@ -696,6 +733,8 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
def test_confirmed_empty_i3_workspace_is_definitely_not_xterm(self): def test_confirmed_empty_i3_workspace_is_definitely_not_xterm(self):
manager = input_event_manager.InputEventManager() manager = input_event_manager.InputEventManager()
manager._lastEwmhActiveWindowId = 0x200000C
manager._lastEwmhXtermMatch = True
snapshot = compositor_state_types.DesktopContextSnapshot( snapshot = compositor_state_types.DesktopContextSnapshot(
session_type="x11", session_type="x11",
backend_name="i3-ipc", backend_name="i3-ipc",
@@ -710,6 +749,8 @@ class InputEventManagerX11FocusRegressionTests(unittest.TestCase):
result = manager._active_x11_window_xterm_match() result = manager._active_x11_window_xterm_match()
self.assertFalse(result) self.assertFalse(result)
self.assertIsNone(manager._lastEwmhActiveWindowId)
self.assertFalse(manager._lastEwmhXtermMatch)
getEwmhWindowId.assert_not_called() getEwmhWindowId.assert_not_called()
matcher.assert_not_called() matcher.assert_not_called()