diff --git a/skills/ui-query/scripts/common.py b/skills/ui-query/scripts/common.py index f6aa735..6105f76 100644 --- a/skills/ui-query/scripts/common.py +++ b/skills/ui-query/scripts/common.py @@ -1,7 +1,38 @@ """Shared utilities for ui-query scripts.""" +import sys + import pyatspi +# Debug mode - set via set_debug() or --debug flag in scripts +_debug = False + + +def set_debug(enabled): + """Enable or disable debug mode for verbose error logging.""" + global _debug + _debug = enabled + + +def log_debug(msg): + """Log message to stderr if debug mode is enabled.""" + if _debug: + print(f"[DEBUG] {msg}", file=sys.stderr) + + +def safe_get_attr(accessible, attr, default=""): + """Safely get an attribute from an accessible, handling stale objects. + + AT-SPI objects can become stale at any time (window closed, app crashed). + This helper catches those errors and returns a default value. + """ + try: + value = getattr(accessible, attr) + return value if value is not None else default + except Exception as e: + log_debug(f"Failed to get {attr}: {e}") + return default + def find_windows(pattern=None, app_name=None, all_windows=False): """Find windows matching criteria. @@ -113,10 +144,11 @@ def find_elements(accessible, role=None, name=None, results=None, max_depth=15, child = accessible.getChildAtIndex(i) if child: find_elements(child, role, name, results, max_depth, depth + 1, limit) - except Exception: + except Exception as e: + log_debug(f"Error accessing child {i}: {e}") continue - except Exception: - pass + except Exception as e: + log_debug(f"Error searching element: {e}") return results diff --git a/skills/ui-query/scripts/find-element.py b/skills/ui-query/scripts/find-element.py index 7a48305..da305a0 100755 --- a/skills/ui-query/scripts/find-element.py +++ b/skills/ui-query/scripts/find-element.py @@ -22,7 +22,7 @@ import sys import pyatspi -from common import find_windows, get_all_windows, find_elements +from common import find_windows, get_all_windows, find_elements, set_debug, log_debug def get_element_info(element): @@ -56,8 +56,8 @@ def get_element_info(element): "width": rect.width, "height": rect.height, } - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get geometry for {info['name']}: {e}") # Get text content if available try: @@ -66,8 +66,8 @@ def get_element_info(element): text = text_iface.getText(0, min(100, text_iface.characterCount)) if text: info["text"] = text - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get text for {info['name']}: {e}") # Build path for context path = [] @@ -123,8 +123,13 @@ def main(): parser.add_argument("--app", "-a", help="Limit to application") parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument("--limit", type=int, default=20, help="Max results") + parser.add_argument("--debug", action="store_true", + help="Show debug messages for AT-SPI errors") args = parser.parse_args() + if args.debug: + set_debug(True) + if not args.role and not args.name: parser.print_help() print("\nExamples:") diff --git a/skills/ui-query/scripts/get-text.py b/skills/ui-query/scripts/get-text.py index 458c570..57ef86d 100755 --- a/skills/ui-query/scripts/get-text.py +++ b/skills/ui-query/scripts/get-text.py @@ -21,7 +21,7 @@ from dataclasses import dataclass, field import pyatspi -from common import find_windows +from common import find_windows, set_debug, log_debug @dataclass @@ -52,8 +52,8 @@ def get_text_content(accessible): char_count = text_iface.characterCount if char_count > 0: text = text_iface.getText(0, char_count) - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get text content: {e}") return text @@ -82,7 +82,8 @@ def extract_text_tree(accessible, max_depth=10, depth=0): child_node = extract_text_tree(child, max_depth, depth + 1) if child_node: node.children.append(child_node) - except Exception: + except Exception as e: + log_debug(f"Error accessing child {i} at depth {depth}: {e}") continue # Prune nodes with no useful content @@ -91,7 +92,8 @@ def extract_text_tree(accessible, max_depth=10, depth=0): return node - except Exception: + except Exception as e: + log_debug(f"Error extracting text tree at depth {depth}: {e}") return None @@ -127,8 +129,13 @@ def main(): parser.add_argument("--all", action="store_true", help="Extract from all windows") parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument("--max-depth", type=int, default=10, help="Max tree depth") + parser.add_argument("--debug", action="store_true", + help="Show debug messages for AT-SPI errors") args = parser.parse_args() + if args.debug: + set_debug(True) + if not args.pattern and not args.app and not args.all: parser.print_help() print("\nExamples:") diff --git a/skills/ui-query/scripts/list-windows.py b/skills/ui-query/scripts/list-windows.py index b202266..6502d84 100755 --- a/skills/ui-query/scripts/list-windows.py +++ b/skills/ui-query/scripts/list-windows.py @@ -15,6 +15,8 @@ import sys import pyatspi +from common import set_debug, log_debug + def get_window_info(window, verbose=False): """Extract info from a window accessible.""" @@ -42,22 +44,23 @@ def get_window_info(window, verbose=False): "width": rect.width, "height": rect.height, } - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get geometry for {info['name']}: {e}") if verbose: # Count children recursively (expensive) - def count_children(acc): + def count_children(acc, path=""): try: count = acc.childCount for i in range(acc.childCount): child = acc.getChildAtIndex(i) if child: - count += count_children(child) + count += count_children(child, f"{path}/{i}") return count - except Exception: + except Exception as e: + log_debug(f"Error counting children at {path}: {e}") return 0 - info["child_count"] = count_children(window) + info["child_count"] = count_children(window, info["name"]) return info @@ -122,8 +125,13 @@ def main(): parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument("--verbose", "-v", action="store_true", help="Include child element counts (slower)") + parser.add_argument("--debug", action="store_true", + help="Show debug messages for AT-SPI errors") args = parser.parse_args() + if args.debug: + set_debug(True) + try: apps = list_windows(verbose=args.verbose) except Exception as e: diff --git a/skills/ui-query/scripts/query-state.py b/skills/ui-query/scripts/query-state.py index e9469de..eb469c2 100755 --- a/skills/ui-query/scripts/query-state.py +++ b/skills/ui-query/scripts/query-state.py @@ -21,7 +21,7 @@ import sys import pyatspi -from common import find_elements +from common import find_elements, set_debug, log_debug # Human-readable state descriptions STATE_DESCRIPTIONS = { @@ -108,8 +108,8 @@ def get_element_details(element, all_states=False): "x": rect.x, "y": rect.y, "width": rect.width, "height": rect.height, } - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get geometry: {e}") # Text content try: @@ -120,8 +120,8 @@ def get_element_details(element, all_states=False): # Caret position if editable if states.get("editable"): info["caret_offset"] = text_iface.caretOffset - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get text: {e}") # Value (for sliders, progress bars, etc.) try: @@ -132,8 +132,8 @@ def get_element_details(element, all_states=False): "min": value_iface.minimumValue, "max": value_iface.maximumValue, } - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get value: {e}") # Actions available try: @@ -144,8 +144,8 @@ def get_element_details(element, all_states=False): actions.append(action_iface.getName(i)) if actions: info["actions"] = actions - except Exception: - pass + except Exception as e: + log_debug(f"Failed to get actions: {e}") return info @@ -166,8 +166,8 @@ def find_focused_element(accessible, depth=0, max_depth=20): result = find_focused_element(child, depth + 1, max_depth) if result: return result - except Exception: - pass + except Exception as e: + log_debug(f"Error finding focused element at depth {depth}: {e}") return None @@ -217,8 +217,13 @@ def main(): parser.add_argument("--focused", "-f", action="store_true", help="Show focused element only") parser.add_argument("--all-states", "-a", action="store_true", help="Show all state flags") parser.add_argument("--json", action="store_true", help="Output as JSON") + parser.add_argument("--debug", action="store_true", + help="Show debug messages for AT-SPI errors") args = parser.parse_args() + if args.debug: + set_debug(True) + if not args.role and not args.name and not args.focused: parser.print_help() print("\nExamples:")