2025, Oct 08 07:00
Capture Super+V on Linux without stealing the grab: passive XRecord listening with python-xlib
Why Super-based global hotkeys on Linux conflict with desktop grabs, and how to detect Super+V reliably with XRecord and python-xlib without blocking anything.
Global hotkeys on Linux desktops work fine until the Super key enters the room. A common case: binding Super+V in a Python app should trigger a handler, but instead the first press types a plain “v” into the focused text field. Other combos like Alt+F3 behave as expected. Below is a walkthrough of why this happens on Linux Mint 22.1 Cinnamon and how to capture Super+V reliably without stealing the grab from the desktop environment.
Minimal example that exposes the issue
The code below binds Super+V via gi.repository Keybinder and keeps a Gtk application alive. Pressing Super+V the first time prints a lowercase “v” in the active text area; only a subsequent plain “v” triggers the handler. Alt+F3 or similar combos do not show this behavior.
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Keybinder', '3.0')
from gi.repository import Keybinder, Gtk
class ClipService(Gtk.Application):
    def __init__(self):
        super().__init__(application_id="org.sample.keydemo", flags=0)
        self.connect("activate", self._on_activate)
        self.connect("startup", self._on_start)
    def _on_start(self, app):
        print("Application started.")
        self.hold()
    def _on_activate(self, app):
        pass
    def on_hotkey(self, user_data=None):
        print("Key was pressed!")
if __name__ == "__main__":
    svc = ClipService()
    Keybinder.init()
    Keybinder.bind("<Super>V", svc.on_hotkey)
    svc.run(None)
What’s going on with Super
Only one client can own a global key grab at a time. On Linux Mint, the desktop menu typically uses Super (often via Mod4) and grabs it. When another program tries to grab the same combination, the X server denies it, resulting in Xlib.error.BadAccess if you attempt a direct grab with Xlib’s grab_key. That aligns with the observation that disabling the “show menu” action bound to Super_L makes the first Super+V press work, and assigning Super+V in Keyboard → Shortcuts also allows the first press to go through. In short, the desktop already owns the grab; your process sees inconsistent behavior because it is not the grab owner for Super-based chords.
Other modifiers like Alt+F3 tend to work because they are not claimed by the menu, so the grab succeeds and the callback runs as expected.
A practical way to listen without grabbing: XRecord
If another component already grabbed Super, competing with it is a losing game. Instead, listen passively. The XRecord extension mirrors keyboard events to your process without taking ownership, which means you can detect Super+V even while the desktop keeps its own bindings. This is how mature tools like AutoKey approach the problem, and it allows additional listeners to coexist.
The example below uses python-xlib with XRecord to detect Super+V as well as which Super was pressed (left or right). It reports KeyPress and KeyRelease and continues to work when Super_L is assigned to the menu or when Super+V is assigned to a function in AutoKey. It does not block any of those actions, because XRecord is a mirror rather than a grab.
from Xlib import X, XK, display
from Xlib.ext import record
from Xlib.protocol import rq
# open two connections: one for local lookups, one for the recorder
x_disp = display.Display()
x_rec = display.Display()
# target keys and masks
ks_v = XK.string_to_keysym('v')
kc_v = x_disp.keysym_to_keycode(ks_v)
mask_mod4 = X.Mod4Mask
# left/right Super lookup
ks_super_l = XK.string_to_keysym("Super_L")
ks_super_r = XK.string_to_keysym("Super_R")
kc_super_l = x_disp.keysym_to_keycode(ks_super_l)
kc_super_r = x_disp.keysym_to_keycode(ks_super_r)
# binary event parser
evt_field = rq.EventField(None)
def on_record(reply):
    if reply.category != record.FromServer:
        return
    if reply.client_swapped:
        return
    if not reply.data:
        return
    evt, _ = evt_field.parse_binary_value(reply.data, x_disp.display, None, None)
    if evt.type == X.KeyPress:
        if evt.detail == kc_v and (evt.state & mask_mod4):
            print("Super+V pressed")
        if evt.detail == kc_super_l:
            print("Super_L pressed")
        elif evt.detail == kc_super_r:
            print("Super_R pressed")
    if evt.type == X.KeyRelease:
        if evt.detail == kc_v and (evt.state & mask_mod4):
            print("Super+V released")
        if evt.detail == kc_super_l:
            print("Super_L released")
        elif evt.detail == kc_super_r:
            print("Super_R released")
# create and enable record context without grabbing keys
ctx = x_rec.record_create_context(
    0,
    [record.AllClients],
    [{
        'core_requests': (0, 0),
        'core_replies': (0, 0),
        'ext_requests': (0, 0, 0, 0),
        'ext_replies': (0, 0, 0, 0),
        'delivered_events': (0, 0),
        'device_events': (X.KeyPress, X.KeyRelease),
        'errors': (0, 0),
        'client_started': False,
        'client_died': False,
    }]
)
print("Listening for Super+V without grabbing...")
x_rec.record_enable_context(ctx, on_record)
x_rec.record_free_context(ctx)
This was verified on Linux Mint 22.1 (Xia) with Mate 1.26.2 and Python 3.13.
Important nuance: listening is not blocking
The XRecord approach mirrors events; it does not cancel them. If a text field is focused, the “v” will still be delivered by the system, and your process will see the mirrored Super+V. This is by design and matches the goal of coexisting with the desktop’s own bindings. If you need to be the exclusive handler, you’d have to rely on a grab-based path, which collides with whoever already owns the same grab.
Why you should care
Global keyboard integrations in Linux desktops live under policy: the desktop environment claims certain keys. Understanding the difference between grab-based APIs and mirror-based listening saves time and avoids fragile workarounds. It also explains why some shortcuts work immediately while Super-based ones don’t, and why disabling or reassigning the menu’s Super binding changes the behavior.
Wrap-up
If Super+V is already taken by the environment, don’t fight for the grab. Use XRecord to listen passively and keep the desktop responsive. If your requirement is to activate on the first press with Super, reconfigure the environment so it does not grab the same chord or define the shortcut at the desktop level. Tools like AutoKey demonstrate that mirroring with XRecord is practical and robust for observing Super-modified keys without interfering with the system.
The article is based on a question from StackOverflow by AlxPav Chr and an answer by furas.