Skip to content

Commit f37880c

Browse files
FichteFollclaude
andcommitted
Move the Linux Ctrl-release backends into a linux sub-package
`plugin/ctrl_release/linux/` now holds one module per backend: `x11.py` for the X11 poller and `keystate_helper.py` for the helper. Its `__init__.py` keeps the backend selection (`create_poller`, `probe`) and `is_native_wayland`. No behavior changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent dabb29c commit f37880c

6 files changed

Lines changed: 50 additions & 34 deletions

File tree

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
- `TabStackListener.on_close` prunes closed sheets and removes empty window state.
1616
- Tab captions come from `plugin/captions.py`; widgets, panels, and transient/non-tab views are excluded from MRU.
1717
- Ctrl-release detection lives in `plugin/ctrl_release/`.
18-
- Under Linux, Ctrl-release detection uses the `tabstack-keystate` helper (`plugin/ctrl_release/keystate_helper.py`) when its probe succeeds.
18+
- Under Linux, Ctrl-release detection uses the `tabstack-keystate` helper (`plugin/ctrl_release/linux/keystate_helper.py`) when its probe succeeds.
1919
`plugin_loaded` probes it on the async thread, because running it can take up to 2 s; until the result arrives, Linux behaves as if the helper were missing, but without the setup dialog.
2020
The probe records the helper path it ran with, the poller runs exactly that path, and a change of `keystate_helper_path` triggers a new probe.
2121
The helper reads evdev key state with `cap_dac_read_search`, is spawned once per session, and reports Ctrl state changes on stdout.
22-
- Without a working helper, Linux falls back to the X11 key state API, but not on native Wayland (`is_native_wayland()` in `plugin/ctrl_release/linux.py`), because XWayland reports Ctrl as released while a Wayland client has focus; Sublime exposes no key-release API.
22+
- Without a working helper, Linux falls back to the X11 key state API (`plugin/ctrl_release/linux/x11.py`), but not on native Wayland (`is_native_wayland()` in `plugin/ctrl_release/linux/__init__.py`), because XWayland reports Ctrl as released while a Wayland client has focus; Sublime exposes no key-release API.
2323
- On native Wayland without a working helper, the first `ctrl+tab` per plugin load shows a dialog offering the setup (`offer_wayland_setup` in `plugin/setup.py`), unless `show_wayland_setup_dialog` is `false`.
2424
- `tab_stack_setup_keystate_helper` (Command Palette: `TabStack: Set Up Ctrl Detection for Wayland`) re-probes, extracts `keystate/tabstack-keystate.c` to `sublime.cache_path()/TabStack/keystate/`, and shows the build, install and `setcap` commands.
2525
The plugin never runs `sudo`, a compiler or `setcap`; the user installs the helper to `keystate_helper_path` (default `/usr/local/libexec/tabstack-keystate`) with `cap_dac_read_search`, which the helper drops right after opening the input devices.

‎plugin/ctrl_release/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ def is_native_wayland() -> bool:
3535
return False
3636

3737
else:
38-
from .keystate_helper import HelperProbe, start_helper_probe
39-
from .keystate_helper import watch_helper_path as _watch_platform_settings
4038
from .linux import create_poller, is_native_wayland, probe
4139
from .linux import plugin_unloaded as _platform_plugin_unloaded
40+
from .linux.keystate_helper import HelperProbe, start_helper_probe
41+
from .linux.keystate_helper import watch_helper_path as _watch_platform_settings
4242

4343
def _start_platform_probe(on_done: Callable[[], None]) -> None:
4444
def on_helper_probed(_: HelperProbe) -> None:
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from collections.abc import Callable
5+
from typing import Optional
6+
7+
from ...state import CtrlPoller
8+
from .keystate_helper import HelperCtrlReleasePoller, HelperStatus, get_helper_probe
9+
from .keystate_helper import plugin_unloaded as _helper_plugin_unloaded
10+
from .x11 import X11CtrlReleasePoller, is_x11_available
11+
from .x11 import plugin_unloaded as _x11_plugin_unloaded
12+
13+
14+
def is_native_wayland() -> bool:
15+
return bool(os.environ.get("WAYLAND_DISPLAY")) and os.environ.get("GDK_BACKEND") != "x11"
16+
17+
18+
def create_poller(on_release: Callable[[], None], interval_ms: int) -> CtrlPoller:
19+
path = _working_helper_path()
20+
if path is not None:
21+
return HelperCtrlReleasePoller(on_release, interval_ms, path)
22+
return X11CtrlReleasePoller(on_release, interval_ms)
23+
24+
25+
def probe() -> bool:
26+
if _working_helper_path() is not None:
27+
return True
28+
# Under native Wayland, XWayland reports Ctrl as released while Sublime has focus,
29+
# which would commit every session immediately.
30+
return not is_native_wayland() and is_x11_available()
31+
32+
33+
def _working_helper_path() -> Optional[str]:
34+
helper_probe = get_helper_probe()
35+
if helper_probe is None or helper_probe.status is not HelperStatus.OK:
36+
return None
37+
return helper_probe.path
38+
39+
40+
def plugin_unloaded() -> None:
41+
_x11_plugin_unloaded()
42+
_helper_plugin_unloaded()
File renamed without changes.
Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010

1111
import sublime
1212

13-
from ..state import CtrlPoller
14-
from ._availability import set_unavailable
15-
from .keystate_helper import HelperCtrlReleasePoller, HelperStatus, get_helper_probe
16-
from .keystate_helper import plugin_unloaded as _helper_plugin_unloaded
13+
from .._availability import set_unavailable
1714

1815
_XK_Control_L = 0xFFE3
1916
_XK_Control_R = 0xFFE4
@@ -114,30 +111,8 @@ def _open_x11_state(self) -> Optional[_X11State]:
114111
)
115112

116113

117-
def is_native_wayland() -> bool:
118-
return bool(os.environ.get("WAYLAND_DISPLAY")) and os.environ.get("GDK_BACKEND") != "x11"
119-
120-
121-
def create_poller(on_release: Callable[[], None], interval_ms: int) -> CtrlPoller:
122-
path = _working_helper_path()
123-
if path is not None:
124-
return HelperCtrlReleasePoller(on_release, interval_ms, path)
125-
return X11CtrlReleasePoller(on_release, interval_ms)
126-
127-
128-
def probe() -> bool:
129-
if _working_helper_path() is not None:
130-
return True
131-
# Under native Wayland, XWayland reports Ctrl as released while Sublime has focus,
132-
# which would commit every session immediately.
133-
return not is_native_wayland() and _get_x11() is not None
134-
135-
136-
def _working_helper_path() -> Optional[str]:
137-
helper_probe = get_helper_probe()
138-
if helper_probe is None or helper_probe.status is not HelperStatus.OK:
139-
return None
140-
return helper_probe.path
114+
def is_x11_available() -> bool:
115+
return _get_x11() is not None
141116

142117

143118
def _keycode_is_down(keymap: ctypes.Array[ctypes.c_ubyte], keycode: int) -> bool:
@@ -179,4 +154,3 @@ def _get_x11() -> Optional[ctypes.CDLL]:
179154
def plugin_unloaded() -> None:
180155
global _x11
181156
_x11 = None
182-
_helper_plugin_unloaded()

‎plugin/setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import sublime_plugin
88

99
from .ctrl_release import is_native_wayland, refresh_availability
10-
from .ctrl_release.keystate_helper import (
10+
from .ctrl_release.linux.keystate_helper import (
1111
DEFAULT_HELPER_PATH,
1212
HelperProbe,
1313
HelperStatus,

0 commit comments

Comments
 (0)