5151}
5252
5353_XK_MODIFIERS : dict [str , int ] = {
54- "ctrl" : 0xFFE3 , # XK_Control_L
55- "alt" : 0xFFE9 , # XK_Alt_L
54+ "ctrl" : 0xFFE3 , # XK_Control_L
55+ "alt" : 0xFFE9 , # XK_Alt_L
5656 "shift" : 0xFFE1 , # XK_Shift_L
57- "meta" : 0xFFEB , # XK_Super_L
57+ "meta" : 0xFFEB , # XK_Super_L
5858}
5959
6060
6161# ---------------------------------------------------------------------------
6262# XTest keyboard/mouse input via ctypes
6363# ---------------------------------------------------------------------------
6464
65+
6566class _XTest :
6667 """Thin ctypes wrapper around Xlib + XTest for input simulation."""
6768
@@ -81,9 +82,7 @@ def _ensure_open(self):
8182
8283 libxtst_name = ctypes .util .find_library ("Xtst" )
8384 if not libxtst_name :
84- raise RuntimeError (
85- "libXtst not found. Install libxtst-dev or xorg-x11-server-utils."
86- )
85+ raise RuntimeError ("libXtst not found. Install libxtst-dev or xorg-x11-server-utils." )
8786 self ._xtst = ctypes .cdll .LoadLibrary (libxtst_name )
8887
8988 display_name = os .environ .get ("DISPLAY" , ":0" ).encode ()
@@ -99,17 +98,27 @@ def _ensure_open(self):
9998 self ._xlib .XKeysymToKeycode .restype = ctypes .c_ubyte
10099
101100 self ._xtst .XTestFakeKeyEvent .argtypes = [
102- ctypes .c_void_p , ctypes .c_uint , ctypes .c_int , ctypes .c_ulong ,
101+ ctypes .c_void_p ,
102+ ctypes .c_uint ,
103+ ctypes .c_int ,
104+ ctypes .c_ulong ,
103105 ]
104106 self ._xtst .XTestFakeKeyEvent .restype = ctypes .c_int
105107
106108 self ._xtst .XTestFakeButtonEvent .argtypes = [
107- ctypes .c_void_p , ctypes .c_uint , ctypes .c_int , ctypes .c_ulong ,
109+ ctypes .c_void_p ,
110+ ctypes .c_uint ,
111+ ctypes .c_int ,
112+ ctypes .c_ulong ,
108113 ]
109114 self ._xtst .XTestFakeButtonEvent .restype = ctypes .c_int
110115
111116 self ._xtst .XTestFakeMotionEvent .argtypes = [
112- ctypes .c_void_p , ctypes .c_int , ctypes .c_int , ctypes .c_int , ctypes .c_ulong ,
117+ ctypes .c_void_p ,
118+ ctypes .c_int ,
119+ ctypes .c_int ,
120+ ctypes .c_int ,
121+ ctypes .c_ulong ,
113122 ]
114123 self ._xtst .XTestFakeMotionEvent .restype = ctypes .c_int
115124
@@ -153,6 +162,7 @@ def _get_xtest() -> _XTest:
153162# Input simulation helpers
154163# ---------------------------------------------------------------------------
155164
165+
156166def _send_key_combo (combo_str : str ) -> None :
157167 """Send a keyboard combination via XTest fake key events."""
158168 xt = _get_xtest ()
@@ -308,6 +318,7 @@ def _send_scroll(x: int, y: int, direction: str, amount: int = 5) -> None:
308318# AT-SPI2 action helpers
309319# ---------------------------------------------------------------------------
310320
321+
311322def _atspi_do_action (accessible , action_name : str ) -> bool :
312323 """Invoke a named action on an AT-SPI2 accessible object.
313324
@@ -399,6 +410,7 @@ def _atspi_get_selection_iface(accessible):
399410# App launching helpers
400411# ---------------------------------------------------------------------------
401412
413+
402414def _discover_desktop_apps () -> dict [str , str ]:
403415 """Discover installed Linux apps from .desktop files.
404416
@@ -407,12 +419,8 @@ def _discover_desktop_apps() -> dict[str, str]:
407419 apps : dict [str , str ] = {}
408420
409421 # Standard XDG data directories
410- xdg_data_dirs = os .environ .get (
411- "XDG_DATA_DIRS" , "/usr/local/share:/usr/share"
412- ).split (":" )
413- xdg_data_home = os .environ .get (
414- "XDG_DATA_HOME" , os .path .expanduser ("~/.local/share" )
415- )
422+ xdg_data_dirs = os .environ .get ("XDG_DATA_DIRS" , "/usr/local/share:/usr/share" ).split (":" )
423+ xdg_data_home = os .environ .get ("XDG_DATA_HOME" , os .path .expanduser ("~/.local/share" ))
416424 search_dirs = [xdg_data_home ] + xdg_data_dirs
417425
418426 for data_dir in search_dirs :
@@ -488,8 +496,9 @@ def _fuzzy_match(
488496 substring_matches = [c for c in candidates if query_lower in c ]
489497 if substring_matches :
490498 word_boundary = [
491- c for c in substring_matches
492- if re .search (r'(?:^|[\s\-_])' + re .escape (query_lower ) + r'(?:$|[\s\-_])' , c )
499+ c
500+ for c in substring_matches
501+ if re .search (r"(?:^|[\s\-_])" + re .escape (query_lower ) + r"(?:$|[\s\-_])" , c )
493502 ]
494503 if word_boundary :
495504 return min (word_boundary , key = len )
@@ -615,9 +624,7 @@ def _click(self, element) -> ActionResult:
615624 _send_mouse_click (center [0 ], center [1 ])
616625 return ActionResult (success = True , message = "Clicked (mouse fallback)" )
617626 except Exception as exc :
618- return ActionResult (
619- success = False , message = "" , error = f"Mouse click failed: { exc } "
620- )
627+ return ActionResult (success = False , message = "" , error = f"Mouse click failed: { exc } " )
621628
622629 return ActionResult (
623630 success = False ,
@@ -634,9 +641,7 @@ def _toggle(self, element) -> ActionResult:
634641 if _atspi_do_action (element , "click" ):
635642 return ActionResult (success = True , message = "Toggled" )
636643
637- return ActionResult (
638- success = False , message = "" , error = "Element does not support toggle"
639- )
644+ return ActionResult (success = False , message = "" , error = "Element does not support toggle" )
640645
641646 def _type (self , element , text : str ) -> ActionResult :
642647 """Type text into an element.
@@ -678,9 +683,7 @@ def _type(self, element, text: str) -> ActionResult:
678683
679684 return ActionResult (success = True , message = f"Typed: { text } " )
680685 except Exception as exc :
681- return ActionResult (
682- success = False , message = "" , error = f"Failed to type: { exc } "
683- )
686+ return ActionResult (success = False , message = "" , error = f"Failed to type: { exc } " )
684687
685688 def _setvalue (self , element , text : str ) -> ActionResult :
686689 """Set value programmatically via AT-SPI2 Value or EditableText interface."""
@@ -715,6 +718,7 @@ def _expand(self, element) -> ActionResult:
715718 try :
716719 state_set = element .get_state_set ()
717720 from gi .repository import Atspi
721+
718722 if state_set .contains (Atspi .StateType .EXPANDED ):
719723 return ActionResult (success = True , message = "Already expanded" )
720724 except Exception :
@@ -728,15 +732,14 @@ def _expand(self, element) -> ActionResult:
728732 if _atspi_do_action (element , "click" ) or _atspi_do_action (element , "activate" ):
729733 return ActionResult (success = True , message = "Expanded" )
730734
731- return ActionResult (
732- success = False , message = "" , error = "Element does not support expand"
733- )
735+ return ActionResult (success = False , message = "" , error = "Element does not support expand" )
734736
735737 def _collapse (self , element ) -> ActionResult :
736738 # Check if already collapsed
737739 try :
738740 state_set = element .get_state_set ()
739741 from gi .repository import Atspi
742+
740743 if not state_set .contains (Atspi .StateType .EXPANDED ):
741744 return ActionResult (success = True , message = "Already collapsed" )
742745 except Exception :
@@ -748,9 +751,7 @@ def _collapse(self, element) -> ActionResult:
748751 if _atspi_do_action (element , "click" ) or _atspi_do_action (element , "activate" ):
749752 return ActionResult (success = True , message = "Collapsed" )
750753
751- return ActionResult (
752- success = False , message = "" , error = "Element does not support collapse"
753- )
754+ return ActionResult (success = False , message = "" , error = "Element does not support collapse" )
754755
755756 def _select (self , element ) -> ActionResult :
756757 # Try Selection interface on the parent (e.g., list selects child)
@@ -780,9 +781,7 @@ def _scroll(self, element, direction: str) -> ActionResult:
780781 _send_scroll (center [0 ], center [1 ], direction )
781782 return ActionResult (success = True , message = f"Scrolled { direction } " )
782783 except Exception as exc :
783- return ActionResult (
784- success = False , message = "" , error = f"Scroll failed: { exc } "
785- )
784+ return ActionResult (success = False , message = "" , error = f"Scroll failed: { exc } " )
786785
787786 return ActionResult (
788787 success = False ,
@@ -810,9 +809,7 @@ def _increment(self, element) -> ActionResult:
810809 except Exception :
811810 pass
812811
813- return ActionResult (
814- success = False , message = "" , error = "Element does not support increment"
815- )
812+ return ActionResult (success = False , message = "" , error = "Element does not support increment" )
816813
817814 def _decrement (self , element ) -> ActionResult :
818815 if _atspi_do_action (element , "decrement" ):
@@ -832,9 +829,7 @@ def _decrement(self, element) -> ActionResult:
832829 except Exception :
833830 pass
834831
835- return ActionResult (
836- success = False , message = "" , error = "Element does not support decrement"
837- )
832+ return ActionResult (success = False , message = "" , error = "Element does not support decrement" )
838833
839834 def _rightclick (self , element ) -> ActionResult :
840835 center = _get_element_center (element )
@@ -843,9 +838,7 @@ def _rightclick(self, element) -> ActionResult:
843838 _send_mouse_click (center [0 ], center [1 ], button = "right" )
844839 return ActionResult (success = True , message = "Right-clicked" )
845840 except Exception as exc :
846- return ActionResult (
847- success = False , message = "" , error = f"Right-click failed: { exc } "
848- )
841+ return ActionResult (success = False , message = "" , error = f"Right-click failed: { exc } " )
849842
850843 return ActionResult (
851844 success = False ,
@@ -860,9 +853,7 @@ def _doubleclick(self, element) -> ActionResult:
860853 _send_mouse_click (center [0 ], center [1 ], count = 2 )
861854 return ActionResult (success = True , message = "Double-clicked" )
862855 except Exception as exc :
863- return ActionResult (
864- success = False , message = "" , error = f"Double-click failed: { exc } "
865- )
856+ return ActionResult (success = False , message = "" , error = f"Double-click failed: { exc } " )
866857
867858 return ActionResult (
868859 success = False ,
@@ -873,9 +864,7 @@ def _doubleclick(self, element) -> ActionResult:
873864 def _focus (self , element ) -> ActionResult :
874865 if _atspi_grab_focus (element ):
875866 return ActionResult (success = True , message = "Focused" )
876- return ActionResult (
877- success = False , message = "" , error = "Failed to focus element"
878- )
867+ return ActionResult (success = False , message = "" , error = "Failed to focus element" )
879868
880869 def _dismiss (self , element ) -> ActionResult :
881870 # Try AT-SPI close/dismiss action
@@ -890,9 +879,7 @@ def _dismiss(self, element) -> ActionResult:
890879 _send_key_combo ("escape" )
891880 return ActionResult (success = True , message = "Dismissed (Escape)" )
892881 except Exception as exc :
893- return ActionResult (
894- success = False , message = "" , error = f"Failed to dismiss: { exc } "
895- )
882+ return ActionResult (success = False , message = "" , error = f"Failed to dismiss: { exc } " )
896883
897884 def _longpress (self , element ) -> ActionResult :
898885 center = _get_element_center (element )
@@ -901,9 +888,7 @@ def _longpress(self, element) -> ActionResult:
901888 _send_mouse_long_press (center [0 ], center [1 ])
902889 return ActionResult (success = True , message = "Long-pressed" )
903890 except Exception as exc :
904- return ActionResult (
905- success = False , message = "" , error = f"Long-press failed: { exc } "
906- )
891+ return ActionResult (success = False , message = "" , error = f"Long-press failed: { exc } " )
907892
908893 return ActionResult (
909894 success = False ,
@@ -920,9 +905,7 @@ def open_app(self, name: str) -> ActionResult:
920905 fuzzy-matches the name, and launches the best match.
921906 """
922907 if not name or not name .strip ():
923- return ActionResult (
924- success = False , message = "" , error = "App name must not be empty"
925- )
908+ return ActionResult (success = False , message = "" , error = "App name must not be empty" )
926909
927910 try :
928911 apps = _discover_desktop_apps ()
@@ -986,6 +969,7 @@ def _wait_for_window(
986969 """Poll AT-SPI2 desktop for a new window matching the launched app."""
987970 try :
988971 import gi
972+
989973 gi .require_version ("Atspi" , "2.0" )
990974 from gi .repository import Atspi
991975 except Exception :
0 commit comments