skills/skills/ui-query/scripts/list-windows.py
dan 6eee2be66e feat(skills): add ui-query skill with list-windows
Initial AT-SPI integration for semantic UI access:
- list-windows.py: enumerate windows via accessibility tree
- Wrapper script handles nix dependencies (pyatspi, pygobject3)
- Outputs table or JSON with window geometry and states

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 11:57:03 -08:00

143 lines
4.1 KiB
Python
Executable file

#!/usr/bin/env python3
"""List windows visible to AT-SPI accessibility tree.
Usage:
list-windows.py [--json] [--verbose]
Options:
--json Output as JSON
--verbose Include child element counts
"""
import argparse
import json
import sys
import pyatspi
def get_window_info(window, verbose=False):
"""Extract info from a window accessible."""
info = {
"name": window.name or "(unnamed)",
"role": window.getRoleName(),
}
# Get state
state = window.getState()
states = []
for s in pyatspi.STATE_VALUE_TO_NAME:
if state.contains(s):
states.append(pyatspi.STATE_VALUE_TO_NAME[s])
info["states"] = states
# Get position/size if available
try:
component = window.queryComponent()
if component:
rect = component.getExtents(pyatspi.DESKTOP_COORDS)
info["geometry"] = {
"x": rect.x,
"y": rect.y,
"width": rect.width,
"height": rect.height,
}
except Exception:
pass
if verbose:
# Count children recursively (expensive)
def count_children(acc):
try:
count = acc.childCount
for i in range(acc.childCount):
child = acc.getChildAtIndex(i)
if child:
count += count_children(child)
return count
except Exception:
return 0
info["child_count"] = count_children(window)
return info
def list_windows(verbose=False):
"""Get all windows from AT-SPI desktop."""
desktop = pyatspi.Registry.getDesktop(0)
apps = []
for i in range(desktop.childCount):
app = desktop.getChildAtIndex(i)
if not app:
continue
app_info = {
"name": app.name or "(unnamed)",
"windows": [],
}
for j in range(app.childCount):
window = app.getChildAtIndex(j)
if window:
window_info = get_window_info(window, verbose)
app_info["windows"].append(window_info)
if app_info["windows"]: # Only include apps with windows
apps.append(app_info)
return apps
def print_table(apps):
"""Print apps/windows as formatted table."""
if not apps:
print("No windows found via AT-SPI.")
print("\nTips:")
print(" - Apps started before AT-SPI enablement won't register")
print(" - GTK/Qt apps should work; Electron varies")
print(" - Try: accerciser (GUI) to explore the tree")
return
for app in apps:
print(f"\n{app['name']}")
print("-" * len(app["name"]))
for win in app["windows"]:
geo = win.get("geometry", {})
geo_str = f"{geo.get('width', '?')}x{geo.get('height', '?')}" if geo else ""
states_str = ", ".join(win.get("states", [])[:3]) # First 3 states
line = f" [{win['role']}] {win['name']}"
if geo_str:
line += f" ({geo_str})"
if states_str:
line += f" [{states_str}]"
if "child_count" in win:
line += f" ({win['child_count']} elements)"
print(line)
def main():
parser = argparse.ArgumentParser(description="List AT-SPI accessible windows")
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)")
args = parser.parse_args()
try:
apps = list_windows(verbose=args.verbose)
except Exception as e:
print(f"Error accessing AT-SPI: {e}", file=sys.stderr)
print("\nIs AT-SPI enabled?", file=sys.stderr)
print(" Check: dbus-send --session --dest=org.a11y.Bus --print-reply /org/a11y/bus org.freedesktop.DBus.Peer.Ping", file=sys.stderr)
sys.exit(1)
if args.json:
print(json.dumps(apps, indent=2))
else:
print_table(apps)
if __name__ == "__main__":
main()