108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// Linux: AT-SPI 2 through python3 + gi (Atspi), a fixed script. The
|
|
// accessibility bus is per session and found through the X display, so the
|
|
// display discovered for computer:windows is handed to the helper. A box
|
|
// without the binding says so, with the package to install.
|
|
|
|
const computerTreeKind = "atspi-python"
|
|
|
|
const linuxTreeScript = `
|
|
import json, os, sys
|
|
try:
|
|
import gi
|
|
gi.require_version("Atspi", "2.0")
|
|
from gi.repository import Atspi
|
|
except Exception as e:
|
|
print(json.dumps({"error": "AT-SPI binding not available: %s (install python3-gi and gir1.2-atspi-2.0)" % e})); sys.exit(0)
|
|
pid = int(os.environ["PRIVATAE_TARGET_PID"]); cap = int(os.environ["PRIVATAE_MAX_NODES"])
|
|
count = 0; trunc = False
|
|
def node(el, depth):
|
|
global count, trunc
|
|
if count >= cap or depth > 40:
|
|
trunc = True; return None
|
|
count += 1
|
|
n = {"role": Atspi.role_get_name(el.get_role()).replace(" ", "_")}
|
|
name = el.get_name() or ""
|
|
if name: n["name"] = name
|
|
try:
|
|
st = el.get_state_set()
|
|
n["enabled"] = bool(st.contains(Atspi.StateType.ENABLED))
|
|
if st.contains(Atspi.StateType.FOCUSED): n["focused"] = True
|
|
if not st.contains(Atspi.StateType.SHOWING): n["offscreen"] = True
|
|
except Exception: pass
|
|
try:
|
|
r = el.get_extents(Atspi.CoordType.SCREEN)
|
|
if r.width or r.height: n.update(x=r.x, y=r.y, w=r.width, h=r.height)
|
|
except Exception: pass
|
|
try:
|
|
# Accessible carries a deprecated zero-arg get_text() that shadows
|
|
# Text.get_text(start, end) on the gi class, so call the interface
|
|
# method explicitly.
|
|
t = el.get_text_iface()
|
|
if t is not None:
|
|
v = Atspi.Text.get_text(t, 0, min(Atspi.Text.get_character_count(t), 200))
|
|
if v: n["value"] = v
|
|
except Exception: pass
|
|
kids = []
|
|
try:
|
|
for i in range(el.get_child_count()):
|
|
k = node(el.get_child_at_index(i), depth + 1)
|
|
if k: kids.append(k)
|
|
if trunc: break
|
|
except Exception: pass
|
|
if kids: n["children"] = kids
|
|
return n
|
|
def fail(msg):
|
|
print(json.dumps({"error": msg})); sys.exit(0)
|
|
try:
|
|
desk = Atspi.get_desktop(0)
|
|
n_apps = desk.get_child_count()
|
|
except Exception as e:
|
|
fail("could not reach the accessibility bus: %s" % e)
|
|
app = None
|
|
for i in range(n_apps):
|
|
try:
|
|
a = desk.get_child_at_index(i)
|
|
if a is not None and a.get_process_id() == pid: app = a; break
|
|
except Exception: continue
|
|
if app is None:
|
|
fail("pid %d has no AT-SPI application (not running, no toolkit support, or accessibility off)" % pid)
|
|
wins = []
|
|
try:
|
|
for i in range(app.get_child_count()):
|
|
w = node(app.get_child_at_index(i), 0)
|
|
if w: wins.append(w)
|
|
name = app.get_name() or ""
|
|
except Exception as e:
|
|
fail("the application stopped answering the accessibility bus: %s" % e)
|
|
print(json.dumps({"app": name, "truncated": trunc, "windows": wins}))
|
|
`
|
|
|
|
func readTree(pid, maxNodes int) ([]byte, error) {
|
|
if !execOnPath("python3") {
|
|
return nil, errors.New("python3 not installed — the AT-SPI reader needs python3 with gi (python3-gi, gir1.2-atspi-2.0)")
|
|
}
|
|
env := helperEnv(pid, maxNodes)
|
|
if d := x11Display(); d != "" {
|
|
env = append(env, "DISPLAY="+d)
|
|
}
|
|
out, err := runFixedEnv(env, "python3", "-c", linuxTreeScript)
|
|
if err != nil {
|
|
var ee *exec.ExitError
|
|
if errors.As(err, &ee) && len(ee.Stderr) > 0 {
|
|
return nil, errors.New("AT-SPI walk failed: " + truncate(strings.TrimSpace(string(ee.Stderr)), 300))
|
|
}
|
|
return nil, errors.New("python3 failed: " + err.Error())
|
|
}
|
|
return out, nil
|
|
}
|