182 lines
7.4 KiB
JavaScript
182 lines
7.4 KiB
JavaScript
import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
|
import Adw from 'gi://Adw';
|
|
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
import Gtk from 'gi://Gtk';
|
|
|
|
const BIND = Gio.SettingsBindFlags.DEFAULT;
|
|
const BUS = 'io.qvac.Jarvis';
|
|
const PATH = '/io/qvac/Jarvis';
|
|
const IFACE = 'io.qvac.Jarvis.Session';
|
|
const PRIVACY_MODES = ['full-listen-after-wake', 'wake-only', 'off'];
|
|
const OVERLAY_STYLES = ['tray', 'expanded'];
|
|
const COMPUTER_MODES = ['off', 'observe', 'act'];
|
|
const MODEL_PROFILES = ['laptop-8gb', 'laptop-16gb', 'desktop-gpu'];
|
|
|
|
function persistDaemonConfig(settings) {
|
|
const dir = GLib.build_filenamev([GLib.get_user_config_dir(), 'jarvis']);
|
|
GLib.mkdir_with_parents(dir, 0o755);
|
|
const file = Gio.File.new_for_path(GLib.build_filenamev([dir, 'config.json']));
|
|
let config = {};
|
|
try {
|
|
const [, contents] = file.load_contents(null);
|
|
const text = typeof contents === 'string' ? contents : new TextDecoder().decode(contents);
|
|
config = JSON.parse(text);
|
|
} catch {}
|
|
config.wakePhrase = settings.get_string('wake-phrase');
|
|
config.aliases = settings.get_strv('aliases');
|
|
config.ttsEnabled = settings.get_boolean('tts-enabled');
|
|
config.modelProfile = settings.get_string('model-profile');
|
|
file.replace_contents(`${JSON.stringify(config, null, 2)}\n`, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
|
}
|
|
|
|
function entryRow(settings, title, key) {
|
|
const row = new Adw.EntryRow({ title });
|
|
row.set_text(settings.get_string(key));
|
|
row.connect('changed', () => settings.set_string(key, row.get_text()));
|
|
return row;
|
|
}
|
|
|
|
function strvRow(settings, title, key) {
|
|
const row = new Adw.EntryRow({ title });
|
|
row.set_text(settings.get_strv(key).join(', '));
|
|
row.connect('changed', () => {
|
|
const values = row.get_text().split(',').map((item) => item.trim()).filter(Boolean);
|
|
settings.set_strv(key, values);
|
|
});
|
|
return row;
|
|
}
|
|
|
|
function switchRow(settings, title, subtitle, key) {
|
|
const row = new Adw.SwitchRow({ title, subtitle });
|
|
settings.bind(key, row, 'active', BIND);
|
|
return row;
|
|
}
|
|
|
|
function comboRow(settings, title, subtitle, key, values) {
|
|
const row = new Adw.ComboRow({ title, subtitle, model: Gtk.StringList.new(values) });
|
|
const current = settings.get_string(key);
|
|
row.selected = Math.max(0, values.indexOf(current));
|
|
row.connect('notify::selected', () => {
|
|
const value = values[row.selected];
|
|
if (value) settings.set_string(key, value);
|
|
});
|
|
return row;
|
|
}
|
|
|
|
function callDaemon(name, signature, values, onDone) {
|
|
Gio.DBus.session.call(
|
|
BUS,
|
|
PATH,
|
|
IFACE,
|
|
name,
|
|
signature ? GLib.Variant.new(signature, values) : null,
|
|
null,
|
|
Gio.DBusCallFlags.NONE,
|
|
4000,
|
|
null,
|
|
(_source, result) => {
|
|
try {
|
|
const reply = Gio.DBus.session.call_finish(result);
|
|
onDone?.(null, reply);
|
|
} catch (error) {
|
|
onDone?.(error);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function grantRow() {
|
|
const row = new Adw.ActionRow({
|
|
title: 'Desktop grant',
|
|
subtitle: 'Off. Jarvis cannot click or type until you allow it here.',
|
|
});
|
|
const allow = new Gtk.Button({ label: 'Allow now', valign: Gtk.Align.CENTER });
|
|
allow.add_css_class('suggested-action');
|
|
const revoke = new Gtk.Button({ label: 'Revoke', valign: Gtk.Align.CENTER });
|
|
revoke.add_css_class('destructive-action');
|
|
const refresh = () => {
|
|
callDaemon('ComputerStatus', null, null, (error, reply) => {
|
|
if (error) {
|
|
row.subtitle = 'Jarvis daemon is unavailable. Start jarvisd, then try Allow now.';
|
|
return;
|
|
}
|
|
let status = {};
|
|
try {
|
|
const unpacked = reply.deep_unpack?.() ?? reply.unpack?.();
|
|
const raw = Array.isArray(unpacked) ? unpacked[0] : unpacked;
|
|
status = JSON.parse(String(raw || '{}'));
|
|
} catch {}
|
|
if (status.active) {
|
|
row.subtitle = `Active. ${Number(status.steps_used) || 0} of ${Number(status.steps_max) || 20} steps used.`;
|
|
} else {
|
|
row.subtitle = 'Off. Press Allow now so Jarvis can observe or control the desktop for three minutes.';
|
|
}
|
|
});
|
|
};
|
|
allow.connect('clicked', () => {
|
|
callDaemon('ComputerGrant', '(b)', [true], (error) => {
|
|
row.subtitle = error ? `Could not grant: ${error.message}` : 'Grant requested. A portal prompt may appear.';
|
|
refresh();
|
|
});
|
|
});
|
|
revoke.connect('clicked', () => {
|
|
callDaemon('ComputerRevoke', null, null, (error) => {
|
|
row.subtitle = error ? `Could not revoke: ${error.message}` : 'Grant revoked.';
|
|
refresh();
|
|
});
|
|
});
|
|
row.add_suffix(allow);
|
|
row.add_suffix(revoke);
|
|
row.activatable_widget = allow;
|
|
refresh();
|
|
return row;
|
|
}
|
|
|
|
export default class JarvisPreferences extends ExtensionPreferences {
|
|
fillPreferencesWindow(window) {
|
|
window.set_title('Jarvis QVAC');
|
|
const settings = this.getSettings();
|
|
const voice = new Adw.PreferencesPage({ title: 'Voice', name: 'voice' });
|
|
const voiceGroup = new Adw.PreferencesGroup({ title: 'Speech and wake' });
|
|
voiceGroup.add(switchRow(settings, 'Spoken replies', 'Play Jarvis replies through local TTS', 'tts-enabled'));
|
|
voiceGroup.add(switchRow(settings, 'Wake chime', 'Play a short chime when Jarvis starts listening', 'chime-enabled'));
|
|
voiceGroup.add(entryRow(settings, 'Wake phrase', 'wake-phrase'));
|
|
voiceGroup.add(strvRow(settings, 'Wake aliases', 'aliases'));
|
|
voiceGroup.add(entryRow(settings, 'Voice id', 'voice-id'));
|
|
voiceGroup.add(entryRow(settings, 'Language', 'language'));
|
|
voice.add(voiceGroup);
|
|
|
|
const desktop = new Adw.PreferencesPage({ title: 'Desktop', name: 'desktop' });
|
|
const desktopGroup = new Adw.PreferencesGroup({ title: 'Overlay and shortcuts' });
|
|
desktopGroup.add(strvRow(settings, 'Hotkey', 'hotkey'));
|
|
desktopGroup.add(entryRow(settings, 'Accent color', 'accent-color'));
|
|
desktopGroup.add(comboRow(settings, 'Desktop layout', 'Tray keeps Jarvis in the top bar; expanded opens a conversation panel', 'overlay-style', OVERLAY_STYLES));
|
|
desktopGroup.add(switchRow(settings, 'Confirm destructive actions', 'Ask before write, delete, or computer-use changes', 'confirm-destructive'));
|
|
desktop.add(desktopGroup);
|
|
|
|
const computer = new Adw.PreferencesPage({ title: 'Computer use', name: 'computer' });
|
|
const computerGroup = new Adw.PreferencesGroup({ title: 'Desktop control' });
|
|
computerGroup.add(comboRow(settings, 'Computer use mode', 'Observe is read-only. Act can click and type only after you press Allow now.', 'computer-use-mode', COMPUTER_MODES));
|
|
computerGroup.add(grantRow());
|
|
computerGroup.add(switchRow(settings, 'Legacy input', 'Use the older input backend when portals are unavailable', 'computer-use-legacy-input'));
|
|
computerGroup.add(comboRow(settings, 'Privacy mode', 'How long Jarvis keeps the microphone open after wake', 'privacy-mode', PRIVACY_MODES));
|
|
computer.add(computerGroup);
|
|
|
|
const models = new Adw.PreferencesPage({ title: 'Models', name: 'models' });
|
|
const modelGroup = new Adw.PreferencesGroup({ title: 'Local profile' });
|
|
modelGroup.add(comboRow(settings, 'Model profile', 'Restart jarvisd after changing the QVAC profile', 'model-profile', MODEL_PROFILES));
|
|
models.add(modelGroup);
|
|
|
|
window.add(voice);
|
|
window.add(desktop);
|
|
window.add(computer);
|
|
window.add(models);
|
|
|
|
persistDaemonConfig(settings);
|
|
for (const key of ['wake-phrase', 'aliases', 'tts-enabled', 'model-profile']) {
|
|
settings.connect(`changed::${key}`, () => persistDaemonConfig(settings));
|
|
}
|
|
}
|
|
}
|