|
1
|
|
|
"""Xlib-based global hotkey-binding code""" |
|
2
|
|
|
|
|
3
|
|
|
__author__ = "Stephan Sokolow (deitarion/SSokolow)" |
|
4
|
|
|
__license__ = "GNU GPL 2.0 or later" |
|
5
|
|
|
|
|
6
|
|
|
import logging |
|
7
|
|
|
|
|
8
|
|
|
import gobject, gtk |
|
9
|
|
|
from Xlib import X |
|
10
|
|
|
from Xlib.display import Display |
|
11
|
|
|
from Xlib.error import BadAccess, DisplayConnectionError |
|
12
|
|
|
from Xlib.protocol.event import KeyPress as XKeyPress |
|
13
|
|
|
|
|
14
|
|
|
from .util import powerset, XInitError |
|
15
|
|
|
|
|
16
|
|
|
# Allow MyPy to work without depending on the `typing` package |
|
17
|
|
|
# (And silence complaints from only using the imported types in comments) |
|
18
|
|
|
try: |
|
19
|
|
|
# pylint: disable=unused-import |
|
20
|
|
|
from typing import (Any, Callable, Dict, Iterable, Iterator, List, # NOQA |
|
21
|
|
|
Optional, Sequence, Sized, Tuple) |
|
22
|
|
|
|
|
23
|
|
|
from Xlib.error import XError # NOQA |
|
24
|
|
|
from .commands import CommandRegistry # NOQA |
|
25
|
|
|
from .wm import WindowManager # NOQA |
|
26
|
|
|
from .util import CommandCB # NOQA |
|
27
|
|
|
except: # pylint: disable=bare-except |
|
28
|
|
|
pass |
|
29
|
|
|
|
|
30
|
|
|
class KeyBinder(object): |
|
31
|
|
|
"""A convenience class for wrapping C{XGrabKey}.""" |
|
32
|
|
|
|
|
33
|
|
|
#: @todo: Figure out how to set the modifier mask in X11 and use |
|
34
|
|
|
#: C{gtk.accelerator_get_default_mod_mask()} to feed said code. |
|
35
|
|
|
ignored_modifiers = ['Mod2Mask', 'LockMask'] |
|
36
|
|
|
|
|
37
|
|
|
#: Used to pass state from L{cb_xerror} |
|
38
|
|
|
keybind_failed = False |
|
39
|
|
|
|
|
40
|
|
|
def __init__(self, xdisplay=None): # type: (Optional[Display]) -> None |
|
41
|
|
|
"""Connect to X11 and the Glib event loop. |
|
42
|
|
|
|
|
43
|
|
|
@param xdisplay: A C{python-xlib} display handle. |
|
44
|
|
|
@type xdisplay: C{Xlib.display.Display} |
|
45
|
|
|
""" |
|
46
|
|
|
try: |
|
47
|
|
|
self.xdisp = xdisplay or Display() |
|
48
|
|
|
except (UnicodeDecodeError, DisplayConnectionError), err: |
|
49
|
|
|
raise XInitError("python-xlib failed with %s when asked to open" |
|
50
|
|
|
" a connection to the X server. Cannot bind keys." |
|
51
|
|
|
"\n\tIt's unclear why this happens, but it is" |
|
52
|
|
|
" usually fixed by deleting your ~/.Xauthority" |
|
53
|
|
|
" file and rebooting." |
|
54
|
|
|
% err.__class__.__name__) |
|
55
|
|
|
|
|
56
|
|
|
self.xroot = self.xdisp.screen().root |
|
57
|
|
|
self._keys = {} # type: Dict[Tuple[int, int], Callable] |
|
58
|
|
|
|
|
59
|
|
|
# Resolve these at runtime to avoid NameErrors |
|
60
|
|
|
self._ignored_modifiers = [getattr(X, name) for name in |
|
61
|
|
|
self.ignored_modifiers] # type: List[int] |
|
62
|
|
|
|
|
63
|
|
|
# We want to receive KeyPress events |
|
64
|
|
|
self.xroot.change_attributes(event_mask=X.KeyPressMask) |
|
65
|
|
|
|
|
66
|
|
|
# Set up a handler to catch XGrabKey() failures |
|
67
|
|
|
self.xdisp.set_error_handler(self.cb_xerror) |
|
68
|
|
|
|
|
69
|
|
|
# Merge python-xlib into the Glib event loop |
|
70
|
|
|
# Source: http://www.pygtk.org/pygtk2tutorial/sec-MonitoringIO.html |
|
71
|
|
|
gobject.io_add_watch(self.xroot.display, |
|
72
|
|
|
gobject.IO_IN, self.cb_xevent) |
|
73
|
|
|
|
|
74
|
|
|
def bind(self, accel, callback): # type: (str, Callable[[], None]) -> bool |
|
75
|
|
|
"""Bind a global key combination to a callback. |
|
76
|
|
|
|
|
77
|
|
|
@param accel: An accelerator as either a string to be parsed by |
|
78
|
|
|
C{gtk.accelerator_parse()} or a tuple as returned by it.) |
|
79
|
|
|
@param callback: The function to call when the key is pressed. |
|
80
|
|
|
|
|
81
|
|
|
@type accel: C{str} or C{(int, gtk.gdk.ModifierType)} or C{(int, int)} |
|
82
|
|
|
@type callback: C{function} |
|
83
|
|
|
|
|
84
|
|
|
@returns: A boolean indicating whether the provided keybinding was |
|
85
|
|
|
parsed successfully. (But not whether it was registered |
|
86
|
|
|
successfully due to the asynchronous nature of the C{XGrabKey} |
|
87
|
|
|
request.) |
|
88
|
|
|
@rtype: C{bool} |
|
89
|
|
|
""" |
|
90
|
|
|
keycode, modmask = self.parse_accel(accel) |
|
91
|
|
|
if keycode is None or modmask is None: |
|
92
|
|
|
return False |
|
93
|
|
|
|
|
94
|
|
|
# Ignore modifiers like Mod2 (NumLock) and Lock (CapsLock) |
|
95
|
|
|
self._keys[(keycode, 0)] = callback # Null modifiers seem to be a risk |
|
96
|
|
|
for mmask in self._vary_modmask(modmask, self._ignored_modifiers): |
|
97
|
|
|
self._keys[(keycode, mmask)] = callback |
|
98
|
|
|
self.xroot.grab_key(keycode, mmask, |
|
99
|
|
|
1, X.GrabModeAsync, X.GrabModeAsync) |
|
100
|
|
|
|
|
101
|
|
|
# If we don't do this, then nothing works. |
|
102
|
|
|
# I assume it flushes the XGrabKey calls to the server. |
|
103
|
|
|
self.xdisp.sync() |
|
104
|
|
|
|
|
105
|
|
|
# React to any cb_xerror that might have resulted from xdisp.sync() |
|
106
|
|
|
if self.keybind_failed: |
|
107
|
|
|
self.keybind_failed = False |
|
108
|
|
|
logging.warning("Failed to bind key. It may already be in use: %s", |
|
109
|
|
|
accel) |
|
110
|
|
|
return False |
|
111
|
|
|
|
|
112
|
|
|
return True |
|
113
|
|
|
|
|
114
|
|
|
def cb_xerror(self, err, _): # type: (XError, Any) -> None |
|
115
|
|
|
"""Used to identify when attempts to bind keys fail. |
|
116
|
|
|
@note: If you can make python-xlib's C{CatchError} actually work or if |
|
117
|
|
|
you can retrieve more information to show, feel free. |
|
118
|
|
|
""" |
|
119
|
|
|
if isinstance(err, BadAccess): |
|
120
|
|
|
self.keybind_failed = True |
|
121
|
|
|
else: |
|
122
|
|
|
self.xdisp.display.default_error_handler(err) |
|
123
|
|
|
|
|
124
|
|
|
def cb_xevent(self, src, cond, handle=None): # pylint: disable=W0613 |
|
125
|
|
|
# type: (Any, Any, Optional[Display]) -> bool |
|
126
|
|
|
"""Callback to dispatch X events to more specific handlers. |
|
127
|
|
|
|
|
128
|
|
|
@rtype: C{True} |
|
129
|
|
|
|
|
130
|
|
|
@todo: Make sure uncaught exceptions are prevented from making |
|
131
|
|
|
quicktile unresponsive in the general case. |
|
132
|
|
|
""" |
|
133
|
|
|
handle = handle or self.xroot.display |
|
134
|
|
|
|
|
135
|
|
|
for _ in range(0, handle.pending_events()): |
|
136
|
|
|
xevent = handle.next_event() |
|
137
|
|
|
if xevent.type == X.KeyPress: |
|
138
|
|
|
self.handle_keypress(xevent) |
|
139
|
|
|
|
|
140
|
|
|
# Necessary for proper function |
|
141
|
|
|
return True |
|
142
|
|
|
|
|
143
|
|
|
def handle_keypress(self, xevent): # type: (XKeyPress) -> None |
|
144
|
|
|
"""Dispatch C{XKeyPress} events to their callbacks.""" |
|
145
|
|
|
keysig = (xevent.detail, xevent.state) |
|
146
|
|
|
if keysig not in self._keys: |
|
147
|
|
|
logging.error("Received an event for an unrecognized keybind: " |
|
148
|
|
|
"%s, %s", xevent.detail, xevent.state) |
|
149
|
|
|
return |
|
150
|
|
|
|
|
151
|
|
|
# Display a meaningful debug message |
|
152
|
|
|
# FIXME: Only call this code if --debug |
|
153
|
|
|
# FIXME: Proper "index" arg for keycode_to_keysym |
|
154
|
|
|
keysym = self.xdisp.keycode_to_keysym(keysig[0], 0) |
|
155
|
|
|
kbstr = gtk.accelerator_name(keysym, keysig[1]) # pylint: disable=E1101 |
|
156
|
|
|
logging.debug("Received keybind: %s", kbstr) |
|
157
|
|
|
|
|
158
|
|
|
# Call the associated callback |
|
159
|
|
|
self._keys[keysig]() |
|
160
|
|
|
|
|
161
|
|
|
def parse_accel(self, accel # type: str |
|
162
|
|
|
): # type: (...) -> Tuple[Optional[int], Optional[int]] |
|
163
|
|
|
"""Convert an accelerator string into the form XGrabKey needs.""" |
|
164
|
|
|
|
|
165
|
|
|
keysym, modmask = gtk.accelerator_parse(accel) |
|
166
|
|
|
if not gtk.accelerator_valid(keysym, modmask): # pylint: disable=E1101 |
|
167
|
|
|
logging.error("Invalid keybinding: %s", accel) |
|
168
|
|
|
return None, None |
|
169
|
|
|
|
|
170
|
|
|
if modmask > 2**16 - 1: |
|
171
|
|
|
logging.error("Modifier out of range for XGrabKey " |
|
172
|
|
|
"(int(modmask) > 65535). " |
|
173
|
|
|
"Did you use <Super> instead of <Mod4>?") |
|
174
|
|
|
return None, None |
|
175
|
|
|
|
|
176
|
|
|
# Convert to what XGrabKey expects |
|
177
|
|
|
keycode = self.xdisp.keysym_to_keycode(keysym) |
|
178
|
|
|
if isinstance(modmask, gtk.gdk.ModifierType): |
|
179
|
|
|
modmask = modmask.real |
|
180
|
|
|
|
|
181
|
|
|
return keycode, modmask |
|
182
|
|
|
|
|
183
|
|
|
@staticmethod |
|
184
|
|
|
def _vary_modmask(modmask, ignored): |
|
185
|
|
|
# type: (int, Sequence[int]) -> Iterator[int] |
|
186
|
|
|
"""Generate all possible variations on C{modmask} that need to be |
|
187
|
|
|
taken into consideration if we can't properly ignore the modifiers in |
|
188
|
|
|
C{ignored}. (Typically NumLock and CapsLock) |
|
189
|
|
|
|
|
190
|
|
|
@param modmask: A bitfield to be combinatorically grown. |
|
191
|
|
|
@param ignored: Modifiers to be combined with C{modmask}. |
|
192
|
|
|
|
|
193
|
|
|
@type modmask: C{int} or C{gtk.gdk.ModifierType} |
|
194
|
|
|
@type ignored: C{list(int)} |
|
195
|
|
|
|
|
196
|
|
|
@rtype: generator of C{type(modmask)} |
|
197
|
|
|
""" |
|
198
|
|
|
|
|
199
|
|
|
for ignored in powerset(ignored): |
|
200
|
|
|
imask = reduce(lambda x, y: x | y, ignored, 0) |
|
201
|
|
|
yield modmask | imask |
|
202
|
|
|
|
|
203
|
|
|
def init(modmask, # type: Optional[str] |
|
204
|
|
|
mappings, # type: Dict[str, CommandCB] |
|
205
|
|
|
commands, # type: CommandRegistry |
|
206
|
|
|
winman # type: WindowManager |
|
207
|
|
|
): # type: (...) -> Optional[KeyBinder] |
|
208
|
|
|
"""Initialize the keybinder and bind the requested mappings""" |
|
209
|
|
|
# Allow modmask to be empty for keybinds which don't share a common prefix |
|
210
|
|
|
if not modmask or modmask.lower() == 'none': |
|
211
|
|
|
modmask = '' |
|
212
|
|
|
|
|
213
|
|
|
try: |
|
214
|
|
|
keybinder = KeyBinder() |
|
215
|
|
|
except XInitError as err: |
|
216
|
|
|
logging.error("%s", err) |
|
217
|
|
|
return None |
|
218
|
|
|
else: |
|
219
|
|
|
# TODO: Take a mapping dict with pre-modmasked keys |
|
220
|
|
|
# and pre-closured commands |
|
221
|
|
|
for key, func in mappings.items(): |
|
222
|
|
|
def call(func=func): |
|
223
|
|
|
"""Closure to resolve `func` and call it on a |
|
224
|
|
|
`WindowManager` instance""" |
|
225
|
|
|
commands.call(func, winman) |
|
226
|
|
|
|
|
227
|
|
|
keybinder.bind(modmask + key, call) |
|
228
|
|
|
return keybinder |
|
229
|
|
|
|