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>
143 lines
4.1 KiB
Python
Executable file
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()
|