11from evdev import UInput , ecodes as e , util , InputDevice , list_devices
22import threading
3- from select import select
3+ import os
4+ import selectors
5+
46from psutil import process_iter
57
68from plover .output .keyboard import GenericKeyboardEmulation
@@ -412,21 +414,31 @@ def send_key_combination(self, combo):
412414
413415
414416class KeyboardCapture (Capture ):
417+ _selector : selectors .DefaultSelector
418+ _device_thread : threading .Thread | None
419+ # Pipes to signal `_run` thread to stop
420+ _device_thread_read_pipe : int | None
421+ _device_thread_write_pipe : int | None
422+
415423 def __init__ (self ):
416424 super ().__init__ ()
417- # This is based on the example from the python-evdev documentation, using the first of the three alternative methods: https://python-evdev.readthedocs.io/en/latest/tutorial.html#reading-events-from-multiple-devices-using-select
418425 self ._devices = self ._get_devices ()
419426 self ._running = False
420- self ._thread = None
427+
428+ self ._selector = selectors .DefaultSelector ()
429+ self ._device_thread = None
430+ self ._device_thread_read_pipe = None
431+ self ._device_thread_write_pipe = None
432+
421433 self ._res = util .find_ecodes_by_regex (r"KEY_.*" )
422434 self ._ui = UInput (self ._res )
423- self ._suppressed_keys = []
435+ self ._suppressed_keys = set ()
424436 # The keycodes from evdev, e.g. e.KEY_A refers to the *physical* a, which corresponds with the qwerty layout.
425437
426438 def _get_devices (self ):
427439 input_devices = [InputDevice (path ) for path in list_devices ()]
428440 keyboard_devices = [dev for dev in input_devices if self ._filter_devices (dev )]
429- return { dev . fd : dev for dev in keyboard_devices }
441+ return keyboard_devices
430442
431443 def _filter_devices (self , device ):
432444 """
@@ -451,7 +463,7 @@ def _grab_devices(self):
451463 There is likely a race condition here between checking active keys and
452464 actually grabbing the device, but it appears to work fine.
453465 """
454- for device in self ._devices . values () :
466+ for device in self ._devices :
455467 if len (device .active_keys ()) > 0 :
456468 for _ in device .read_loop ():
457469 if len (device .active_keys ()) == 0 :
@@ -461,56 +473,92 @@ def _grab_devices(self):
461473
462474 def _ungrab_devices (self ):
463475 """Ungrab all devices. Handles all exceptions when ungrabbing."""
464- for device in self ._devices . values () :
476+ for device in self ._devices :
465477 try :
466478 device .ungrab ()
467479 except :
468480 log .debug ("failed to ungrab device" , exc_info = True )
469481
470482 def start (self ):
483+ # Exception handling note: cancel() will eventually be called when the
484+ # machine reconnect button is pressed or when the machine is changed.
485+ # Therefore, cancel() does not need to be called in the except block.
471486 try :
472487 self ._grab_devices ()
473- except Exception as e :
488+ self ._device_thread_read_pipe , self ._device_thread_write_pipe = os .pipe ()
489+ self ._selector .register (self ._device_thread_read_pipe , selectors .EVENT_READ )
490+ for device in self ._devices :
491+ self ._selector .register (device , selectors .EVENT_READ )
492+
493+ self ._device_thread = threading .Thread (target = self ._run )
494+ self ._device_thread .start ()
495+
496+ self ._running = True
497+ except Exception :
474498 self ._ungrab_devices ()
475499 raise
476- self ._running = True
477- self ._thread = threading .Thread (target = self ._run )
478- self ._thread .start ()
479500
480501 def cancel (self ):
502+ # Write some arbitrary data to the pipe to signal the _run thread to stop
503+ if self ._device_thread_write_pipe is not None :
504+ try :
505+ os .write (self ._device_thread_write_pipe , b"a" )
506+ except Exception :
507+ log .warning ("failed to write to device thread pipe" , exc_info = True )
508+ if self ._device_thread is not None :
509+ try :
510+ self ._device_thread .join ()
511+ except Exception :
512+ log .warning ("failed to join device thread" , exc_info = True )
513+ self ._device_thread = None
514+ try :
515+ self ._ungrab_devices ()
516+ except Exception :
517+ log .warning ("failed to ungrab devices" , exc_info = True )
518+ try :
519+ self ._selector .close ()
520+ except Exception :
521+ log .warning ("failed to close selector" , exec_info = True )
522+
523+ if self ._device_thread_read_pipe is not None :
524+ try :
525+ os .close (self ._device_thread_read_pipe )
526+ except Exception :
527+ log .warning ("failed to close device thread read pipe" , exc_info = True )
528+ self ._device_thread_read_pipe = None
529+ if self ._device_thread_write_pipe is not None :
530+ try :
531+ os .close (self ._device_thread_write_pipe )
532+ except Exception :
533+ log .warning ("failed to close device thread write pipe" , exc_info = True )
534+ self ._device_thread_write_pipe = None
535+
481536 self ._running = False
482- if self ._thread is not None :
483- self ._thread .join ()
484537
485538 def suppress (self , suppressed_keys = ()):
486539 """
487540 UInput is not capable of suppressing only specific keys. To get around this, non-suppressed keys
488541 are passed through to a UInput device and emulated, while keys in this list get sent to plover.
489542 It does add a little bit of delay, but that is not noticeable.
490543 """
491- self ._suppressed_keys = suppressed_keys
544+ self ._suppressed_keys = set ( suppressed_keys )
492545
493546 def _run (self ):
494547 try :
495- while self ._running :
496- """
497- The select() call blocks the loop until it gets an input, which meant that the keyboard
498- had to be pressed once after executing `cancel()`. Now, there is a 1 second delay instead
499- FIXME: maybe use one of the other options to avoid the timeout
500- https://python-evdev.readthedocs.io/en/latest/tutorial.html#reading-events-from-multiple-devices-using-select
501- """
502- r , _ , _ = select (self ._devices , [], [], 1 )
503- for fd in r :
504- for event in self ._devices [fd ].read ():
505- if event .type == e .EV_KEY :
506- if event .code in KEYCODE_TO_KEY :
507- key_name = KEYCODE_TO_KEY [event .code ]
508- if key_name in self ._suppressed_keys :
509- pressed = event .value == 1
510- (self .key_down if pressed else self .key_up )(
511- key_name
512- )
513- continue # Go to the next iteration, skipping the below code:
548+ while True :
549+ for key , events in self ._selector .select ():
550+ if key .fd == self ._device_thread_read_pipe :
551+ # Stop this thread
552+ return
553+ assert isinstance (key .fileobj , InputDevice )
554+ device : InputDevice = key .fileobj
555+ for event in device .read ():
556+ if event .code in KEYCODE_TO_KEY :
557+ key_name = KEYCODE_TO_KEY [event .code ]
558+ if key_name in self ._suppressed_keys :
559+ pressed = event .value == 1
560+ (self .key_down if pressed else self .key_up )(key_name )
561+ continue # Go to the next iteration, skipping the below code:
514562 self ._ui .write (e .EV_KEY , event .code , event .value )
515563 self ._ui .syn ()
516564 except :
0 commit comments