Created
June 22, 2025 16:30
-
-
Save ssokolow/74c8bda3dafc133e615e3152b815196f to your computer and use it in GitHub Desktop.
Helper to keep the Thunderbird Quick Filter Bar from seeing Escape keypresses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Helper to block Esc key presses from reaching Thunderbird | |
(Works by using XGrabKey to claim Esc on all Thunderbird "3pane" windows, | |
either at startup or when the window first receives focus.) | |
Requires: | |
- Python | |
- python-xlib | |
Copyright 2018,2025 Stephan Sokolow | |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
""" | |
from __future__ import print_function | |
import time | |
from contextlib import contextmanager | |
from functools import reduce | |
from itertools import chain, combinations | |
import Xlib | |
import Xlib.display | |
from Xlib import X, XK | |
__author__ = "Stephan Sokolow (deitarion/SSokolow)" | |
__license__ = "MIT" | |
KEY = ("Escape", X.NoEventMask) | |
WINCLASS = "thunderbird-esr" | |
WINROLE = "3pane" | |
def vary_modmask(modmask, ignored_list): | |
"""Produce all combinations of modifiers which must be grabbed to | |
effectively ignore the state of certain modifiers. | |
""" | |
for ignored in chain.from_iterable(combinations(ignored_list, j) | |
for j in range(len(ignored_list) + 1)): | |
imask = reduce(lambda x, y: x | y, ignored, 0) | |
yield modmask | imask | |
@contextmanager | |
def window_obj(win_id, display): | |
"""Simplify dealing with BadWindow (make it either valid or None)""" | |
window_obj = None | |
if win_id: | |
try: | |
window_obj = display.create_resource_object('window', win_id) | |
except Xlib.error.XError: | |
pass | |
yield window_obj | |
class KeyBlocker(object): # pylint: disable=too-many-instance-attributes | |
"""Encapsulation of the program to allow reconnect on failure""" | |
last_seen = None | |
def __init__(self, key=KEY, winclass=WINCLASS, winrole=WINROLE): | |
self.key = key | |
self.winclass = winclass | |
self.winrole = winrole | |
# Connect to the X server and get the root window | |
self.disp = Xlib.display.Display() | |
self.root = self.disp.screen().root | |
self.keysym = XK.string_to_keysym(self.key[0]) | |
self.keycode = self.disp.keysym_to_keycode(self.keysym) | |
# Prepare the property names we use so they can be fed into X11 APIs | |
self.net_active_window = self.disp.intern_atom('_NET_ACTIVE_WINDOW') | |
self.net_client_list = self.disp.intern_atom('_NET_CLIENT_LIST') | |
self.window_role = self.disp.intern_atom('WINDOW_ROLE') | |
# Listen for _NET_ACTIVE_WINDOW changes | |
self.root.change_attributes(event_mask=Xlib.X.PropertyChangeMask) | |
def _bind_existing_windows(self): | |
"""Minimize the chance of a window receiving Ctrl+C before focus | |
(eg. A Firefox window that was focused before we started) | |
""" | |
for xid in self.root.get_full_property( | |
self.net_client_list, X.AnyPropertyType).value: | |
with window_obj(xid, self.disp) as new_win: | |
self.grab_key(new_win) | |
def grab_key(self, window): | |
"""Grab the undesirable key on the given Windows if it's Firefox""" | |
if not window: | |
return # Allow null windows for robustness and simple structure | |
winclass = window.get_wm_class() | |
winrole = window.get_full_property(self.window_role, 0) | |
if not (winclass and winclass[-1] == self.winclass) and ( | |
winrole and winrole == self.winrole): | |
return # Skip non-Firefox windows | |
# To avoid the risk of an XID collision allowing data loss via Ctrl+Q, | |
# take advantage of the X server not complaining if we re-grab | |
# something we already grabbed. | |
# | |
# (In my stress tests, re-grabbing like this appears to not have any | |
# harmful consequences as long as the X11 event queue is allowed to | |
# flush properly.) | |
for modmask in vary_modmask(self.key[1], (X.Mod2Mask, X.LockMask)): | |
window.grab_key(self.keycode, modmask, 1, | |
X.GrabModeAsync, X.GrabModeAsync) | |
def handle_xevent(self, event): | |
"""Handler for X events which aims for minimal overhead""" | |
# Ignore any unwanted events as quickly and efficiently as possible in | |
# concert with setting event_mask. | |
if (event.type != Xlib.X.PropertyNotify or | |
event.atom != self.net_active_window): | |
return | |
win_id = self.root.get_full_property(self.net_active_window, | |
Xlib.X.AnyPropertyType).value[0] | |
if win_id == self.last_seen: | |
return # Active window has not changed | |
self.last_seen = win_id | |
with window_obj(win_id, self.disp) as new_win: | |
self.grab_key(new_win) | |
def run(self): | |
"""Main loop""" | |
while True: # next_event() sleeps until we get an event | |
self.handle_xevent(self.disp.next_event()) | |
if __name__ == '__main__': | |
while True: | |
try: | |
app = KeyBlocker() | |
app.run() | |
except Exception as err: # pylint: disable=broad-except | |
print("Error encountered. Restarting in 1 second.\n\t{}".format( | |
err)) | |
time.sleep(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment