@@ -1,4 +1,6 @@
|
|||||||
# JARVIS-QVAC
|
# Jarvis
|
||||||
|
|
||||||
|
Local voice for your desktop.
|
||||||
|
|
||||||
JARVIS-QVAC is a local-first voice assistant and computer-use layer for Ubuntu
|
JARVIS-QVAC is a local-first voice assistant and computer-use layer for Ubuntu
|
||||||
GNOME. It combines a wake-word loop, QVAC speech and language models, the
|
GNOME. It combines a wake-word loop, QVAC speech and language models, the
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
Run `python3 apps/control-center/main.py` (or `gjs -m apps/control-center/main.js`).
|
Run `python3 apps/control-center/main.py` (or `gjs -m apps/control-center/main.js`).
|
||||||
The Control Center and the GNOME extension's Settings button now open the same
|
The Control Center and the GNOME extension's Settings button now open the same
|
||||||
Libadwaita settings pages and use the same configuration catalog. Requires GJS,
|
Libadwaita settings pages and use the same configuration catalog. Requires GJS,
|
||||||
GTK 4, and Libadwaita 1.4 or newer.
|
GTK 4, and Libadwaita 1.4 or newer. The window uses the Jarvis mark, Gold accent,
|
||||||
|
and tagline “Local voice for your desktop.”
|
||||||
|
|
||||||
Voice, listening, and desktop settings are saved to
|
Voice, listening, and desktop settings are saved to
|
||||||
`$XDG_CONFIG_HOME/jarvis/config.json` (default `~/.config/jarvis/config.json`).
|
`$XDG_CONFIG_HOME/jarvis/config.json` (default `~/.config/jarvis/config.json`).
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
#!/usr/bin/gjs -m
|
#!/usr/bin/gjs -m
|
||||||
import Adw from 'gi://Adw';
|
import Adw from 'gi://Adw';
|
||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
|
import Gtk from 'gi://Gtk';
|
||||||
|
import Gdk from 'gi://Gdk';
|
||||||
import { fillSettingsWindow } from '../gnome-extension/[email protected]/settings-window.js';
|
import { fillSettingsWindow } from '../gnome-extension/[email protected]/settings-window.js';
|
||||||
|
|
||||||
const directory = Gio.File.new_for_uri(import.meta.url).get_parent().get_parent().get_child('gnome-extension').get_child('[email protected]').get_path();
|
const directory = Gio.File.new_for_uri(import.meta.url).get_parent().get_parent().get_child('gnome-extension').get_child('[email protected]').get_path();
|
||||||
const application = new Adw.Application({ application_id: 'io.qvac.Jarvis.Control', flags: Gio.ApplicationFlags.DEFAULT_FLAGS });
|
const application = new Adw.Application({ application_id: 'io.qvac.Jarvis.Control', flags: Gio.ApplicationFlags.DEFAULT_FLAGS });
|
||||||
let window;
|
let window;
|
||||||
|
application.connect('startup', () => {
|
||||||
|
try {
|
||||||
|
Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).add_search_path(`${directory}/brand/icons`);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
application.connect('activate', () => {
|
application.connect('activate', () => {
|
||||||
if (!window) {
|
if (!window) {
|
||||||
const source = Gio.SettingsSchemaSource.new_from_directory(`${directory}/schemas`, Gio.SettingsSchemaSource.get_default(), false);
|
const source = Gio.SettingsSchemaSource.new_from_directory(`${directory}/schemas`, Gio.SettingsSchemaSource.get_default(), false);
|
||||||
const settings = new Gio.Settings({ settings_schema: source.lookup('org.gnome.shell.extensions.jarvis', true) });
|
const settings = new Gio.Settings({ settings_schema: source.lookup('org.gnome.shell.extensions.jarvis', true) });
|
||||||
window = new Adw.PreferencesWindow({ application });
|
window = new Adw.PreferencesWindow({ application, icon_name: 'io.qvac.Jarvis.Control' });
|
||||||
fillSettingsWindow(window, settings, directory);
|
fillSettingsWindow(window, settings, directory);
|
||||||
window.connect('close-request', () => { window = null; return false; });
|
window.connect('close-request', () => { window = null; return false; });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export const BRAND = {
|
||||||
|
name: 'Jarvis',
|
||||||
|
product: 'Jarvis QVAC',
|
||||||
|
tagline: 'Local voice for your desktop',
|
||||||
|
applicationId: 'io.qvac.Jarvis.Control',
|
||||||
|
uuid: '[email protected]',
|
||||||
|
colors: {
|
||||||
|
gold: '#F4B942',
|
||||||
|
goldBright: '#FFE09A',
|
||||||
|
goldDeep: '#C48A1A',
|
||||||
|
signal: '#4FD2FF',
|
||||||
|
ink: '#0B0E14',
|
||||||
|
slate: '#16191F',
|
||||||
|
paper: '#F6F7FB',
|
||||||
|
mute: '#AEB6C8',
|
||||||
|
dim: '#9AA8C0',
|
||||||
|
},
|
||||||
|
accents: [
|
||||||
|
{ id: 'gold', label: 'Gold', value: '#F4B942' },
|
||||||
|
{ id: 'signal', label: 'Signal', value: '#4FD2FF' },
|
||||||
|
{ id: 'copper', label: 'Copper', value: '#E07A3D' },
|
||||||
|
{ id: 'jade', label: 'Jade', value: '#3DDC97' },
|
||||||
|
{ id: 'rose', label: 'Rose', value: '#F071A8' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_ACCENT = BRAND.colors.gold;
|
||||||
|
export const SIGNAL = BRAND.colors.signal;
|
||||||
|
|
||||||
|
export function normalizeAccent(value, fallback = DEFAULT_ACCENT) {
|
||||||
|
const color = String(value || '').trim();
|
||||||
|
return /^#[0-9A-Fa-f]{6}$/.test(color) ? color : fallback;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
@define-color jarvis_gold #F4B942;
|
||||||
|
@define-color jarvis_gold_bright #FFE09A;
|
||||||
|
@define-color jarvis_signal #4FD2FF;
|
||||||
|
@define-color jarvis_ink #0B0E14;
|
||||||
|
@define-color jarvis_slate #16191F;
|
||||||
|
@define-color accent_color @jarvis_gold;
|
||||||
|
@define-color accent_bg_color @jarvis_gold;
|
||||||
|
@define-color accent_fg_color @jarvis_slate;
|
||||||
|
|
||||||
|
.jarvis-brand-name {
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jarvis-brand-tagline {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.jarvis-swatch {
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid alpha(#ffffff, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.jarvis-swatch-gold { background: #F4B942; }
|
||||||
|
button.jarvis-swatch-signal { background: #4FD2FF; }
|
||||||
|
button.jarvis-swatch-copper { background: #E07A3D; }
|
||||||
|
button.jarvis-swatch-jade { background: #3DDC97; }
|
||||||
|
button.jarvis-swatch-rose { background: #F071A8; }
|
||||||
|
|
||||||
|
button.jarvis-swatch:hover {
|
||||||
|
border-color: alpha(#ffffff, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.jarvis-swatch:checked,
|
||||||
|
button.jarvis-swatch:active {
|
||||||
|
box-shadow: 0 0 0 2px @jarvis_ink, 0 0 0 4px @jarvis_gold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Jarvis">
|
||||||
|
<title>Jarvis</title>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="glow" cx="50%" cy="42%" r="48%">
|
||||||
|
<stop offset="0%" stop-color="#4FD2FF" stop-opacity=".22"/>
|
||||||
|
<stop offset="100%" stop-color="#0B0E14" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0B0E14"/>
|
||||||
|
<rect x="5" y="5" width="118" height="118" rx="24" fill="none" stroke="#F4B942" stroke-opacity=".2" stroke-width="1.5"/>
|
||||||
|
<circle cx="64" cy="58" r="42" fill="url(#glow)"/>
|
||||||
|
<g transform="translate(32 22) scale(1)">
|
||||||
|
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<circle cx="34" cy="33" r="5.5" fill="#4FD2FF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 971 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" role="img" aria-label="Jarvis">
|
||||||
|
<path d="M3.2 7.4A5.3 5.3 0 0 1 8.5 2.2c2.2 0 4.1 1.33 4.9 3.25" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
||||||
|
<path d="M8.5 2.2v7.7A3.1 3.1 0 0 1 5.4 13" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
|
||||||
|
<circle cx="8.5" cy="8.1" r="1.25" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 415 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" role="img" aria-label="Jarvis">
|
||||||
|
<title>Jarvis</title>
|
||||||
|
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
|
||||||
|
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="34" cy="33" r="5" fill="#4FD2FF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 438 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 64" role="img" aria-label="Jarvis">
|
||||||
|
<title>Jarvis</title>
|
||||||
|
<g transform="translate(8 10) scale(.68)">
|
||||||
|
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" fill="none" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
|
||||||
|
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" fill="none" stroke="#F4B942" stroke-width="5.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="34" cy="33" r="5" fill="#4FD2FF"/>
|
||||||
|
</g>
|
||||||
|
<text x="64" y="42" fill="#F6F7FB" font-family="Adwaita Sans, Cantarell, sans-serif" font-size="26" font-weight="700" letter-spacing="3.2">JARVIS</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 664 B |
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Jarvis">
|
||||||
|
<title>Jarvis</title>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="glow" cx="50%" cy="42%" r="48%">
|
||||||
|
<stop offset="0%" stop-color="#4FD2FF" stop-opacity=".22"/>
|
||||||
|
<stop offset="100%" stop-color="#0B0E14" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="128" height="128" rx="28" fill="#0B0E14"/>
|
||||||
|
<rect x="5" y="5" width="118" height="118" rx="24" fill="none" stroke="#F4B942" stroke-opacity=".2" stroke-width="1.5"/>
|
||||||
|
<circle cx="64" cy="58" r="42" fill="url(#glow)"/>
|
||||||
|
<g transform="translate(32 22) scale(1)">
|
||||||
|
<path d="M14.5 30.5c0-10.77 8.73-19.5 19.5-19.5 8.05 0 14.95 4.88 17.92 11.82" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<path d="M34 11v29.5c0 6.35-5.15 11.5-11.5 11.5" fill="none" stroke="#F4B942" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<circle cx="34" cy="33" r="5.5" fill="#4FD2FF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 971 B |
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "Jarvis",
|
||||||
|
"product": "Jarvis QVAC",
|
||||||
|
"tagline": "Local voice for your desktop",
|
||||||
|
"voice": "Precise, warm, local-first. Gold is presence. Cyan is thought.",
|
||||||
|
"applicationId": "io.qvac.Jarvis.Control",
|
||||||
|
"uuid": "[email protected]",
|
||||||
|
"colors": {
|
||||||
|
"gold": "#F4B942",
|
||||||
|
"goldBright": "#FFE09A",
|
||||||
|
"goldDeep": "#C48A1A",
|
||||||
|
"signal": "#4FD2FF",
|
||||||
|
"ink": "#0B0E14",
|
||||||
|
"slate": "#16191F",
|
||||||
|
"paper": "#F6F7FB",
|
||||||
|
"mute": "#AEB6C8",
|
||||||
|
"dim": "#9AA8C0"
|
||||||
|
},
|
||||||
|
"accents": [
|
||||||
|
{ "id": "gold", "label": "Gold", "value": "#F4B942" },
|
||||||
|
{ "id": "signal", "label": "Signal", "value": "#4FD2FF" },
|
||||||
|
{ "id": "copper", "label": "Copper", "value": "#E07A3D" },
|
||||||
|
{ "id": "jade", "label": "Jade", "value": "#3DDC97" },
|
||||||
|
{ "id": "rose", "label": "Rose", "value": "#F071A8" }
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"ui": "Adwaita Sans / Cantarell",
|
||||||
|
"wordmarkTracking": "0.8px",
|
||||||
|
"wordmarkWeight": 700
|
||||||
|
},
|
||||||
|
"radius": {
|
||||||
|
"hud": 16,
|
||||||
|
"chip": 999,
|
||||||
|
"row": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ import { ConversationView, JarvisOsd, ComputerUseChrome, SessionPanel, coerceTex
|
|||||||
const BUS = 'io.qvac.Jarvis';
|
const BUS = 'io.qvac.Jarvis';
|
||||||
const PATH = '/io/qvac/Jarvis';
|
const PATH = '/io/qvac/Jarvis';
|
||||||
const IFACE = 'io.qvac.Jarvis.Session';
|
const IFACE = 'io.qvac.Jarvis.Session';
|
||||||
const GLYPHS = { ARMED: '◯', LISTENING: '◌', THINKING: '◉', SPEAKING: '◎', SLEEPING: '◐' };
|
const DEFAULT_ACCENT = '#F4B942';
|
||||||
|
const SIGNAL = '#4FD2FF';
|
||||||
|
|
||||||
class JarvisProxy {
|
class JarvisProxy {
|
||||||
async connect() {
|
async connect() {
|
||||||
@@ -55,17 +56,23 @@ export default class JarvisExtension extends Extension {
|
|||||||
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._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.settings = this.getSettings();
|
||||||
this.proxy = new JarvisProxy();
|
this.proxy = new JarvisProxy();
|
||||||
this.popup = new ConversationView({ compact: true });
|
this.popup = new ConversationView({ compact: true, brandDir: this.path });
|
||||||
this.osd = new JarvisOsd();
|
this.osd = new JarvisOsd();
|
||||||
this.cu = new ComputerUseChrome(); this.cu.attach();
|
this.cu = new ComputerUseChrome(); this.cu.attach();
|
||||||
this.session = new SessionPanel(); this.session.attach();
|
this.session = new SessionPanel({ brandDir: this.path }); this.session.attach();
|
||||||
this._bindSurface(this.popup);
|
this._bindSurface(this.popup);
|
||||||
this._bindSurface(this.session.view);
|
this._bindSurface(this.session.view);
|
||||||
this._applyAccessibility();
|
this._applyAccessibility();
|
||||||
this._removeShellService = installShellService();
|
this._removeShellService = installShellService();
|
||||||
this._indicator = new PanelMenu.Button(0.0, 'Jarvis QVAC', false); this._indicator.accessible_name = 'Jarvis voice assistant';
|
this._indicator = new PanelMenu.Button(0.0, 'Jarvis QVAC', false); this._indicator.accessible_name = 'Jarvis voice assistant';
|
||||||
this._panelBox = new St.BoxLayout({ style_class: 'jarvis-panel-box', y_align: Clutter.ActorAlign.CENTER });
|
this._panelBox = new St.BoxLayout({ style_class: 'jarvis-panel-box', y_align: Clutter.ActorAlign.CENTER });
|
||||||
this._glyph = new St.Label({ text: '◯ Jarvis', style_class: 'jarvis-panel-glyph', y_align: Clutter.ActorAlign.CENTER }); this._glyph.accessible_name = 'Jarvis idle';
|
this._mark = new St.Icon({ icon_name: 'audio-input-microphone-symbolic', icon_size: 16, style_class: 'jarvis-panel-mark', y_align: Clutter.ActorAlign.CENTER });
|
||||||
|
try {
|
||||||
|
const file = Gio.File.new_for_path(`${this.path}/brand/icons/jarvis-mark.svg`);
|
||||||
|
if (file.query_exists(null)) this._mark.gicon = new Gio.FileIcon({ file });
|
||||||
|
} catch (error) { log(`Jarvis mark unavailable: ${error.message}`); }
|
||||||
|
this._glyph = new St.Label({ text: 'Jarvis', style_class: 'jarvis-panel-glyph', y_align: Clutter.ActorAlign.CENTER }); this._glyph.accessible_name = 'Jarvis idle';
|
||||||
|
this._panelBox.add_child(this._mark);
|
||||||
this._panelBox.add_child(this._glyph);
|
this._panelBox.add_child(this._glyph);
|
||||||
this.osd.attach(this._panelBox);
|
this.osd.attach(this._panelBox);
|
||||||
this._indicator.add_child(this._panelBox);
|
this._indicator.add_child(this._panelBox);
|
||||||
@@ -264,17 +271,27 @@ export default class JarvisExtension extends Extension {
|
|||||||
}
|
}
|
||||||
_setState(state) {
|
_setState(state) {
|
||||||
const value = safeText(state);
|
const value = safeText(state);
|
||||||
this._glyph.text = `${GLYPHS[value] || '◯'} Jarvis`;
|
this._glyph.text = 'Jarvis';
|
||||||
this._glyph.accessible_name = `Jarvis ${value.toLowerCase()}`;
|
this._glyph.accessible_name = `Jarvis ${value.toLowerCase()}`;
|
||||||
|
this._indicator.accessible_name = `Jarvis ${value.toLowerCase()}`;
|
||||||
|
if (this._panelBox) {
|
||||||
|
for (const name of ['armed', 'listening', 'speaking', 'thinking', 'sleeping']) {
|
||||||
|
this._panelBox.remove_style_class_name(`jarvis-state-${name}`);
|
||||||
|
}
|
||||||
|
this._panelBox.add_style_class_name(`jarvis-state-${value.toLowerCase()}`);
|
||||||
|
}
|
||||||
this._eachView((view) => view.setState(value));
|
this._eachView((view) => view.setState(value));
|
||||||
this.osd.setState(value);
|
this.osd.setState(value);
|
||||||
}
|
}
|
||||||
_applyAccent() {
|
_applyAccent() {
|
||||||
const style = `--jarvis-accent: ${this.settings.get_string('accent-color')};`;
|
const color = this.settings.get_string('accent-color');
|
||||||
|
const accent = /^#[0-9A-Fa-f]{6}$/.test(String(color || '')) ? color : DEFAULT_ACCENT;
|
||||||
|
const style = `--jarvis-accent: ${accent}; --jarvis-signal: ${SIGNAL};`;
|
||||||
this.popup.root.set_style(style);
|
this.popup.root.set_style(style);
|
||||||
this.session.root.set_style(style);
|
this.session.root.set_style(style);
|
||||||
this.osd.root.set_style(style);
|
this.osd.root.set_style(style);
|
||||||
this.cu.root.set_style(style);
|
this.cu.root.set_style(style);
|
||||||
|
this._panelBox?.set_style(style);
|
||||||
}
|
}
|
||||||
_applyAccessibility() {
|
_applyAccessibility() {
|
||||||
try {
|
try {
|
||||||
@@ -300,7 +317,7 @@ export default class JarvisExtension extends Extension {
|
|||||||
this.osd?.destroy(); this.cu?.destroy(); this.session?.destroy(); this.popup?.destroy();
|
this.osd?.destroy(); this.cu?.destroy(); this.session?.destroy(); this.popup?.destroy();
|
||||||
this._indicator?.destroy();
|
this._indicator?.destroy();
|
||||||
if (this._theme && this._stylesheet) { try { this._theme.unload_stylesheet(this._stylesheet); } catch {} }
|
if (this._theme && this._stylesheet) { try { this._theme.unload_stylesheet(this._stylesheet); } catch {} }
|
||||||
this.popup = this.session = this.osd = this.cu = this._indicator = this._glyph = this.proxy = null;
|
this.popup = this.session = this.osd = this.cu = this._indicator = this._glyph = this._mark = this.proxy = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"uuid": "[email protected]",
|
"uuid": "[email protected]",
|
||||||
"name": "Jarvis QVAC",
|
"name": "Jarvis",
|
||||||
"description": "Local-first QVAC voice assistant in the GNOME top bar",
|
"description": "Local voice for your desktop. Jarvis QVAC lives in the GNOME top bar.",
|
||||||
"shell-version": ["46", "47", "48", "49", "50"],
|
"shell-version": ["46", "47", "48", "49", "50"],
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"session-modes": ["user"],
|
"session-modes": ["user"],
|
||||||
|
|||||||
@@ -149,8 +149,9 @@ export class SettingsEditor {
|
|||||||
button.connect('clicked', () => { for (const field of fields) { this.setValue(field, field.default); this.rows.get(field.key)?._setValue(field.default); } });
|
button.connect('clicked', () => { for (const field of fields) { this.setValue(field, field.default); this.rows.get(field.key)?._setValue(field.default); } });
|
||||||
reset.add_suffix(button); group.add(reset); page.add(group);
|
reset.add_suffix(button); group.add(reset); page.add(group);
|
||||||
}
|
}
|
||||||
page(parent, title, groups, iconName, preview = false) {
|
page(parent, title, groups, iconName, preview = false, intro = null) {
|
||||||
const page = new Adw.PreferencesPage({ title, icon_name: iconName });
|
const page = new Adw.PreferencesPage({ title, icon_name: iconName });
|
||||||
|
if (intro) page.add(intro);
|
||||||
const fields = this.fields.filter(field => groups.includes(field.group));
|
const fields = this.fields.filter(field => groups.includes(field.group));
|
||||||
this.controls(page, fields, { preview });
|
this.controls(page, fields, { preview });
|
||||||
for (const name of groups) {
|
for (const name of groups) {
|
||||||
|
|||||||
@@ -2,17 +2,61 @@ import Adw from 'gi://Adw';
|
|||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
import GLib from 'gi://GLib';
|
import GLib from 'gi://GLib';
|
||||||
import Gtk from 'gi://Gtk';
|
import Gtk from 'gi://Gtk';
|
||||||
|
import Gdk from 'gi://Gdk';
|
||||||
import { SettingsEditor } from './settings-editor.js';
|
import { SettingsEditor } from './settings-editor.js';
|
||||||
|
import { BRAND, normalizeAccent } from './brand.js';
|
||||||
|
|
||||||
const BUS = 'io.qvac.Jarvis';
|
const BUS = 'io.qvac.Jarvis';
|
||||||
const PATH = '/io/qvac/Jarvis';
|
const PATH = '/io/qvac/Jarvis';
|
||||||
const IFACE = 'io.qvac.Jarvis.Session';
|
const IFACE = 'io.qvac.Jarvis.Session';
|
||||||
const OVERLAY_STYLES = ['tray', 'expanded'];
|
const OVERLAY_STYLES = ['tray', 'expanded'];
|
||||||
|
|
||||||
function entryRow(settings, title, key) {
|
function applyBrandCss(directory) {
|
||||||
const row = new Adw.EntryRow({ title });
|
try {
|
||||||
row.set_text(settings.get_string(key));
|
const css = new Gtk.CssProvider();
|
||||||
row.connect('changed', () => settings.set_string(key, row.get_text()));
|
css.load_from_path(`${directory}/brand/gtk.css`);
|
||||||
|
Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function brandBanner(directory) {
|
||||||
|
const row = new Adw.ActionRow({ title: BRAND.product, subtitle: BRAND.tagline });
|
||||||
|
try {
|
||||||
|
const image = Gtk.Image.new_from_file(`${directory}/brand/icons/jarvis-mark.svg`);
|
||||||
|
image.set_pixel_size(36);
|
||||||
|
row.add_prefix(image);
|
||||||
|
} catch {}
|
||||||
|
row.activatable = false;
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function accentRow(settings) {
|
||||||
|
const current = normalizeAccent(settings.get_string('accent-color'));
|
||||||
|
const row = new Adw.ActionRow({
|
||||||
|
title: 'Accent color',
|
||||||
|
subtitle: 'Gold is the Jarvis default. This tints the HUD immediately.',
|
||||||
|
});
|
||||||
|
const box = new Gtk.Box({ spacing: 6, valign: Gtk.Align.CENTER });
|
||||||
|
for (const accent of BRAND.accents) {
|
||||||
|
const button = new Gtk.Button({ tooltip_text: accent.label, valign: Gtk.Align.CENTER });
|
||||||
|
button.add_css_class('circular');
|
||||||
|
button.add_css_class('jarvis-swatch');
|
||||||
|
button.add_css_class(`jarvis-swatch-${accent.id}`);
|
||||||
|
button.set_size_request(24, 24);
|
||||||
|
button.connect('clicked', () => settings.set_string('accent-color', accent.value));
|
||||||
|
box.append(button);
|
||||||
|
}
|
||||||
|
const entry = new Gtk.Entry({ text: current, width_chars: 8, maxlength: 7, valign: Gtk.Align.CENTER });
|
||||||
|
entry.connect('changed', () => {
|
||||||
|
const value = normalizeAccent(entry.get_text(), '');
|
||||||
|
if (value) settings.set_string('accent-color', value);
|
||||||
|
});
|
||||||
|
settings.connect('changed::accent-color', () => {
|
||||||
|
const value = normalizeAccent(settings.get_string('accent-color'));
|
||||||
|
if (entry.get_text() !== value) entry.set_text(value);
|
||||||
|
});
|
||||||
|
row.add_suffix(box);
|
||||||
|
row.add_suffix(entry);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +151,14 @@ function grantRow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fillSettingsWindow(window, settings, directory) {
|
export function fillSettingsWindow(window, settings, directory) {
|
||||||
window.set_title('Jarvis QVAC');
|
window.set_title(BRAND.product);
|
||||||
window.set_default_size(840, 780);
|
window.set_default_size(840, 780);
|
||||||
window.search_enabled = true;
|
window.search_enabled = true;
|
||||||
|
applyBrandCss(directory);
|
||||||
const editor = new SettingsEditor(directory, settings);
|
const editor = new SettingsEditor(directory, settings);
|
||||||
const voice = editor.page(window, 'Voice', ['Speech', 'Voice design'], 'audio-speakers-symbolic', true);
|
const intro = new Adw.PreferencesGroup();
|
||||||
|
intro.add(brandBanner(directory));
|
||||||
|
const voice = editor.page(window, 'Voice', ['Speech', 'Voice design'], 'audio-speakers-symbolic', true, intro);
|
||||||
window.add(voice);
|
window.add(voice);
|
||||||
const listening = editor.page(window, 'Listening', ['Listening', 'Wake and privacy', 'Detection tuning', 'Audio routing'], 'audio-input-microphone-symbolic');
|
const listening = editor.page(window, 'Listening', ['Listening', 'Wake and privacy', 'Detection tuning', 'Audio routing'], 'audio-input-microphone-symbolic');
|
||||||
window.add(listening);
|
window.add(listening);
|
||||||
@@ -119,7 +166,7 @@ export function fillSettingsWindow(window, settings, directory) {
|
|||||||
const desktop = editor.page(window, 'Desktop', ['Desktop access', 'Desktop images'], 'preferences-desktop-display-symbolic');
|
const desktop = editor.page(window, 'Desktop', ['Desktop access', 'Desktop images'], 'preferences-desktop-display-symbolic');
|
||||||
const desktopGroup = new Adw.PreferencesGroup({ title: 'Overlay and shortcuts', description: 'These appearance settings take effect immediately.' });
|
const desktopGroup = new Adw.PreferencesGroup({ title: 'Overlay and shortcuts', description: 'These appearance settings take effect immediately.' });
|
||||||
desktopGroup.add(strvRow(settings, 'Hotkey', 'hotkey'));
|
desktopGroup.add(strvRow(settings, 'Hotkey', 'hotkey'));
|
||||||
desktopGroup.add(entryRow(settings, 'Accent color', 'accent-color'));
|
desktopGroup.add(accentRow(settings));
|
||||||
desktopGroup.add(comboRow(settings, 'Desktop layout', 'Tray keeps Jarvis in the top bar; expanded opens a conversation panel', 'overlay-style', OVERLAY_STYLES));
|
desktopGroup.add(comboRow(settings, 'Desktop layout', 'Tray keeps Jarvis in the top bar; expanded opens a conversation panel', 'overlay-style', OVERLAY_STYLES));
|
||||||
desktop.add(desktopGroup);
|
desktop.add(desktopGroup);
|
||||||
const grantGroup = new Adw.PreferencesGroup({ title: 'Temporary desktop access' });
|
const grantGroup = new Adw.PreferencesGroup({ title: 'Temporary desktop access' });
|
||||||
|
|||||||
@@ -1,21 +1,61 @@
|
|||||||
.jarvis-panel-box { spacing: 6px; }
|
/* Jarvis brand tokens. Gold is presence; cyan is thought. Accent is overridden live via --jarvis-accent. */
|
||||||
.jarvis-panel-glyph { color: var(--jarvis-accent, #F4B942); font-size: 14px; }
|
.jarvis-panel-box {
|
||||||
.jarvis-panel-state { padding: 1px 8px; border-radius: 999px; background-color: rgba(244, 185, 66, .22); }
|
spacing: 7px;
|
||||||
.jarvis-panel-state-label { color: var(--jarvis-accent, #F4B942); font-size: 12px; font-weight: bold; }
|
--jarvis-signal: #4FD2FF;
|
||||||
|
--jarvis-ink: #0B0E14;
|
||||||
|
--jarvis-slate: #16191F;
|
||||||
|
--jarvis-paper: #F6F7FB;
|
||||||
|
--jarvis-mute: #AEB6C8;
|
||||||
|
--jarvis-dim: #9AA8C0;
|
||||||
|
}
|
||||||
|
.jarvis-panel-mark { color: var(--jarvis-accent, #F4B942); icon-size: 16px; }
|
||||||
|
.jarvis-panel-glyph {
|
||||||
|
color: var(--jarvis-accent, #F4B942);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
}
|
||||||
|
.jarvis-panel-state { padding: 2px 9px; border-radius: 999px; background-color: alpha(var(--jarvis-accent, #F4B942), .22); }
|
||||||
|
.jarvis-panel-state-label { color: var(--jarvis-accent, #F4B942); font-size: 11px; font-weight: 700; letter-spacing: 0.3px; }
|
||||||
|
.jarvis-panel-box.jarvis-state-thinking .jarvis-panel-state,
|
||||||
|
.jarvis-panel-state.jarvis-state-thinking { background-color: alpha(#4FD2FF, .22); }
|
||||||
|
.jarvis-panel-box.jarvis-state-thinking .jarvis-panel-state-label,
|
||||||
|
.jarvis-panel-state.jarvis-state-thinking .jarvis-panel-state-label { color: #4FD2FF; }
|
||||||
|
.jarvis-panel-box.jarvis-state-sleeping .jarvis-panel-state,
|
||||||
|
.jarvis-panel-state.jarvis-state-sleeping { background-color: rgba(174, 182, 200, .18); }
|
||||||
|
.jarvis-panel-box.jarvis-state-sleeping .jarvis-panel-state-label,
|
||||||
|
.jarvis-panel-state.jarvis-state-sleeping .jarvis-panel-state-label { color: #AEB6C8; }
|
||||||
.jarvis-menu { max-width: 360px; }
|
.jarvis-menu { max-width: 360px; }
|
||||||
.jarvis-menu-item { padding: 0; }
|
.jarvis-menu-item { padding: 0; }
|
||||||
.jarvis-popup { width: 320px; padding: 12px 14px; spacing: 8px; color: #f6f7fb; }
|
.jarvis-popup {
|
||||||
.jarvis-session { width: 420px; padding: 14px 16px; spacing: 8px; border-radius: 16px; background-color: rgba(11, 14, 20, .94); border: 1px solid rgba(244, 185, 66, .42); color: #f6f7fb; box-shadow: 0 12px 40px rgba(0, 0, 0, .45); }
|
width: 320px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
spacing: 8px;
|
||||||
|
color: #f6f7fb;
|
||||||
|
--jarvis-signal: #4FD2FF;
|
||||||
|
}
|
||||||
|
.jarvis-session {
|
||||||
|
width: 420px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
spacing: 8px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: rgba(11, 14, 20, .94);
|
||||||
|
border: 1px solid alpha(var(--jarvis-accent, #F4B942), .42);
|
||||||
|
color: #f6f7fb;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, .45);
|
||||||
|
--jarvis-signal: #4FD2FF;
|
||||||
|
}
|
||||||
.jarvis-popup-header { spacing: 8px; }
|
.jarvis-popup-header { spacing: 8px; }
|
||||||
.jarvis-title { font-weight: bold; letter-spacing: 0.4px; font-size: 14px; }
|
.jarvis-mark { icon-size: 18px; color: var(--jarvis-accent, #F4B942); }
|
||||||
.jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; }
|
.jarvis-title { font-weight: 700; letter-spacing: 0.6px; font-size: 15px; color: #f6f7fb; }
|
||||||
|
.jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; font-weight: 700; letter-spacing: 0.4px; }
|
||||||
.jarvis-status-line { color: #aeb6c8; font-size: 12px; }
|
.jarvis-status-line { color: #aeb6c8; font-size: 12px; }
|
||||||
.jarvis-tabs { spacing: 6px; }
|
.jarvis-tabs { spacing: 6px; }
|
||||||
.jarvis-tab { padding: 4px 10px; border-radius: 999px; background-color: transparent; color: #9aa8c0; font-size: 12px; }
|
.jarvis-tab { padding: 4px 10px; border-radius: 999px; background-color: transparent; color: #9aa8c0; font-size: 12px; }
|
||||||
.jarvis-tab:hover, .jarvis-tab:focus { color: #f6f7fb; background-color: rgba(255, 255, 255, .08); }
|
.jarvis-tab:hover, .jarvis-tab:focus { color: #f6f7fb; background-color: rgba(255, 255, 255, .08); }
|
||||||
.jarvis-tab-active { background-color: var(--jarvis-accent, #F4B942); color: #16191f; font-weight: bold; }
|
.jarvis-tab-active { background-color: var(--jarvis-accent, #F4B942); color: #16191f; font-weight: 700; }
|
||||||
.jarvis-thinking { color: #aeb6c8; font-size: 12px; padding: 8px 10px; border-left: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .06); border-radius: 8px; }
|
.jarvis-thinking { color: #aeb6c8; font-size: 12px; padding: 8px 10px; border-left: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .06); border-radius: 8px; }
|
||||||
.jarvis-notice { color: #f4b942; font-size: 12px; }
|
.jarvis-notice { color: var(--jarvis-accent, #F4B942); font-size: 12px; }
|
||||||
.jarvis-confirm { spacing: 8px; padding: 4px 0; }
|
.jarvis-confirm { spacing: 8px; padding: 4px 0; }
|
||||||
.jarvis-confirm-actions { spacing: 6px; }
|
.jarvis-confirm-actions { spacing: 6px; }
|
||||||
.jarvis-confirm-label { color: #f6f7fb; font-size: 13px; }
|
.jarvis-confirm-label { color: #f6f7fb; font-size: 13px; }
|
||||||
@@ -26,22 +66,26 @@
|
|||||||
.jarvis-session-thinking { height: 200px; }
|
.jarvis-session-thinking { height: 200px; }
|
||||||
.jarvis-transcript { spacing: 6px; }
|
.jarvis-transcript { spacing: 6px; }
|
||||||
.jarvis-row { padding: 6px 10px; border-radius: 8px; font-size: 13px; }
|
.jarvis-row { padding: 6px 10px; border-radius: 8px; font-size: 13px; }
|
||||||
.jarvis-row-user { border-left: 2px solid var(--jarvis-accent, #F4B942); background-color: rgba(244, 185, 66, .08); }
|
.jarvis-row-user { border-left: 2px solid var(--jarvis-accent, #F4B942); background-color: alpha(var(--jarvis-accent, #F4B942), .08); }
|
||||||
.jarvis-row-jarvis { border-right: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .05); }
|
.jarvis-row-jarvis { border-right: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .05); }
|
||||||
.jarvis-row-tool { background-color: transparent; color: #9aa8c0; font-size: 12px; padding: 2px 6px; }
|
.jarvis-row-tool { background-color: transparent; color: #9aa8c0; font-size: 12px; padding: 2px 6px; }
|
||||||
.jarvis-chip-scroll { height: 32px; }
|
.jarvis-chip-scroll { height: 32px; }
|
||||||
.jarvis-chips { spacing: 6px; }
|
.jarvis-chips { spacing: 6px; }
|
||||||
.jarvis-chip { padding: 5px 9px; border-radius: 999px; background-color: rgba(255, 255, 255, .08); }
|
.jarvis-chip { padding: 5px 9px; border-radius: 999px; background-color: rgba(255, 255, 255, .08); }
|
||||||
.jarvis-chip:hover, .jarvis-chip:focus { background-color: rgba(244, 185, 66, .25); }
|
.jarvis-chip:hover, .jarvis-chip:focus { background-color: alpha(var(--jarvis-accent, #F4B942), .25); }
|
||||||
.jarvis-chip-quiet { background-color: transparent; color: #aeb6c8; padding: 5px 8px; }
|
.jarvis-chip-quiet { background-color: transparent; color: #aeb6c8; padding: 5px 8px; }
|
||||||
.jarvis-chip-quiet:hover, .jarvis-chip-quiet:focus { color: #f6f7fb; background-color: rgba(255, 255, 255, .08); }
|
.jarvis-chip-quiet:hover, .jarvis-chip-quiet:focus { color: #f6f7fb; background-color: rgba(255, 255, 255, .08); }
|
||||||
.jarvis-talk { background-color: #F4B942; color: #16191f; font-weight: bold; padding: 8px 14px; }
|
.jarvis-talk { background-color: var(--jarvis-accent, #F4B942); color: #16191f; font-weight: 700; padding: 8px 14px; }
|
||||||
|
.jarvis-talk:hover, .jarvis-talk:focus { background-color: #ffe09a; }
|
||||||
.jarvis-talk:active { background-color: #ffe09a; }
|
.jarvis-talk:active { background-color: #ffe09a; }
|
||||||
.jarvis-controls { spacing: 6px; }
|
.jarvis-controls { spacing: 6px; }
|
||||||
.jarvis-settings { font-size: 12px; padding: 4px 8px; }
|
.jarvis-settings { font-size: 12px; padding: 4px 8px; }
|
||||||
.jarvis-popup StEntry, .jarvis-session StEntry { border-radius: 10px; padding: 8px 10px; background-color: rgba(255, 255, 255, .06); color: #f6f7fb; border: 1px solid rgba(255, 255, 255, .15); }
|
.jarvis-popup StEntry, .jarvis-session StEntry { border-radius: 10px; padding: 8px 10px; background-color: rgba(255, 255, 255, .06); color: #f6f7fb; border: 1px solid rgba(255, 255, 255, .15); }
|
||||||
.jarvis-popup StEntry:focus, .jarvis-session StEntry:focus { border-color: #F4B942; }
|
.jarvis-popup StEntry:focus, .jarvis-session StEntry:focus { border-color: var(--jarvis-accent, #F4B942); }
|
||||||
.jarvis-cu { padding: 8px 12px; spacing: 4px; border-radius: 12px; background-color: rgba(11, 14, 20, .82); border: 1px solid rgba(79, 210, 255, .4); }
|
.jarvis-cu { padding: 8px 12px; spacing: 4px; border-radius: 12px; background-color: rgba(11, 14, 20, .82); border: 1px solid rgba(79, 210, 255, .4); }
|
||||||
.jarvis-job, .jarvis-target, .jarvis-cu-step { color: #4FD2FF; font-size: 12px; }
|
.jarvis-job, .jarvis-target, .jarvis-cu-step { color: #4FD2FF; font-size: 12px; }
|
||||||
.jarvis-agent-cursor { color: var(--jarvis-accent, #F4B942); font-size: 22px; }
|
.jarvis-agent-cursor { color: var(--jarvis-accent, #F4B942); font-size: 22px; }
|
||||||
.jarvis-high-contrast { border-width: 2px; background-color: #000; }
|
.jarvis-high-contrast { border-width: 2px; background-color: #000; }
|
||||||
|
.jarvis-high-contrast .jarvis-title,
|
||||||
|
.jarvis-high-contrast .jarvis-panel-glyph,
|
||||||
|
.jarvis-high-contrast .jarvis-status-line { color: #fff; }
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
import Gio from 'gi://Gio';
|
||||||
import GLib from 'gi://GLib';
|
import GLib from 'gi://GLib';
|
||||||
import St from 'gi://St';
|
import St from 'gi://St';
|
||||||
import Clutter from 'gi://Clutter';
|
import Clutter from 'gi://Clutter';
|
||||||
import Pango from 'gi://Pango';
|
import Pango from 'gi://Pango';
|
||||||
|
|
||||||
|
function brandMark(size = 16, directory = '') {
|
||||||
|
if (typeof Gio === 'undefined' || typeof St.Icon !== 'function' || !directory) return null;
|
||||||
|
try {
|
||||||
|
const file = Gio.File.new_for_path(`${directory}/brand/icons/jarvis-mark.svg`);
|
||||||
|
if (!file.query_exists(null)) return null;
|
||||||
|
return new St.Icon({
|
||||||
|
gicon: new Gio.FileIcon({ file }),
|
||||||
|
icon_size: size,
|
||||||
|
style_class: 'jarvis-mark',
|
||||||
|
y_align: Clutter.ActorAlign.CENTER,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const STATES = new Set(['ARMED', 'LISTENING', 'THINKING', 'SPEAKING', 'SLEEPING']);
|
export const STATES = new Set(['ARMED', 'LISTENING', 'THINKING', 'SPEAKING', 'SLEEPING']);
|
||||||
export const POPUP_ROWS = 8;
|
export const POPUP_ROWS = 8;
|
||||||
export const SESSION_WIDTH = 420;
|
export const SESSION_WIDTH = 420;
|
||||||
@@ -56,17 +73,19 @@ function wrapLabel(label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ConversationView {
|
export class ConversationView {
|
||||||
constructor({ compact = true, maxRows = compact ? POPUP_ROWS : 40 } = {}) {
|
constructor({ compact = true, maxRows = compact ? POPUP_ROWS : 40, brandDir = '' } = {}) {
|
||||||
this.compact = compact;
|
this.compact = compact;
|
||||||
this.maxRows = maxRows;
|
this.maxRows = maxRows;
|
||||||
this.root = new St.BoxLayout({ style_class: compact ? 'jarvis-popup' : 'jarvis-session', vertical: true, reactive: true, can_focus: true, x_expand: true });
|
this.root = new St.BoxLayout({ style_class: compact ? 'jarvis-popup' : 'jarvis-session', vertical: true, reactive: true, can_focus: true, x_expand: true });
|
||||||
this.root.accessible_name = compact ? 'Jarvis voice assistant' : 'Jarvis conversation';
|
this.root.accessible_name = compact ? 'Jarvis voice assistant' : 'Jarvis conversation';
|
||||||
this.header = new St.BoxLayout({ style_class: 'jarvis-popup-header', x_expand: true });
|
this.header = new St.BoxLayout({ style_class: 'jarvis-popup-header', x_expand: true });
|
||||||
|
this.mark = brandMark(compact ? 16 : 20, brandDir);
|
||||||
this.title = new St.Label({ text: 'Jarvis', style_class: 'jarvis-title', x_align: Clutter.ActorAlign.START });
|
this.title = new St.Label({ text: 'Jarvis', style_class: 'jarvis-title', x_align: Clutter.ActorAlign.START });
|
||||||
this.title.accessible_name = 'Jarvis status';
|
this.title.accessible_name = 'Jarvis status';
|
||||||
this.status = new St.Label({ text: 'LOCAL', style_class: 'jarvis-local' });
|
this.status = new St.Label({ text: 'LOCAL', style_class: 'jarvis-local' });
|
||||||
this.status.accessible_name = 'Local model status';
|
this.status.accessible_name = 'Local model status';
|
||||||
if (this.status.clutter_text) this.status.clutter_text.ellipsize = Pango.EllipsizeMode.END;
|
if (this.status.clutter_text) this.status.clutter_text.ellipsize = Pango.EllipsizeMode.END;
|
||||||
|
if (this.mark) this.header.add_child(this.mark);
|
||||||
this.header.add_child(this.title);
|
this.header.add_child(this.title);
|
||||||
this.header.add_child(new St.Widget({ x_expand: true }));
|
this.header.add_child(new St.Widget({ x_expand: true }));
|
||||||
this.header.add_child(this.status);
|
this.header.add_child(this.status);
|
||||||
@@ -418,6 +437,10 @@ export class JarvisOsd {
|
|||||||
this.label.text = text;
|
this.label.text = text;
|
||||||
this.root.visible = Boolean(text);
|
this.root.visible = Boolean(text);
|
||||||
this.root.accessible_name = text ? `Jarvis ${text}` : 'Jarvis status';
|
this.root.accessible_name = text ? `Jarvis ${text}` : 'Jarvis status';
|
||||||
|
for (const name of ['armed', 'listening', 'speaking', 'thinking', 'sleeping']) {
|
||||||
|
this.root.remove_style_class_name(`jarvis-state-${name}`);
|
||||||
|
}
|
||||||
|
if (this._state) this.root.add_style_class_name(`jarvis-state-${this._state.toLowerCase()}`);
|
||||||
}
|
}
|
||||||
_clearTimer() { if (this._timeout) { GLib.Source.remove(this._timeout); this._timeout = 0; } }
|
_clearTimer() { if (this._timeout) { GLib.Source.remove(this._timeout); this._timeout = 0; } }
|
||||||
hide() { this._wake = false; this._clearTimer(); this.root.visible = false; }
|
hide() { this._wake = false; this._clearTimer(); this.root.visible = false; }
|
||||||
@@ -463,8 +486,8 @@ export class ComputerUseChrome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SessionPanel {
|
export class SessionPanel {
|
||||||
constructor() {
|
constructor({ brandDir = '' } = {}) {
|
||||||
this.view = new ConversationView({ compact: false });
|
this.view = new ConversationView({ compact: false, brandDir });
|
||||||
this.root = this.view.root;
|
this.root = this.view.root;
|
||||||
this.minimized = true;
|
this.minimized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -157,7 +157,8 @@ and WebP quality tune observation detail and processing cost.
|
|||||||
|
|
||||||
The Desktop page also exposes the GNOME hotkey, accent color, and tray versus
|
The Desktop page also exposes the GNOME hotkey, accent color, and tray versus
|
||||||
expanded layout. These retain their GSettings keys: `hotkey`, `accent-color`,
|
expanded layout. These retain their GSettings keys: `hotkey`, `accent-color`,
|
||||||
and `overlay-style`.
|
and `overlay-style`. Gold `#F4B942` is the brand default. Signal, Copper, Jade,
|
||||||
|
and Rose are presets; a custom hex value is also accepted.
|
||||||
|
|
||||||
Previously displayed controls for chimes, legacy input fallback, enrollment,
|
Previously displayed controls for chimes, legacy input fallback, enrollment,
|
||||||
startup, memory ingestion, Lab toggles, transcript retention, and disabling
|
startup, memory ingestion, Lab toggles, transcript retention, and disabling
|
||||||
|
|||||||
@@ -194,6 +194,28 @@ Exec=/bin/false
|
|||||||
SystemdService=jarvisd.service
|
SystemdService=jarvisd.service
|
||||||
EOF
|
EOF
|
||||||
log "wrote ${DBUS_DIR}/io.qvac.Jarvis.service"
|
log "wrote ${DBUS_DIR}/io.qvac.Jarvis.service"
|
||||||
|
ICON_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor/scalable/apps"
|
||||||
|
APP_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
|
||||||
|
mkdir -p "${ICON_HOME}" "${APP_HOME}"
|
||||||
|
ICON_SRC="${ROOT_DIR}/apps/gnome-extension/[email protected]/brand/icons/io.qvac.Jarvis.Control.svg"
|
||||||
|
if [[ -f "${ICON_SRC}" ]]; then
|
||||||
|
cp "${ICON_SRC}" "${ICON_HOME}/io.qvac.Jarvis.Control.svg"
|
||||||
|
log "installed application icon io.qvac.Jarvis.Control"
|
||||||
|
fi
|
||||||
|
cat > "${APP_HOME}/io.qvac.Jarvis.Control.desktop" <<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Jarvis
|
||||||
|
GenericName=Voice Assistant
|
||||||
|
Comment=Local voice for your desktop
|
||||||
|
Exec=gjs -m ${APP_DIR}/apps/control-center/main.js
|
||||||
|
Icon=io.qvac.Jarvis.Control
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=GTK;Settings;Utility;
|
||||||
|
Keywords=voice;assistant;jarvis;qvac;
|
||||||
|
StartupNotify=true
|
||||||
|
EOF
|
||||||
|
log "installed ${APP_HOME}/io.qvac.Jarvis.Control.desktop"
|
||||||
if [[ -f "${CONFIG_DIR}/config.json" ]]; then
|
if [[ -f "${CONFIG_DIR}/config.json" ]]; then
|
||||||
log "kept existing ${CONFIG_DIR}/config.json"
|
log "kept existing ${CONFIG_DIR}/config.json"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ gnome-extensions disable [email protected] 2>/dev/null || true
|
|||||||
systemctl --user disable --now jarvisd.service 2>/dev/null || true
|
systemctl --user disable --now jarvisd.service 2>/dev/null || true
|
||||||
rm -f "${HOME}/.config/systemd/user/jarvisd.service"
|
rm -f "${HOME}/.config/systemd/user/jarvisd.service"
|
||||||
rm -f "${XDG_DATA_HOME:-${HOME}/.local/share}/dbus-1/services/io.qvac.Jarvis.service"
|
rm -f "${XDG_DATA_HOME:-${HOME}/.local/share}/dbus-1/services/io.qvac.Jarvis.service"
|
||||||
|
rm -f "${XDG_DATA_HOME:-${HOME}/.local/share}/applications/io.qvac.Jarvis.Control.desktop"
|
||||||
|
rm -f "${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor/scalable/apps/io.qvac.Jarvis.Control.svg"
|
||||||
rm -rf "${HOME}/.local/share/jarvis-qvac" "${HOME}/.local/share/gnome-shell/extensions/[email protected]"
|
rm -rf "${HOME}/.local/share/jarvis-qvac" "${HOME}/.local/share/gnome-shell/extensions/[email protected]"
|
||||||
systemctl --user daemon-reload 2>/dev/null || true
|
systemctl --user daemon-reload 2>/dev/null || true
|
||||||
if [[ "${2:-}" == "--purge-data" ]]; then
|
if [[ "${2:-}" == "--purge-data" ]]; then
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
|
||||||
|
const root = new URL('../apps/gnome-extension/[email protected]/', import.meta.url);
|
||||||
|
const tokens = JSON.parse(readFileSync(new URL('brand/tokens.json', root), 'utf8'));
|
||||||
|
const brandSource = readFileSync(new URL('brand.js', root), 'utf8');
|
||||||
|
const stylesheet = readFileSync(new URL('stylesheet.css', root), 'utf8');
|
||||||
|
const gtkCss = readFileSync(new URL('brand/gtk.css', root), 'utf8');
|
||||||
|
const metadata = JSON.parse(readFileSync(new URL('metadata.json', root), 'utf8'));
|
||||||
|
const settingsWindow = readFileSync(new URL('settings-window.js', root), 'utf8');
|
||||||
|
const extensionSource = readFileSync(new URL('extension.js', root), 'utf8');
|
||||||
|
const install = readFileSync(new URL('../packaging/install.sh', import.meta.url), 'utf8');
|
||||||
|
const uninstall = readFileSync(new URL('../packaging/uninstall.sh', import.meta.url), 'utf8');
|
||||||
|
|
||||||
|
const icons = [
|
||||||
|
'brand/icons/jarvis-mark.svg',
|
||||||
|
'brand/icons/jarvis-mark-symbolic.svg',
|
||||||
|
'brand/icons/jarvis-wordmark.svg',
|
||||||
|
'brand/icons/io.qvac.Jarvis.Control.svg',
|
||||||
|
'brand/icons/scalable/apps/io.qvac.Jarvis.Control.svg',
|
||||||
|
];
|
||||||
|
|
||||||
|
test('brand tokens match the coded gold and signal palette', () => {
|
||||||
|
assert.equal(tokens.name, 'Jarvis');
|
||||||
|
assert.equal(tokens.product, 'Jarvis QVAC');
|
||||||
|
assert.equal(tokens.tagline, 'Local voice for your desktop');
|
||||||
|
assert.equal(tokens.colors.gold, '#F4B942');
|
||||||
|
assert.equal(tokens.colors.signal, '#4FD2FF');
|
||||||
|
assert.equal(tokens.colors.ink, '#0B0E14');
|
||||||
|
assert.equal(tokens.accents[0].id, 'gold');
|
||||||
|
assert.match(brandSource, /gold: '#F4B942'/);
|
||||||
|
assert.match(brandSource, /signal: '#4FD2FF'/);
|
||||||
|
assert.match(brandSource, /tagline: 'Local voice for your desktop'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('brand icons ship as SVG marks with gold visor and cyan core', () => {
|
||||||
|
for (const icon of icons) {
|
||||||
|
const path = new URL(icon, root);
|
||||||
|
assert.equal(existsSync(path), true, icon);
|
||||||
|
const svg = readFileSync(path, 'utf8');
|
||||||
|
assert.match(svg, /<svg /);
|
||||||
|
assert.match(svg, /viewBox=/);
|
||||||
|
}
|
||||||
|
const mark = readFileSync(new URL('brand/icons/jarvis-mark.svg', root), 'utf8');
|
||||||
|
assert.match(mark, /#F4B942/);
|
||||||
|
assert.match(mark, /#4FD2FF/);
|
||||||
|
const wordmark = readFileSync(new URL('brand/icons/jarvis-wordmark.svg', root), 'utf8');
|
||||||
|
assert.match(wordmark, /viewBox="0 0 300 64"/);
|
||||||
|
assert.doesNotMatch(wordmark, /translate\(0\s+-?\d+\)/);
|
||||||
|
const app = readFileSync(new URL('brand/icons/io.qvac.Jarvis.Control.svg', root), 'utf8');
|
||||||
|
assert.match(app, /#0B0E14/);
|
||||||
|
const symbolic = readFileSync(new URL('brand/icons/jarvis-mark-symbolic.svg', root), 'utf8');
|
||||||
|
assert.match(symbolic, /currentColor/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HUD, settings, and launcher consume the brand package', () => {
|
||||||
|
assert.match(stylesheet, /--jarvis-accent/);
|
||||||
|
assert.match(stylesheet, /--jarvis-signal: #4FD2FF/);
|
||||||
|
assert.match(stylesheet, /\.jarvis-panel-mark/);
|
||||||
|
assert.match(stylesheet, /\.jarvis-talk \{ background-color: var\(--jarvis-accent/);
|
||||||
|
assert.match(gtkCss, /@define-color jarvis_gold #F4B942/);
|
||||||
|
assert.match(gtkCss, /button\.jarvis-swatch-gold/);
|
||||||
|
assert.equal(metadata.name, 'Jarvis');
|
||||||
|
assert.match(metadata.description, /Local voice for your desktop/);
|
||||||
|
assert.match(settingsWindow, /from '\.\/brand\.js'/);
|
||||||
|
assert.match(settingsWindow, /brandBanner/);
|
||||||
|
assert.match(settingsWindow, /accentRow/);
|
||||||
|
assert.match(extensionSource, /brand\/icons\/jarvis-mark\.svg/);
|
||||||
|
assert.match(extensionSource, /text: 'Jarvis'/);
|
||||||
|
assert.match(install, /io\.qvac\.Jarvis\.Control\.desktop/);
|
||||||
|
assert.match(install, /Local voice for your desktop/);
|
||||||
|
assert.match(uninstall, /io\.qvac\.Jarvis\.Control\.desktop/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accent helper rejects invalid hex and keeps Gold', async () => {
|
||||||
|
const { normalizeAccent, DEFAULT_ACCENT, BRAND } = await import(new URL('../apps/gnome-extension/[email protected]/brand.js', import.meta.url));
|
||||||
|
assert.equal(DEFAULT_ACCENT, '#F4B942');
|
||||||
|
assert.equal(normalizeAccent('#4FD2FF'), '#4FD2FF');
|
||||||
|
assert.equal(normalizeAccent('not-a-color'), '#F4B942');
|
||||||
|
assert.equal(normalizeAccent(''), '#F4B942');
|
||||||
|
assert.equal(BRAND.accents.length, 5);
|
||||||
|
});
|
||||||
@@ -51,6 +51,7 @@ function harness() {
|
|||||||
grab_key_focus() {}
|
grab_key_focus() {}
|
||||||
set_style() {}
|
set_style() {}
|
||||||
add_style_class_name() {}
|
add_style_class_name() {}
|
||||||
|
remove_style_class_name() {}
|
||||||
get_text() { return this.text || ''; }
|
get_text() { return this.text || ''; }
|
||||||
set_text(value) { this.text = value; }
|
set_text(value) { this.text = value; }
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,7 @@ function harness() {
|
|||||||
const context = vm.createContext({
|
const context = vm.createContext({
|
||||||
Extension: class {},
|
Extension: class {},
|
||||||
global: { stage: { get_key_focus() { return null; } } },
|
global: { stage: { get_key_focus() { return null; } } },
|
||||||
St: { BoxLayout, Label, Widget: Actor, Button: Actor, Entry: Actor, ScrollView, PolicyType: { NEVER: 0, AUTOMATIC: 1 } },
|
St: { BoxLayout, Label, Icon: Actor, Widget: Actor, Button: Actor, Entry: Actor, ScrollView, PolicyType: { NEVER: 0, AUTOMATIC: 1 } },
|
||||||
Clutter: { ActorAlign: { CENTER: 0, START: 1 }, EVENT_STOP: 1, EVENT_PROPAGATE: 0, KEY_space: 32, KEY_Return: 65293, KEY_Escape: 65307 },
|
Clutter: { ActorAlign: { CENTER: 0, START: 1 }, EVENT_STOP: 1, EVENT_PROPAGATE: 0, KEY_space: 32, KEY_Return: 65293, KEY_Escape: 65307 },
|
||||||
Pango: { WrapMode: { WORD_CHAR: 2 }, EllipsizeMode: { NONE: 0, END: 3 } },
|
Pango: { WrapMode: { WORD_CHAR: 2 }, EllipsizeMode: { NONE: 0, END: 3 } },
|
||||||
PopupMenu: {
|
PopupMenu: {
|
||||||
@@ -233,6 +234,8 @@ test('GNOME GI imports expose default namespaces and panel has a real menu', ()
|
|||||||
assert.match(uiSource, /Open conversation/);
|
assert.match(uiSource, /Open conversation/);
|
||||||
assert.match(extensionSource, /PopupMenuSection/);
|
assert.match(extensionSource, /PopupMenuSection/);
|
||||||
assert.match(extensionSource, /osd\.attach\(this\._panelBox\)/);
|
assert.match(extensionSource, /osd\.attach\(this\._panelBox\)/);
|
||||||
|
assert.match(extensionSource, /jarvis-panel-mark/);
|
||||||
|
assert.match(extensionSource, /brand\/icons\/jarvis-mark\.svg/);
|
||||||
assert.match(uiSource, /jarvis-panel-state/);
|
assert.match(uiSource, /jarvis-panel-state/);
|
||||||
assert.doesNotMatch(uiSource, /style_class: 'jarvis-osd'/);
|
assert.doesNotMatch(uiSource, /style_class: 'jarvis-osd'/);
|
||||||
assert.match(extensionSource, /_openSettings/);
|
assert.match(extensionSource, /_openSettings/);
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ exit 0
|
|||||||
assert.equal(existsSync(path.join(home, '.local/share/jarvis-qvac.staging.oldfail')), false);
|
assert.equal(existsSync(path.join(home, '.local/share/jarvis-qvac.staging.oldfail')), false);
|
||||||
assert.equal(existsSync(path.join(extDir, 'old-extension.js')), false);
|
assert.equal(existsSync(path.join(extDir, 'old-extension.js')), false);
|
||||||
assert.equal(existsSync(path.join(extDir, 'extension.js')), true);
|
assert.equal(existsSync(path.join(extDir, 'extension.js')), true);
|
||||||
|
assert.equal(existsSync(path.join(extDir, 'brand/icons/jarvis-mark.svg')), true);
|
||||||
|
assert.equal(existsSync(path.join(home, '.local/share/applications/io.qvac.Jarvis.Control.desktop')), true);
|
||||||
|
assert.equal(existsSync(path.join(home, '.local/share/icons/hicolor/scalable/apps/io.qvac.Jarvis.Control.svg')), true);
|
||||||
|
assert.match(await readFile(path.join(home, '.local/share/applications/io.qvac.Jarvis.Control.desktop'), 'utf8'), /Local voice for your desktop/);
|
||||||
assert.equal(await readFile(path.join(appDir, 'daemon-marker'), 'utf8'), 'new-tree\n');
|
assert.equal(await readFile(path.join(appDir, 'daemon-marker'), 'utf8'), 'new-tree\n');
|
||||||
assert.equal(await readFile(path.join(configDir, 'config.json'), 'utf8'), '{"wakePhrase":"hey jarvis","ttsEnabled":true}\n');
|
assert.equal(await readFile(path.join(configDir, 'config.json'), 'utf8'), '{"wakePhrase":"hey jarvis","ttsEnabled":true}\n');
|
||||||
const unit = await readFile(path.join(home, '.config/systemd/user/jarvisd.service'), 'utf8');
|
const unit = await readFile(path.join(home, '.config/systemd/user/jarvisd.service'), 'utf8');
|
||||||
|
|||||||
Reference in New Issue
Block a user