pythonpygtkkey-bindingslinux-mintcinnamon

Super+V works only for the second time in Keybinder library python linux mint 22.1 cinnamon


I'm using Linux mint 22.1 cinnamon edition and I'm trying to bind Super+V key combination in a python code using the Keybinder library form the gi.repository, and when I press Super+V for the first time, It just prints a lowercase letter "v" in the selected text area, and if I then press "v" again it prints "Key was pressed!", as expected. Here is my code:

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Keybinder', '3.0')
from gi.repository import Keybinder, Gtk

class Application(Gtk.Application):
    def __init__(self):
        super().__init__(application_id="org.example.clipboardhistory", flags=0)
        self.connect("activate", self.on_activate)
        self.connect("startup", self.on_startup)

    def on_startup(self, app):
        print("Application started.")
        self.hold()

    def on_activate(self, app):
        pass

    def key_press(self, user_data=None):
        print("Key was pressed!")

if __name__ == "__main__":
    app = Application()
    Keybinder.init()
    # Keybinder.bind("<Alt>F3", app.show_clipboard_window_action)
    Keybinder.bind("<Super>V", app.key_press)
    app.run(None)

I asked ChatGPT about it, and it said that the code below would work fine, but it works the same way as the first one. Here is the code that ChatGPT gave me:

from Xlib import X, XK, display
from Xlib.ext import record
from Xlib.protocol import rq

# Connect to the X server
d = display.Display()
root = d.screen().root

# Get keycode for 'v' key
v_keysym = XK.string_to_keysym('v')
v_keycode = d.keysym_to_keycode(v_keysym)

# Get keycode for Super (usually Mod4)
mod_mask = X.Mod4Mask

# Grab the key combination on the root window
root.grab_key(v_keycode, mod_mask, True,
              X.GrabModeAsync, X.GrabModeAsync)

print("Listening for Super+V...")

while True:
    event = d.next_event()
    if event.type == X.KeyPress:
        if event.detail == v_keycode and (event.state & mod_mask):
            print("Super+V pressed!")
    

With Alt+F3, or other key combination it works fine, but Is there a way to make it work with Super key? Maybe there is another way to do this? I'm not using Gtk keypress event because I want to catch the key combination without an active window. I would appreciate any answers/comments.

Edit: if in the menu@cinnamon.org applet I don't have Super_L assigned to the "show menu" action, the keypress works for the first time. And if I assign a command to Super+V in the Keyboard > Shortcuts menu, it'd also work for the first keypress.


Solution

  • On Linux Mint I use program AutoKey which has no problem to assign function to Super+V.
    It is created in Python so I tried to dig in its source code. It seems interesting code is in interface.py and it uses XLib with extension XRecord.

    Frankly, code is long so I ask ChatGPT for some help :/

    It seems only one program in system can use grab_key and probably Menu is using it and this is why other programs can't catch the same key combination using grab_key - and when I use print(event) in your second code then I get Xlib.error.BadAccess.

    Other programs need to use extension XRecord to listen and mirroring/duplicating events without grabing them. (And it seems AutoKey use it)

    This is code mostly created by ChatGPT.
    Similar code I found in AutoKey in methods __processEvent(self, reply) and run(self) in class XRecordInterface(XInterfaceBase)

    It detects Super+V but also it can detect if it was left or right Super.
    It detects KeyPress and KeyRelease events.
    It works when SUPER_L is assigned to Menu.
    It works when Super+V is assigned to function in AutoKey and it doesn't block this function.

    from Xlib import X, XK, display
    from Xlib.ext import record
    from Xlib.protocol import rq
    
    # Connect to the X server
    local_display = display.Display()
    record_display = display.Display()
    
    # Keys we want to detect
    v_keysym = XK.string_to_keysym('v')
    v_keycode = local_display.keysym_to_keycode(v_keysym)
    super_mask = X.Mod4Mask
    
    # Lookup Super_L and Super_R keycodes
    super_l_keysym = XK.string_to_keysym("Super_L")
    super_r_keysym = XK.string_to_keysym("Super_R")
    
    super_l_keycode = local_display.keysym_to_keycode(super_l_keysym)
    super_r_keycode = local_display.keysym_to_keycode(super_r_keysym)
    
    # Event parser
    event_parser = rq.EventField(None)
    
    def record_callback(reply):
        if reply.category != record.FromServer:
            return
        if reply.client_swapped:
            return
        if not reply.data:
            return
    
        # Parse the binary data into a real event object
        event, _ = event_parser.parse_binary_value(reply.data, local_display.display, None, None)
    
        if event.type == X.KeyPress:
            if event.detail == v_keycode and (event.state & super_mask):
                print("Super+V pressed")
    
            if event.detail == super_l_keycode:
                print("Super_L pressed")
            elif event.detail == super_r_keycode:
                print("Super_R pressed")
    
        if event.type == X.KeyRelease:
            if event.detail == v_keycode and (event.state & super_mask):
                print("Super+V released")
    
            if event.detail == super_l_keycode:
                print("Super_L released")
            elif event.detail == super_r_keycode:
                print("Super_R released")
    
    
    # Create record context
    ctx = record_display.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...")
    record_display.record_enable_context(ctx, record_callback)
    record_display.record_free_context(ctx)
    

    Tested on Linux Mint 22.1 (Xia) with Mate 1.26.2, Python 3.13.