Branding
Rolling release / release (push) Successful in 6m57s

This commit is contained in:
2026-09-12 14:06:02 -04:00
parent 0e52942b8b
commit 18802cb55d
23 changed files with 459 additions and 39 deletions
+3 -1
View File
@@ -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
+2 -1
View File
@@ -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`).
+8 -1
View File
@@ -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

@@ -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; }
+26 -3
View File
@@ -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
View File
@@ -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
+22
View File
@@ -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
+2
View File
@@ -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
+83
View File
@@ -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);
});
+4 -1
View File
@@ -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/);
+4
View File
@@ -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');