2020
2121pyperclip = require_package ('pyperclip' )
2222
23+ DEFAULT_PASTE_KEYCODE = 47 # Linux evdev KEY_V on QWERTY
24+
2325
2426class TextInjector :
2527 """Handles injecting text into focused applications"""
@@ -42,6 +44,37 @@ def _check_ydotool(self) -> bool:
4244 except Exception :
4345 return False
4446
47+ def _get_paste_keycode (self ) -> int :
48+ """
49+ Get the Linux evdev keycode used for the 'V' part of paste chords.
50+
51+ ydotool's `key` command sends raw keycodes (physical keys). On non-QWERTY
52+ layouts, KEY_V (47) may not map to a keysym 'v', so Ctrl+KEY_V won't paste.
53+ Users can set either:
54+ - `paste_keycode_wev`: the Wayland/XKB keycode printed by `wev` (we subtract 8)
55+ - `paste_keycode`: the Linux evdev keycode directly (advanced)
56+ """
57+ keycode = DEFAULT_PASTE_KEYCODE
58+ if self .config_manager :
59+ wev_keycode = self .config_manager .get_setting ('paste_keycode_wev' , None )
60+ if wev_keycode is not None :
61+ try :
62+ # wev reports Wayland/XKB keycodes, which are typically evdev+8
63+ wev_keycode_int = int (wev_keycode )
64+ converted = wev_keycode_int - 8
65+ return converted if converted > 0 else DEFAULT_PASTE_KEYCODE
66+ except Exception :
67+ # If parsing fails, fall back to evdev keycode setting
68+ pass
69+
70+ keycode = self .config_manager .get_setting ('paste_keycode' , DEFAULT_PASTE_KEYCODE )
71+
72+ try :
73+ keycode_int = int (keycode )
74+ return keycode_int if keycode_int > 0 else DEFAULT_PASTE_KEYCODE
75+ except Exception :
76+ return DEFAULT_PASTE_KEYCODE
77+
4578 def _get_active_window_info (self ) -> Optional [Dict [str , Any ]]:
4679 """Get active window info from Hyprland (if available)"""
4780 try :
@@ -115,35 +148,39 @@ def _send_paste_keys_slow(self, paste_mode: str) -> bool:
115148 the key sequence when modifiers arrive too quickly.
116149 """
117150 try :
151+ paste_keycode = self ._get_paste_keycode ()
152+ paste_keycode_pressed = f'{ paste_keycode } :1'
153+ paste_keycode_released = f'{ paste_keycode } :0'
154+
118155 if paste_mode == 'super' :
119156 # Super+V with delays: Super down, delay, V down, V up, Super up
120157 subprocess .run (['ydotool' , 'key' , '125:1' ], capture_output = True , timeout = 1 )
121158 time .sleep (0.015 )
122- subprocess .run (['ydotool' , 'key' , '47:1' , '47:0' ], capture_output = True , timeout = 1 )
159+ subprocess .run (['ydotool' , 'key' , paste_keycode_pressed , paste_keycode_released ], capture_output = True , timeout = 1 )
123160 time .sleep (0.010 )
124161 subprocess .run (['ydotool' , 'key' , '125:0' ], capture_output = True , timeout = 1 )
125162
126163 elif paste_mode == 'ctrl_shift' :
127164 # Ctrl+Shift+V with delays: mods down, delay, V, delay, mods up
128165 subprocess .run (['ydotool' , 'key' , '29:1' , '42:1' ], capture_output = True , timeout = 1 )
129166 time .sleep (0.015 )
130- subprocess .run (['ydotool' , 'key' , '47:1' , '47:0' ], capture_output = True , timeout = 1 )
167+ subprocess .run (['ydotool' , 'key' , paste_keycode_pressed , paste_keycode_released ], capture_output = True , timeout = 1 )
131168 time .sleep (0.010 )
132169 subprocess .run (['ydotool' , 'key' , '42:0' , '29:0' ], capture_output = True , timeout = 1 )
133170
134171 elif paste_mode == 'ctrl' :
135172 # Ctrl+V with delays
136173 subprocess .run (['ydotool' , 'key' , '29:1' ], capture_output = True , timeout = 1 )
137174 time .sleep (0.015 )
138- subprocess .run (['ydotool' , 'key' , '47:1' , '47:0' ], capture_output = True , timeout = 1 )
175+ subprocess .run (['ydotool' , 'key' , paste_keycode_pressed , paste_keycode_released ], capture_output = True , timeout = 1 )
139176 time .sleep (0.010 )
140177 subprocess .run (['ydotool' , 'key' , '29:0' ], capture_output = True , timeout = 1 )
141178
142179 elif paste_mode == 'alt' :
143180 # Alt+V with delays
144181 subprocess .run (['ydotool' , 'key' , '56:1' ], capture_output = True , timeout = 1 )
145182 time .sleep (0.015 )
146- subprocess .run (['ydotool' , 'key' , '47:1' , '47:0' ], capture_output = True , timeout = 1 )
183+ subprocess .run (['ydotool' , 'key' , paste_keycode_pressed , paste_keycode_released ], capture_output = True , timeout = 1 )
147184 time .sleep (0.010 )
148185 subprocess .run (['ydotool' , 'key' , '56:0' ], capture_output = True , timeout = 1 )
149186
@@ -378,10 +415,11 @@ def _inject_via_clipboard_and_hotkey(self, text: str) -> bool:
378415 self ._clear_stuck_modifiers ()
379416 time .sleep (0.02 ) # Brief settle after clearing modifiers
380417
381- # "super" -> Super+V (125:1 47:1 47:0 125:0)
382- # "ctrl_shift" -> Ctrl+Shift+V (29:1 42:1 47:1 47:0 42:0 29:0)
383- # "ctrl" -> Ctrl+V (29:1 47:1 47:0 29:0)
384- # "alt" -> Alt+V (56:1 47:1 47:0 56:0)
418+ paste_keycode = self ._get_paste_keycode ()
419+ paste_keycode_pressed = f'{ paste_keycode } :1'
420+ paste_keycode_released = f'{ paste_keycode } :0'
421+
422+ # Paste chords are sent as modifiers + a configurable keycode (default KEY_V=47).
385423 paste_mode = None
386424 if self .config_manager :
387425 paste_mode = self .config_manager .get_setting ('paste_mode' , None )
@@ -412,25 +450,25 @@ def _inject_via_clipboard_and_hotkey(self, text: str) -> bool:
412450
413451 # Fast path for non-Kitty terminals (original behavior)
414452 if paste_mode == 'super' :
415- # LeftMeta (Super) = 125, 'V' = 47
453+ # LeftMeta (Super) = 125
416454 result = subprocess .run (
417- ['ydotool' , 'key' , '125:1' , '47:1' , '47:0' , '125:0' ],
455+ ['ydotool' , 'key' , '125:1' , paste_keycode_pressed , paste_keycode_released , '125:0' ],
418456 capture_output = True , timeout = 5
419457 )
420458 elif paste_mode == 'ctrl_shift' :
421459 result = subprocess .run (
422- ['ydotool' , 'key' , '29:1' , '42:1' , '47:1' , '47:0' , '42:0' , '29:0' ],
460+ ['ydotool' , 'key' , '29:1' , '42:1' , paste_keycode_pressed , paste_keycode_released , '42:0' , '29:0' ],
423461 capture_output = True , timeout = 5
424462 )
425463 elif paste_mode == 'ctrl' :
426464 result = subprocess .run (
427- ['ydotool' , 'key' , '29:1' , '47:1' , '47:0' , '29:0' ],
465+ ['ydotool' , 'key' , '29:1' , paste_keycode_pressed , paste_keycode_released , '29:0' ],
428466 capture_output = True , timeout = 5
429467 )
430468 elif paste_mode == 'alt' :
431- # LeftAlt = 56, 'V' = 47
469+ # LeftAlt = 56
432470 result = subprocess .run (
433- ['ydotool' , 'key' , '56:1' , '47:1' , '47:0' , '56:0' ],
471+ ['ydotool' , 'key' , '56:1' , paste_keycode_pressed , paste_keycode_released , '56:0' ],
434472 capture_output = True , timeout = 5
435473 )
436474 else :
@@ -440,12 +478,12 @@ def _inject_via_clipboard_and_hotkey(self, text: str) -> bool:
440478 shift_paste = self .config_manager .get_setting ('shift_paste' , True )
441479 if shift_paste :
442480 result = subprocess .run (
443- ['ydotool' , 'key' , '29:1' , '42:1' , '47:1' , '47:0' , '42:0' , '29:0' ],
481+ ['ydotool' , 'key' , '29:1' , '42:1' , paste_keycode_pressed , paste_keycode_released , '42:0' , '29:0' ],
444482 capture_output = True , timeout = 5
445483 )
446484 else :
447485 result = subprocess .run (
448- ['ydotool' , 'key' , '29:1' , '47:1' , '47:0' , '29:0' ],
486+ ['ydotool' , 'key' , '29:1' , paste_keycode_pressed , paste_keycode_released , '29:0' ],
449487 capture_output = True , timeout = 5
450488 )
451489
0 commit comments