import { spawn } from 'node:child_process'; function runHelper({ spawnImpl, python, helper, args, timeoutMs, label }) { return new Promise((resolve, reject) => { const child = spawnImpl(python, [helper, ...args], { stdio: ['ignore', 'pipe', 'pipe'] }); let out = ''; let err = ''; let settled = false; const timer = timeoutMs > 0 ? setTimeout(() => { if (settled) return; settled = true; child.kill('SIGTERM'); setTimeout(() => child.kill('SIGKILL'), 500); reject(new Error(`${label} timed out`)); }, timeoutMs) : null; const done = (fn) => (value) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); fn(value); }; child.stdout.on('data', (d) => { out += d; }); child.stderr.on('data', (d) => { err += d; }); child.on('error', done(reject)); child.on('close', (code) => done(code === 0 ? resolve : reject)(code === 0 ? out : new Error(err.trim() || `${label} exited ${code}`))); }); } export class AtspiProvider { constructor({ python = 'python3', helper = new URL('./py/atspi_snapshot.py', import.meta.url).pathname, spawnImpl = spawn, timeoutMs = 5000 } = {}) { this.python = python; this.helper = helper; this.spawnImpl = spawnImpl; this.timeoutMs = timeoutMs; } async tree({ focusedOnly = true, maxNodes = 400 } = {}) { const out = await runHelper({ spawnImpl: this.spawnImpl, python: this.python, helper: this.helper, args: [focusedOnly ? 'focused' : 'desktop', String(Math.min(400, maxNodes))], timeoutMs: this.timeoutMs, label: 'AT-SPI' }); return JSON.parse(String(out)); } async action(target, action) { const helper = this.helper.replace('atspi_snapshot.py', 'atspi_action.py'); const out = await runHelper({ spawnImpl: this.spawnImpl, python: this.python, helper, args: [JSON.stringify(target), String(action)], timeoutMs: this.timeoutMs, label: 'AT-SPI action' }); return JSON.parse(String(out)); } }