Skip to content

Commit 08f3c38

Browse files
committed
Use a dedicated thread for ctrl key state polling
The dedicated thread is more reliable than using ST's async thread because we are not at the risk of getting delayed by another task running on it and since we need to respond to user input in a timely manner to update the UI, our polling is timing-critical. Closes #2
1 parent 662dba3 commit 08f3c38

4 files changed

Lines changed: 52 additions & 65 deletions

File tree

plugin/ctrl_release/linux.py

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ctypes
44
import ctypes.util
55
import os
6+
import threading
67
from collections.abc import Callable
78
from dataclasses import dataclass
89
from typing import Any
@@ -24,45 +25,41 @@ class _X11State:
2425
xquery_keymap: Callable[[int, Any], int]
2526

2627

27-
class CtrlReleasePoller:
28+
class CtrlReleasePoller(threading.Thread):
2829
def __init__(self, on_release, interval_ms):
30+
super().__init__(daemon=True)
2931
self._on_release = on_release
3032
self._interval_ms = interval_ms
31-
self._active = False
3233
self._state = self._open_x11_state()
34+
self._stop_event = threading.Event()
3335

3436
def start(self) -> None:
35-
if self._active:
37+
if self.is_alive():
3638
return
37-
self._active = True
38-
self._schedule()
39+
self._stop_event.clear()
40+
super().start()
3941

4042
def stop(self) -> None:
41-
self._active = False
43+
self._stop_event.set()
44+
45+
def run(self) -> None:
46+
try:
47+
while not self._stop_event.wait(self._interval_ms / 1000):
48+
if self._state is None or not self._ctrl_down():
49+
self._fire_release()
50+
break
51+
finally:
52+
self._close_state()
53+
54+
def _close_state(self) -> None:
4255
if self._state is not None:
4356
self._state.xclose_display(self._state.display)
4457
self._state = None
4558

46-
def _schedule(self) -> None:
47-
if not self._active:
48-
return
49-
sublime.set_timeout_async(self._poll, self._interval_ms)
50-
51-
def _poll(self) -> None:
52-
if not self._active:
53-
return
54-
if self._state is None:
55-
self._fire_release()
56-
return
57-
if not self._ctrl_down():
58-
self._fire_release()
59-
return
60-
self._schedule()
61-
6259
def _fire_release(self) -> None:
63-
if not self._active:
60+
if self._stop_event.is_set():
6461
return
65-
self._active = False
62+
self._stop_event.set()
6663
sublime.set_timeout(self._on_release)
6764

6865
def _ctrl_down(self) -> bool:

plugin/ctrl_release/macos.py

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ctypes
44
import ctypes.util
5+
import threading
56
from collections.abc import Callable
67
from typing import Any
78

@@ -14,39 +15,33 @@
1415
_CORE_GRAPHICS: Any | None = None
1516

1617

17-
class CtrlReleasePoller:
18+
class CtrlReleasePoller(threading.Thread):
1819
def __init__(self, on_release: Callable[[], None], interval_ms: int) -> None:
20+
super().__init__(daemon=True)
1921
self._on_release = on_release
2022
self._interval_ms = interval_ms
21-
self._active = False
2223
self._core_graphics = _get_core_graphics()
24+
self._stop_event = threading.Event()
2325

2426
def start(self) -> None:
25-
if self._active:
27+
if self.is_alive():
2628
return
27-
self._active = True
28-
self._schedule()
29+
self._stop_event.clear()
30+
super().start()
2931

3032
def stop(self) -> None:
31-
self._active = False
33+
self._stop_event.set()
3234

33-
def _schedule(self) -> None:
34-
if not self._active:
35-
return
36-
sublime.set_timeout_async(self._poll, self._interval_ms)
37-
38-
def _poll(self) -> None:
39-
if not self._active:
40-
return
41-
if self._core_graphics is None or not self._ctrl_down():
42-
self._fire_release()
43-
return
44-
self._schedule()
35+
def run(self) -> None:
36+
while not self._stop_event.wait(self._interval_ms / 1000):
37+
if self._core_graphics is None or not self._ctrl_down():
38+
self._fire_release()
39+
break
4540

4641
def _fire_release(self) -> None:
47-
if not self._active:
42+
if self._stop_event.is_set():
4843
return
49-
self._active = False
44+
self._stop_event.set()
5045
sublime.set_timeout(self._on_release)
5146

5247
def _ctrl_down(self) -> bool:

plugin/ctrl_release/windows.py

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import ctypes
4+
import threading
45
from collections.abc import Callable
56
from typing import Any
67

@@ -11,39 +12,33 @@
1112
_USER32: Any | None = None
1213

1314

14-
class CtrlReleasePoller:
15+
class CtrlReleasePoller(threading.Thread):
1516
def __init__(self, on_release: Callable[[], None], interval_ms: int) -> None:
17+
super().__init__(daemon=True)
1618
self._on_release = on_release
1719
self._interval_ms = interval_ms
18-
self._active = False
1920
self._user32: Any = _get_user32()
21+
self._stop_event = threading.Event()
2022

2123
def start(self) -> None:
22-
if self._active:
24+
if self.is_alive():
2325
return
24-
self._active = True
25-
self._schedule()
26+
self._stop_event.clear()
27+
super().start()
2628

2729
def stop(self) -> None:
28-
self._active = False
30+
self._stop_event.set()
2931

30-
def _schedule(self) -> None:
31-
if not self._active:
32-
return
33-
sublime.set_timeout_async(self._poll, self._interval_ms)
34-
35-
def _poll(self) -> None:
36-
if not self._active:
37-
return
38-
if self._user32 is None or not self._ctrl_down():
39-
self._fire_release()
40-
return
41-
self._schedule()
32+
def run(self) -> None:
33+
while not self._stop_event.wait(self._interval_ms / 1000):
34+
if self._user32 is None or not self._ctrl_down():
35+
self._fire_release()
36+
break
4237

4338
def _fire_release(self) -> None:
44-
if not self._active:
39+
if self._stop_event.is_set():
4540
return
46-
self._active = False
41+
self._stop_event.set()
4742
sublime.set_timeout(self._on_release)
4843

4944
def _ctrl_down(self) -> bool:

plugin/history.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def sync_selection_history(
119119
for group_key, group_state_stack in list(groups.items()):
120120
try:
121121
group = int(group_key)
122-
except (TypeError, ValueError):
122+
except TypeError, ValueError:
123123
# Unexpected/corrupt key; drop it to avoid crashing.
124124
del groups[group_key]
125125
changed = True

0 commit comments

Comments
 (0)