refactor(ui-query): extract shared find_elements() to common.py

Consolidates duplicate find_elements() from find-element.py and query-state.py.
Shared function returns raw accessibles; callers map through their own info extractors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dan 2026-01-15 15:18:18 -08:00
parent 5a80055ed8
commit fedec9c46a
3 changed files with 59 additions and 82 deletions

View file

@ -67,3 +67,56 @@ def get_all_windows():
windows.append({"accessible": window})
return windows
def find_elements(accessible, role=None, name=None, results=None, max_depth=15, depth=0, limit=20):
"""Recursively search for elements matching role/name criteria.
Args:
accessible: Root accessible to search from
role: Role substring to match (case-insensitive)
name: Name substring to match (case-insensitive)
results: Accumulator list (internal use)
max_depth: Maximum tree depth to traverse
depth: Current depth (internal use)
limit: Maximum results to return
Returns:
List of matching accessible objects
"""
if results is None:
results = []
if len(results) >= limit or depth > max_depth:
return results
try:
matches = True
if role:
elem_role = accessible.getRoleName().lower().replace(" ", "-")
if role.lower() not in elem_role:
matches = False
if name and matches:
elem_name = (accessible.name or "").lower()
if name.lower() not in elem_name:
matches = False
if matches and (role or name):
results.append(accessible)
for i in range(accessible.childCount):
if len(results) >= limit:
break
try:
child = accessible.getChildAtIndex(i)
if child:
find_elements(child, role, name, results, max_depth, depth + 1, limit)
except Exception:
continue
except Exception:
pass
return results

View file

@ -22,7 +22,7 @@ import sys
import pyatspi
from common import find_windows, get_all_windows
from common import find_windows, get_all_windows, find_elements
def get_element_info(element):
@ -86,48 +86,6 @@ def get_element_info(element):
return info
def find_elements(accessible, role=None, name=None, results=None, max_depth=15, depth=0, limit=20):
"""Recursively search for matching elements."""
if results is None:
results = []
if len(results) >= limit or depth > max_depth:
return results
try:
# Check if this element matches
matches = True
if role:
elem_role = accessible.getRoleName().lower().replace(" ", "-")
if role.lower() not in elem_role:
matches = False
if name and matches:
elem_name = (accessible.name or "").lower()
if name.lower() not in elem_name:
matches = False
if matches and (role or name): # Only add if we're filtering
results.append(get_element_info(accessible))
# Search children
for i in range(accessible.childCount):
if len(results) >= limit:
break
try:
child = accessible.getChildAtIndex(i)
if child:
find_elements(child, role, name, results, max_depth, depth + 1, limit)
except Exception:
continue
except Exception:
pass
return results
def print_results(results, as_json=False):
"""Print search results."""
if as_json:
@ -191,13 +149,14 @@ def main():
all_results = []
for win in windows:
results = find_elements(
elements = find_elements(
win["accessible"],
role=args.role,
name=args.name,
limit=args.limit - len(all_results)
)
all_results.extend(results)
# Convert raw accessibles to info dicts
all_results.extend(get_element_info(elem) for elem in elements)
if len(all_results) >= args.limit:
break

View file

@ -21,6 +21,8 @@ import sys
import pyatspi
from common import find_elements
# Human-readable state descriptions
STATE_DESCRIPTIONS = {
"active": "Currently active/foreground",
@ -170,43 +172,6 @@ def find_focused_element(accessible, depth=0, max_depth=20):
return None
def find_elements(accessible, role=None, name=None, results=None, max_depth=15, depth=0, limit=10):
"""Find matching elements."""
if results is None:
results = []
if len(results) >= limit or depth > max_depth:
return results
try:
matches = True
if role:
elem_role = accessible.getRoleName().lower().replace(" ", "-")
if role.lower() not in elem_role:
matches = False
if name and matches:
elem_name = (accessible.name or "").lower()
if name.lower() not in elem_name:
matches = False
if matches and (role or name):
results.append(accessible)
for i in range(accessible.childCount):
if len(results) >= limit:
break
child = accessible.getChildAtIndex(i)
if child:
find_elements(child, role, name, results, max_depth, depth + 1, limit)
except Exception:
pass
return results
def print_element(info, all_states=False):
"""Print element info in readable format."""
print(f"\n[{info['role']}] {info['name'] or '(unnamed)'}")