diff --git a/README.md b/README.md
index 4b16c72..bd4c9e9 100644
--- a/README.md
+++ b/README.md
@@ -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
GNOME. It combines a wake-word loop, QVAC speech and language models, the
diff --git a/apps/control-center/README.md b/apps/control-center/README.md
index 0434958..3bb7ea8 100644
--- a/apps/control-center/README.md
+++ b/apps/control-center/README.md
@@ -3,7 +3,8 @@
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
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
`$XDG_CONFIG_HOME/jarvis/config.json` (default `~/.config/jarvis/config.json`).
diff --git a/apps/control-center/main.js b/apps/control-center/main.js
index 9ee1f68..5a6c577 100644
--- a/apps/control-center/main.js
+++ b/apps/control-center/main.js
@@ -1,16 +1,23 @@
#!/usr/bin/gjs -m
import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
+import Gtk from 'gi://Gtk';
+import Gdk from 'gi://Gdk';
import { fillSettingsWindow } from '../gnome-extension/jarvis@qvac.local/settings-window.js';
const directory = Gio.File.new_for_uri(import.meta.url).get_parent().get_parent().get_child('gnome-extension').get_child('jarvis@qvac.local').get_path();
const application = new Adw.Application({ application_id: 'io.qvac.Jarvis.Control', flags: Gio.ApplicationFlags.DEFAULT_FLAGS });
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', () => {
if (!window) {
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) });
- window = new Adw.PreferencesWindow({ application });
+ window = new Adw.PreferencesWindow({ application, icon_name: 'io.qvac.Jarvis.Control' });
fillSettingsWindow(window, settings, directory);
window.connect('close-request', () => { window = null; return false; });
}
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand.js b/apps/gnome-extension/jarvis@qvac.local/brand.js
new file mode 100644
index 0000000..bbe7d28
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand.js
@@ -0,0 +1,33 @@
+export const BRAND = {
+ name: 'Jarvis',
+ product: 'Jarvis QVAC',
+ tagline: 'Local voice for your desktop',
+ applicationId: 'io.qvac.Jarvis.Control',
+ uuid: 'jarvis@qvac.local',
+ 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;
+}
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand/gtk.css b/apps/gnome-extension/jarvis@qvac.local/brand/gtk.css
new file mode 100644
index 0000000..292f74c
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand/gtk.css
@@ -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;
+}
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand/icons/io.qvac.Jarvis.Control.svg b/apps/gnome-extension/jarvis@qvac.local/brand/icons/io.qvac.Jarvis.Control.svg
new file mode 100644
index 0000000..8bdac68
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand/icons/io.qvac.Jarvis.Control.svg
@@ -0,0 +1,17 @@
+
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-mark-symbolic.svg b/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-mark-symbolic.svg
new file mode 100644
index 0000000..1d1cfa0
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-mark-symbolic.svg
@@ -0,0 +1,5 @@
+
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-mark.svg b/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-mark.svg
new file mode 100644
index 0000000..f8084da
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-mark.svg
@@ -0,0 +1,6 @@
+
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-wordmark.svg b/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-wordmark.svg
new file mode 100644
index 0000000..3d7ce1e
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand/icons/jarvis-wordmark.svg
@@ -0,0 +1,9 @@
+
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand/icons/scalable/apps/io.qvac.Jarvis.Control.svg b/apps/gnome-extension/jarvis@qvac.local/brand/icons/scalable/apps/io.qvac.Jarvis.Control.svg
new file mode 100644
index 0000000..8bdac68
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand/icons/scalable/apps/io.qvac.Jarvis.Control.svg
@@ -0,0 +1,17 @@
+
diff --git a/apps/gnome-extension/jarvis@qvac.local/brand/tokens.json b/apps/gnome-extension/jarvis@qvac.local/brand/tokens.json
new file mode 100644
index 0000000..cb997a7
--- /dev/null
+++ b/apps/gnome-extension/jarvis@qvac.local/brand/tokens.json
@@ -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": "jarvis@qvac.local",
+ "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
+ }
+}
diff --git a/apps/gnome-extension/jarvis@qvac.local/extension.js b/apps/gnome-extension/jarvis@qvac.local/extension.js
index 6d11d75..65906df 100644
--- a/apps/gnome-extension/jarvis@qvac.local/extension.js
+++ b/apps/gnome-extension/jarvis@qvac.local/extension.js
@@ -14,7 +14,8 @@ import { ConversationView, JarvisOsd, ComputerUseChrome, SessionPanel, coerceTex
const BUS = 'io.qvac.Jarvis';
const PATH = '/io/qvac/Jarvis';
const IFACE = 'io.qvac.Jarvis.Session';
-const GLYPHS = { ARMED: '◯', LISTENING: '◌', THINKING: '◉', SPEAKING: '◎', SLEEPING: '◐' };
+const DEFAULT_ACCENT = '#F4B942';
+const SIGNAL = '#4FD2FF';
class JarvisProxy {
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.settings = this.getSettings();
this.proxy = new JarvisProxy();
- this.popup = new ConversationView({ compact: true });
+ this.popup = new ConversationView({ compact: true, brandDir: this.path });
this.osd = new JarvisOsd();
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.session.view);
this._applyAccessibility();
this._removeShellService = installShellService();
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._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.osd.attach(this._panelBox);
this._indicator.add_child(this._panelBox);
@@ -264,17 +271,27 @@ export default class JarvisExtension extends Extension {
}
_setState(state) {
const value = safeText(state);
- this._glyph.text = `${GLYPHS[value] || '◯'} Jarvis`;
+ this._glyph.text = 'Jarvis';
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.osd.setState(value);
}
_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.session.root.set_style(style);
this.osd.root.set_style(style);
this.cu.root.set_style(style);
+ this._panelBox?.set_style(style);
}
_applyAccessibility() {
try {
@@ -300,7 +317,7 @@ export default class JarvisExtension extends Extension {
this.osd?.destroy(); this.cu?.destroy(); this.session?.destroy(); this.popup?.destroy();
this._indicator?.destroy();
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;
}
}
diff --git a/apps/gnome-extension/jarvis@qvac.local/metadata.json b/apps/gnome-extension/jarvis@qvac.local/metadata.json
index 9c4db26..07b89d3 100644
--- a/apps/gnome-extension/jarvis@qvac.local/metadata.json
+++ b/apps/gnome-extension/jarvis@qvac.local/metadata.json
@@ -1,7 +1,7 @@
{
"uuid": "jarvis@qvac.local",
- "name": "Jarvis QVAC",
- "description": "Local-first QVAC voice assistant in the GNOME top bar",
+ "name": "Jarvis",
+ "description": "Local voice for your desktop. Jarvis QVAC lives in the GNOME top bar.",
"shell-version": ["46", "47", "48", "49", "50"],
"version": 2,
"session-modes": ["user"],
diff --git a/apps/gnome-extension/jarvis@qvac.local/settings-editor.js b/apps/gnome-extension/jarvis@qvac.local/settings-editor.js
index 872cf75..26b66e2 100644
--- a/apps/gnome-extension/jarvis@qvac.local/settings-editor.js
+++ b/apps/gnome-extension/jarvis@qvac.local/settings-editor.js
@@ -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); } });
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 });
+ if (intro) page.add(intro);
const fields = this.fields.filter(field => groups.includes(field.group));
this.controls(page, fields, { preview });
for (const name of groups) {
diff --git a/apps/gnome-extension/jarvis@qvac.local/settings-window.js b/apps/gnome-extension/jarvis@qvac.local/settings-window.js
index 0bc21e5..6140f71 100644
--- a/apps/gnome-extension/jarvis@qvac.local/settings-window.js
+++ b/apps/gnome-extension/jarvis@qvac.local/settings-window.js
@@ -2,17 +2,61 @@ import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Gtk from 'gi://Gtk';
+import Gdk from 'gi://Gdk';
import { SettingsEditor } from './settings-editor.js';
+import { BRAND, normalizeAccent } from './brand.js';
const BUS = 'io.qvac.Jarvis';
const PATH = '/io/qvac/Jarvis';
const IFACE = 'io.qvac.Jarvis.Session';
const OVERLAY_STYLES = ['tray', 'expanded'];
-function entryRow(settings, title, key) {
- const row = new Adw.EntryRow({ title });
- row.set_text(settings.get_string(key));
- row.connect('changed', () => settings.set_string(key, row.get_text()));
+function applyBrandCss(directory) {
+ try {
+ const css = new Gtk.CssProvider();
+ 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;
}
@@ -107,11 +151,14 @@ function grantRow() {
}
export function fillSettingsWindow(window, settings, directory) {
- window.set_title('Jarvis QVAC');
+ window.set_title(BRAND.product);
window.set_default_size(840, 780);
window.search_enabled = true;
+ applyBrandCss(directory);
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);
const listening = editor.page(window, 'Listening', ['Listening', 'Wake and privacy', 'Detection tuning', 'Audio routing'], 'audio-input-microphone-symbolic');
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 desktopGroup = new Adw.PreferencesGroup({ title: 'Overlay and shortcuts', description: 'These appearance settings take effect immediately.' });
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));
desktop.add(desktopGroup);
const grantGroup = new Adw.PreferencesGroup({ title: 'Temporary desktop access' });
diff --git a/apps/gnome-extension/jarvis@qvac.local/stylesheet.css b/apps/gnome-extension/jarvis@qvac.local/stylesheet.css
index 7f80f36..2f81838 100644
--- a/apps/gnome-extension/jarvis@qvac.local/stylesheet.css
+++ b/apps/gnome-extension/jarvis@qvac.local/stylesheet.css
@@ -1,21 +1,61 @@
-.jarvis-panel-box { spacing: 6px; }
-.jarvis-panel-glyph { color: var(--jarvis-accent, #F4B942); font-size: 14px; }
-.jarvis-panel-state { padding: 1px 8px; border-radius: 999px; background-color: rgba(244, 185, 66, .22); }
-.jarvis-panel-state-label { color: var(--jarvis-accent, #F4B942); font-size: 12px; font-weight: bold; }
+/* Jarvis brand tokens. Gold is presence; cyan is thought. Accent is overridden live via --jarvis-accent. */
+.jarvis-panel-box {
+ spacing: 7px;
+ --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-item { padding: 0; }
-.jarvis-popup { width: 320px; padding: 12px 14px; spacing: 8px; color: #f6f7fb; }
-.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); }
+.jarvis-popup {
+ 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-title { font-weight: bold; letter-spacing: 0.4px; font-size: 14px; }
-.jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; }
+.jarvis-mark { icon-size: 18px; color: var(--jarvis-accent, #F4B942); }
+.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-tabs { spacing: 6px; }
.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-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-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-actions { spacing: 6px; }
.jarvis-confirm-label { color: #f6f7fb; font-size: 13px; }
@@ -26,22 +66,26 @@
.jarvis-session-thinking { height: 200px; }
.jarvis-transcript { spacing: 6px; }
.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-tool { background-color: transparent; color: #9aa8c0; font-size: 12px; padding: 2px 6px; }
.jarvis-chip-scroll { height: 32px; }
.jarvis-chips { spacing: 6px; }
.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: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-controls { spacing: 6px; }
.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: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-job, .jarvis-target, .jarvis-cu-step { color: #4FD2FF; font-size: 12px; }
.jarvis-agent-cursor { color: var(--jarvis-accent, #F4B942); font-size: 22px; }
.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; }
diff --git a/apps/gnome-extension/jarvis@qvac.local/ui.js b/apps/gnome-extension/jarvis@qvac.local/ui.js
index c1ac7f7..ef80319 100644
--- a/apps/gnome-extension/jarvis@qvac.local/ui.js
+++ b/apps/gnome-extension/jarvis@qvac.local/ui.js
@@ -1,9 +1,26 @@
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import St from 'gi://St';
import Clutter from 'gi://Clutter';
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 POPUP_ROWS = 8;
export const SESSION_WIDTH = 420;
@@ -56,17 +73,19 @@ function wrapLabel(label) {
}
export class ConversationView {
- constructor({ compact = true, maxRows = compact ? POPUP_ROWS : 40 } = {}) {
+ constructor({ compact = true, maxRows = compact ? POPUP_ROWS : 40, brandDir = '' } = {}) {
this.compact = compact;
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.accessible_name = compact ? 'Jarvis voice assistant' : 'Jarvis conversation';
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.accessible_name = 'Jarvis status';
this.status = new St.Label({ text: 'LOCAL', style_class: 'jarvis-local' });
this.status.accessible_name = 'Local model status';
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(new St.Widget({ x_expand: true }));
this.header.add_child(this.status);
@@ -418,6 +437,10 @@ export class JarvisOsd {
this.label.text = text;
this.root.visible = Boolean(text);
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; } }
hide() { this._wake = false; this._clearTimer(); this.root.visible = false; }
@@ -463,8 +486,8 @@ export class ComputerUseChrome {
}
export class SessionPanel {
- constructor() {
- this.view = new ConversationView({ compact: false });
+ constructor({ brandDir = '' } = {}) {
+ this.view = new ConversationView({ compact: false, brandDir });
this.root = this.view.root;
this.minimized = true;
}
diff --git a/docs/settings.md b/docs/settings.md
index 1d79053..1a02e17 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -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
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,
startup, memory ingestion, Lab toggles, transcript retention, and disabling
diff --git a/packaging/install.sh b/packaging/install.sh
index 42e30ed..e946183 100755
--- a/packaging/install.sh
+++ b/packaging/install.sh
@@ -194,6 +194,28 @@ Exec=/bin/false
SystemdService=jarvisd.service
EOF
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/jarvis@qvac.local/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" </dev/null || true
systemctl --user disable --now jarvisd.service 2>/dev/null || true
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}/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/jarvis@qvac.local"
systemctl --user daemon-reload 2>/dev/null || true
if [[ "${2:-}" == "--purge-data" ]]; then
diff --git a/test/brand.test.js b/test/brand.test.js
new file mode 100644
index 0000000..00d7c4f
--- /dev/null
+++ b/test/brand.test.js
@@ -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/jarvis@qvac.local/', 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, /