This commit is contained in:
2026-09-11 14:20:57 -04:00
parent 275a44975f
commit 78cb03e6cb
22 changed files with 248 additions and 25 deletions
@@ -8,6 +8,7 @@ import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import St from 'gi://St';
import Clutter from 'gi://Clutter';
import { installShellService } from './shell-dbus.js';
const BUS = 'io.qvac.Jarvis';
const PATH = '/io/qvac/Jarvis';
@@ -76,6 +77,7 @@ export default class JarvisExtension extends Extension {
enable() {
this._theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); this._stylesheet = Gio.File.new_for_path(`${this.path}/stylesheet.css`); try { this._theme.load_stylesheet(this._stylesheet); } catch (error) { log(`Jarvis stylesheet unavailable: ${error.message}`); }
this.settings = this.getSettings(); this.proxy = new JarvisProxy(); this.overlay = new ArcOverlay(); this.overlay.attach(); this._applyAccessibility();
this._removeShellService = installShellService();
this._indicator = new PanelMenu.Button(0.0, 'Jarvis QVAC', true); this._indicator.accessible_name = 'Jarvis voice assistant';
this._glyph = new St.Label({ text: '◯', style_class: 'jarvis-panel-glyph', y_align: Clutter.ActorAlign.CENTER }); this._glyph.accessible_name = 'Jarvis idle'; this._indicator.add_child(this._glyph);
Main.panel.addToStatusArea('jarvis-qvac', this._indicator, 0, 'right');
@@ -94,5 +96,5 @@ export default class JarvisExtension extends Extension {
_applyAccent() { this.overlay.root.set_style(`--jarvis-accent: ${this.settings.get_string('accent-color')};`); }
_applyAccessibility() { try { const desktop = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' }); this.overlay.reducedMotion = desktop.list_keys().includes('enable-animations') && !desktop.get_boolean('enable-animations'); const theme = desktop.list_keys().includes('gtk-theme') ? desktop.get_string('gtk-theme') : ''; if (/high.?contrast/i.test(theme)) this.overlay.root.add_style_class_name('jarvis-high-contrast'); } catch {} this.overlay.root.connect('key-press-event', (_actor, event) => { if (event.get_key_symbol() === Clutter.KEY_Escape) { this._call('Cancel'); this.overlay.hide(); return Clutter.EVENT_STOP; } return Clutter.EVENT_PROPAGATE; }); }
_buildMenu() { const menu = this._indicator.menu; menu.removeAll(); for (const [label, action] of [['Open ARC', () => this.overlay.show()], ['Stop', () => this._call('Cancel')], ['Privacy mode', () => this._call('Sleep')], ['Settings', () => this.openPreferences()]]) { const item = new PopupMenu.PopupMenuItem(label); item.connect('activate', action); menu.addMenuItem(item); } menu.open(); }
disable() { try { Main.wm.removeKeybinding(this._keyName); } catch {} if (this._settingsChanged) this.settings.disconnect(this._settingsChanged); this._signals?.forEach((id) => this.proxy?.proxy?.disconnect(id)); this.proxy?.close(); this.overlay?.destroy(); this._indicator?.destroy(); if (this._theme && this._stylesheet) { try { this._theme.unload_stylesheet(this._stylesheet); } catch {} } this.overlay = this._indicator = this._glyph = this.proxy = null; }
disable() { this._removeShellService?.(); try { Main.wm.removeKeybinding(this._keyName); } catch {} if (this._settingsChanged) this.settings.disconnect(this._settingsChanged); this._signals?.forEach((id) => this.proxy?.proxy?.disconnect(id)); this.proxy?.close(); this.overlay?.destroy(); this._indicator?.destroy(); if (this._theme && this._stylesheet) { try { this._theme.unload_stylesheet(this._stylesheet); } catch {} } this.overlay = this._indicator = this._glyph = this.proxy = null; }
}
@@ -0,0 +1,17 @@
import Gio from 'gi://Gio';
import Meta from 'gi://Meta';
const BUS = 'io.qvac.Jarvis.Shell';
const PATH = '/io/qvac/Jarvis/Shell';
const XML = `<node><interface name="io.qvac.Jarvis.Shell"><method name="ListWindows"><arg type="s" direction="out"/></method><method name="FocusedWindow"><arg type="s" direction="out"/></method><method name="FocusWindow"><arg type="t" direction="in"/></method></interface></node>`;
const describe = (window) => ({ id: Number(window.get_id?.() || 0), title: window.get_title?.() || '', wm_class: window.get_wm_class?.() || '', pid: Number(window.get_pid?.() || 0), rect: (() => { const r = window.get_frame_rect?.(); return r ? [r.x, r.y, r.width, r.height] : null; })(), focused: window === global.display.get_focus_window?.() });
export function installShellService() {
const implementation = {
ListWindows() { const windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).map(describe); return JSON.stringify(windows); },
FocusedWindow() { const window = global.display.get_focus_window?.(); return JSON.stringify(window ? describe(window) : null); },
FocusWindow(id) { const window = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).find((candidate) => Number(candidate.get_id?.()) === Number(id)); if (!window) throw new Error('window not found'); window.activate(global.get_current_time()); },
};
const ownerId = Gio.bus_own_name(Gio.BusType.SESSION, BUS, Gio.BusNameOwnerFlags.NONE, (connection) => { const exported = Gio.DBusExportedObject.wrapJSObject(XML, implementation); exported.export(connection, PATH); implementation._exported = exported; }, null, () => {});
return () => { implementation._exported?.unexport(); Gio.bus_unown_name(ownerId); };
}