import { spawn } from 'node:child_process'; const DEST = 'io.qvac.Jarvis.Shell'; const PATH = '/io/qvac/Jarvis/Shell'; const IFACE = 'io.qvac.Jarvis.Shell'; function parseGdbusString(out) { const match = String(out).match(/'((?:\\.|[^'\\])*)'/); if (!match) throw new Error('GNOME Shell helper returned no string'); return match[1].replace(/\\(.)/g, '$1'); } export class ShellProvider { constructor({ listWindows, focusedWindow, focusWindow, proxy, spawnImpl = spawn, timeoutMs = 2000 } = {}) { this.listWindowsImpl = listWindows; this.focusedWindowImpl = focusedWindow; this.focusWindowImpl = focusWindow; this.proxy = proxy; this.spawnImpl = spawnImpl; this.timeoutMs = timeoutMs; } _call(method, args = []) { return new Promise((resolve, reject) => { const child = this.spawnImpl('gdbus', ['call', '--session', '--dest', DEST, '--object-path', PATH, '--method', `${IFACE}.${method}`, ...args], { stdio: ['ignore', 'pipe', 'pipe'] }); let out = ''; let err = ''; let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; child.kill('SIGTERM'); reject(new Error('GNOME Shell helper timed out')); }, this.timeoutMs); const done = (fn) => (value) => { if (settled) return; settled = true; 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) => { try { if (code !== 0) throw new Error(err.trim() || `GNOME Shell helper exited ${code}`); done(resolve)(method === 'FocusWindow' ? true : parseGdbusString(out)); } catch (error) { done(reject)(error); } }); }); } async connect() { if (this.proxy || this.listWindowsImpl) return this; await this._call('ListWindows'); return this; } async windows() { if (this.proxy) return { available: true, windows: JSON.parse(await this.proxy.ListWindows()) }; if (this.listWindowsImpl) return { available: true, windows: await this.listWindowsImpl() }; try { return { available: true, windows: JSON.parse(await this._call('ListWindows')) }; } catch (error) { return { available: false, reason: error.message, windows: [] }; } } async focused() { if (this.proxy) return JSON.parse(await this.proxy.FocusedWindow()); if (this.focusedWindowImpl) return this.focusedWindowImpl(); try { return JSON.parse(await this._call('FocusedWindow')); } catch { return null; } } async focus(id) { if (this.proxy) return this.proxy.FocusWindow(BigInt(id)); if (this.focusWindowImpl) return this.focusWindowImpl(id); await this._call('FocusWindow', [String(id)]); } }