feat(electrobun): wire persistence — SQLite, 17 themes, font system
Persistence: - bun:sqlite at ~/.config/agor/settings.db (WAL mode, 500ms busy_timeout) - 4 tables: schema_version, settings, projects, custom_themes - 5 RPC handlers: settings.get/set/getAll, projects get/set Theme system (LIVE switching): - All 17 themes ported from Tauri (4 Catppuccin + 7 Editor + 6 Deep Dark) - applyCssVars() sets 26 --ctp-* vars on document.documentElement - Parallel xterm ITheme mapping per theme - theme-store.svelte.ts: Svelte 5 rune store, persists to SQLite Font system: - font-store.svelte.ts: UI/terminal font family + size - Live CSS var application (--ui-font-family/size, --term-font-family/size) - onTermFontChange() callback registry for terminal instances - Persists all 4 font settings to SQLite AppearanceSettings wired: 17-theme grouped dropdown, font steppers Init on startup: restores saved theme + fonts from SQLite
This commit is contained in:
parent
0b9e8b305a
commit
6002a379e4
13 changed files with 1043 additions and 53 deletions
|
|
@ -231861,6 +231861,105 @@ class PtyClient extends EventEmitter2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/bun/settings-db.ts
|
||||||
|
import { Database as Database2 } from "bun:sqlite";
|
||||||
|
import { homedir as homedir3 } from "os";
|
||||||
|
import { mkdirSync as mkdirSync2 } from "fs";
|
||||||
|
import { join as join7 } from "path";
|
||||||
|
var CONFIG_DIR = join7(homedir3(), ".config", "agor");
|
||||||
|
var DB_PATH = join7(CONFIG_DIR, "settings.db");
|
||||||
|
var SCHEMA = `
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA busy_timeout = 500;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_version (
|
||||||
|
version INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
config TEXT NOT NULL -- JSON blob
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS custom_themes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
palette TEXT NOT NULL -- JSON blob
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
class SettingsDb {
|
||||||
|
db;
|
||||||
|
constructor() {
|
||||||
|
mkdirSync2(CONFIG_DIR, { recursive: true });
|
||||||
|
this.db = new Database2(DB_PATH);
|
||||||
|
this.db.exec(SCHEMA);
|
||||||
|
const version = this.db.query("SELECT version FROM schema_version LIMIT 1").get();
|
||||||
|
if (!version) {
|
||||||
|
this.db.exec("INSERT INTO schema_version (version) VALUES (1)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getSetting(key) {
|
||||||
|
const row = this.db.query("SELECT value FROM settings WHERE key = ?").get(key);
|
||||||
|
return row?.value ?? null;
|
||||||
|
}
|
||||||
|
setSetting(key, value) {
|
||||||
|
this.db.query("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
||||||
|
}
|
||||||
|
getAll() {
|
||||||
|
const rows = this.db.query("SELECT key, value FROM settings").all();
|
||||||
|
return Object.fromEntries(rows.map((r) => [r.key, r.value]));
|
||||||
|
}
|
||||||
|
getProject(id) {
|
||||||
|
const row = this.db.query("SELECT config FROM projects WHERE id = ?").get(id);
|
||||||
|
if (!row)
|
||||||
|
return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(row.config);
|
||||||
|
} catch {
|
||||||
|
console.error(`[settings-db] Failed to parse project config for id=${id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setProject(id, config) {
|
||||||
|
const json = JSON.stringify(config);
|
||||||
|
this.db.query("INSERT INTO projects (id, config) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET config = excluded.config").run(id, json);
|
||||||
|
}
|
||||||
|
listProjects() {
|
||||||
|
const rows = this.db.query("SELECT config FROM projects").all();
|
||||||
|
return rows.flatMap((r) => {
|
||||||
|
try {
|
||||||
|
return [JSON.parse(r.config)];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getCustomThemes() {
|
||||||
|
const rows = this.db.query("SELECT id, name, palette FROM custom_themes").all();
|
||||||
|
return rows.flatMap((r) => {
|
||||||
|
try {
|
||||||
|
return [{ id: r.id, name: r.name, palette: JSON.parse(r.palette) }];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
saveCustomTheme(id, name531, palette) {
|
||||||
|
const json = JSON.stringify(palette);
|
||||||
|
this.db.query("INSERT INTO custom_themes (id, name, palette) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name, palette = excluded.palette").run(id, name531, json);
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
this.db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var settingsDb = new SettingsDb;
|
||||||
|
|
||||||
// src/bun/index.ts
|
// src/bun/index.ts
|
||||||
var DEV_SERVER_PORT = 9760;
|
var DEV_SERVER_PORT = 9760;
|
||||||
var DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
var DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
||||||
|
|
@ -231932,6 +232031,53 @@ var rpc = BrowserView.defineRPC({
|
||||||
ptyClient.closeSession(sessionId);
|
ptyClient.closeSession(sessionId);
|
||||||
} catch {}
|
} catch {}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
},
|
||||||
|
"settings.get": ({ key }) => {
|
||||||
|
try {
|
||||||
|
return { value: settingsDb.getSetting(key) };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.get]", err);
|
||||||
|
return { value: null };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.set": ({ key, value }) => {
|
||||||
|
try {
|
||||||
|
settingsDb.setSetting(key, value);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.set]", err);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.getAll": () => {
|
||||||
|
try {
|
||||||
|
return { settings: settingsDb.getAll() };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.getAll]", err);
|
||||||
|
return { settings: {} };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.getProjects": () => {
|
||||||
|
try {
|
||||||
|
const projects = settingsDb.listProjects().map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
config: JSON.stringify(p)
|
||||||
|
}));
|
||||||
|
return { projects };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.getProjects]", err);
|
||||||
|
return { projects: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.setProject": ({ id, config }) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(config);
|
||||||
|
settingsDb.setProject(id, { id, ...parsed });
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.setProject]", err);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
messages: {}
|
messages: {}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Svelte App</title>
|
<title>Svelte App</title>
|
||||||
<script type="module" crossorigin src="/assets/index-I2iZIyVf.js"></script>
|
<script type="module" crossorigin src="/assets/index-CZZnRPP5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-x9Y0o9Mz.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DSZtflYD.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { BrowserWindow, BrowserView, Updater } from "electrobun/bun";
|
import { BrowserWindow, BrowserView, Updater } from "electrobun/bun";
|
||||||
import { PtyClient } from "./pty-client.ts";
|
import { PtyClient } from "./pty-client.ts";
|
||||||
|
import { settingsDb } from "./settings-db.ts";
|
||||||
import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts";
|
import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts";
|
||||||
|
|
||||||
const DEV_SERVER_PORT = 9760; // Project convention: 9700+ range
|
const DEV_SERVER_PORT = 9760; // Project convention: 9700+ range
|
||||||
|
|
@ -96,6 +97,60 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Settings handlers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
"settings.get": ({ key }) => {
|
||||||
|
try {
|
||||||
|
return { value: settingsDb.getSetting(key) };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.get]", err);
|
||||||
|
return { value: null };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"settings.set": ({ key, value }) => {
|
||||||
|
try {
|
||||||
|
settingsDb.setSetting(key, value);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.set]", err);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"settings.getAll": () => {
|
||||||
|
try {
|
||||||
|
return { settings: settingsDb.getAll() };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.getAll]", err);
|
||||||
|
return { settings: {} };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"settings.getProjects": () => {
|
||||||
|
try {
|
||||||
|
const projects = settingsDb.listProjects().map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
config: JSON.stringify(p),
|
||||||
|
}));
|
||||||
|
return { projects };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.getProjects]", err);
|
||||||
|
return { projects: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"settings.setProject": ({ id, config }) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(config);
|
||||||
|
settingsDb.setProject(id, { id, ...parsed });
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[settings.setProject]", err);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
messages: {
|
messages: {
|
||||||
|
|
|
||||||
173
ui-electrobun/src/bun/settings-db.ts
Normal file
173
ui-electrobun/src/bun/settings-db.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
/**
|
||||||
|
* SQLite-backed settings store for the Bun process.
|
||||||
|
* Uses bun:sqlite (built-in, synchronous, zero external deps).
|
||||||
|
* DB path: ~/.config/agor/settings.db
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
import { homedir } from "os";
|
||||||
|
import { mkdirSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
// ── DB path ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const CONFIG_DIR = join(homedir(), ".config", "agor");
|
||||||
|
const DB_PATH = join(CONFIG_DIR, "settings.db");
|
||||||
|
|
||||||
|
// ── Schema ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SCHEMA = `
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA busy_timeout = 500;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_version (
|
||||||
|
version INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
config TEXT NOT NULL -- JSON blob
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS custom_themes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
palette TEXT NOT NULL -- JSON blob
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ── SettingsDb class ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ProjectConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
cwd: string;
|
||||||
|
accent?: string;
|
||||||
|
provider?: string;
|
||||||
|
profile?: string;
|
||||||
|
model?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomTheme {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
palette: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SettingsDb {
|
||||||
|
private db: Database;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Ensure config dir exists before opening DB
|
||||||
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
||||||
|
|
||||||
|
this.db = new Database(DB_PATH);
|
||||||
|
this.db.exec(SCHEMA);
|
||||||
|
|
||||||
|
// Seed schema_version row if missing
|
||||||
|
const version = this.db
|
||||||
|
.query<{ version: number }, []>("SELECT version FROM schema_version LIMIT 1")
|
||||||
|
.get();
|
||||||
|
if (!version) {
|
||||||
|
this.db.exec("INSERT INTO schema_version (version) VALUES (1)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Settings ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
getSetting(key: string): string | null {
|
||||||
|
const row = this.db
|
||||||
|
.query<{ value: string }, [string]>("SELECT value FROM settings WHERE key = ?")
|
||||||
|
.get(key);
|
||||||
|
return row?.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSetting(key: string, value: string): void {
|
||||||
|
this.db
|
||||||
|
.query("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
||||||
|
.run(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Record<string, string> {
|
||||||
|
const rows = this.db
|
||||||
|
.query<{ key: string; value: string }, []>("SELECT key, value FROM settings")
|
||||||
|
.all();
|
||||||
|
return Object.fromEntries(rows.map((r) => [r.key, r.value]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Projects ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
getProject(id: string): ProjectConfig | null {
|
||||||
|
const row = this.db
|
||||||
|
.query<{ config: string }, [string]>("SELECT config FROM projects WHERE id = ?")
|
||||||
|
.get(id);
|
||||||
|
if (!row) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(row.config) as ProjectConfig;
|
||||||
|
} catch {
|
||||||
|
console.error(`[settings-db] Failed to parse project config for id=${id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProject(id: string, config: ProjectConfig): void {
|
||||||
|
const json = JSON.stringify(config);
|
||||||
|
this.db
|
||||||
|
.query("INSERT INTO projects (id, config) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET config = excluded.config")
|
||||||
|
.run(id, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
listProjects(): ProjectConfig[] {
|
||||||
|
const rows = this.db
|
||||||
|
.query<{ config: string }, []>("SELECT config FROM projects")
|
||||||
|
.all();
|
||||||
|
return rows.flatMap((r) => {
|
||||||
|
try {
|
||||||
|
return [JSON.parse(r.config) as ProjectConfig];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Custom Themes ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
getCustomThemes(): CustomTheme[] {
|
||||||
|
const rows = this.db
|
||||||
|
.query<{ id: string; name: string; palette: string }, []>(
|
||||||
|
"SELECT id, name, palette FROM custom_themes"
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
return rows.flatMap((r) => {
|
||||||
|
try {
|
||||||
|
return [{ id: r.id, name: r.name, palette: JSON.parse(r.palette) }];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCustomTheme(id: string, name: string, palette: Record<string, string>): void {
|
||||||
|
const json = JSON.stringify(palette);
|
||||||
|
this.db
|
||||||
|
.query(
|
||||||
|
"INSERT INTO custom_themes (id, name, palette) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name, palette = excluded.palette"
|
||||||
|
)
|
||||||
|
.run(id, name, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton — one DB handle for the process lifetime
|
||||||
|
export const settingsDb = new SettingsDb();
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import ProjectCard from './ProjectCard.svelte';
|
import ProjectCard from './ProjectCard.svelte';
|
||||||
import SettingsDrawer from './SettingsDrawer.svelte';
|
import SettingsDrawer from './SettingsDrawer.svelte';
|
||||||
import CommandPalette from './CommandPalette.svelte';
|
import CommandPalette from './CommandPalette.svelte';
|
||||||
import ToastContainer from './ToastContainer.svelte';
|
import ToastContainer from './ToastContainer.svelte';
|
||||||
|
import { themeStore } from './theme-store.svelte.ts';
|
||||||
|
import { fontStore } from './font-store.svelte.ts';
|
||||||
|
import { appRpc } from './main.ts';
|
||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────
|
||||||
type AgentStatus = 'running' | 'idle' | 'stalled';
|
type AgentStatus = 'running' | 'idle' | 'stalled';
|
||||||
|
|
@ -78,6 +82,12 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Init theme + fonts on mount ────────────────────────────────
|
||||||
|
onMount(() => {
|
||||||
|
themeStore.initTheme(appRpc).catch(console.error);
|
||||||
|
fontStore.initFonts(appRpc).catch(console.error);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Reactive state ─────────────────────────────────────────────
|
// ── Reactive state ─────────────────────────────────────────────
|
||||||
let settingsOpen = $state(false);
|
let settingsOpen = $state(false);
|
||||||
let paletteOpen = $state(false);
|
let paletteOpen = $state(false);
|
||||||
|
|
|
||||||
129
ui-electrobun/src/mainview/font-store.svelte.ts
Normal file
129
ui-electrobun/src/mainview/font-store.svelte.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/**
|
||||||
|
* Svelte 5 rune-based font store.
|
||||||
|
* Manages UI font and terminal font state.
|
||||||
|
* Applies CSS custom properties instantly; persists via settings RPC.
|
||||||
|
* Exposes a callback registry so terminal instances can react to font changes.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { fontStore } from './font-store.svelte';
|
||||||
|
* fontStore.setUIFont('Inter', 14);
|
||||||
|
* fontStore.setTermFont('JetBrains Mono', 13);
|
||||||
|
* const unsub = fontStore.onTermFontChange((family, size) => terminal.options.fontFamily = family);
|
||||||
|
* await fontStore.initFonts(rpc);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Minimal RPC interface ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface SettingsRpc {
|
||||||
|
request: {
|
||||||
|
"settings.set"(p: { key: string; value: string }): Promise<{ ok: boolean }>;
|
||||||
|
"settings.getAll"(p: Record<string, never>): Promise<{ settings: Record<string, string> }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type TermFontCallback = (family: string, size: number) => void;
|
||||||
|
|
||||||
|
// ── Defaults ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
uiFontFamily: "", // empty = CSS fallback (system-ui)
|
||||||
|
uiFontSize: 14,
|
||||||
|
termFontFamily: "", // empty = CSS fallback (JetBrains Mono, monospace)
|
||||||
|
termFontSize: 13,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ── Store ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function createFontStore() {
|
||||||
|
let uiFontFamily = $state<string>(DEFAULTS.uiFontFamily);
|
||||||
|
let uiFontSize = $state<number>(DEFAULTS.uiFontSize);
|
||||||
|
let termFontFamily = $state<string>(DEFAULTS.termFontFamily);
|
||||||
|
let termFontSize = $state<number>(DEFAULTS.termFontSize);
|
||||||
|
|
||||||
|
let rpc: SettingsRpc | null = null;
|
||||||
|
const termCallbacks = new Set<TermFontCallback>();
|
||||||
|
|
||||||
|
// ── CSS helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function applyUICss(family: string, size: number): void {
|
||||||
|
const style = document.documentElement.style;
|
||||||
|
style.setProperty("--ui-font-family", family || "system-ui, -apple-system, sans-serif");
|
||||||
|
style.setProperty("--ui-font-size", `${size}px`);
|
||||||
|
uiFontFamily = family;
|
||||||
|
uiFontSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTermCss(family: string, size: number): void {
|
||||||
|
const style = document.documentElement.style;
|
||||||
|
style.setProperty("--term-font-family", family || "JetBrains Mono, Consolas, monospace");
|
||||||
|
style.setProperty("--term-font-size", `${size}px`);
|
||||||
|
termFontFamily = family;
|
||||||
|
termFontSize = size;
|
||||||
|
for (const cb of termCallbacks) {
|
||||||
|
try { cb(family, size); }
|
||||||
|
catch (err) { console.error("[font-store] termFont callback error:", err); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public API ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function setUIFont(family: string, size: number): void {
|
||||||
|
applyUICss(family, size);
|
||||||
|
if (rpc) {
|
||||||
|
rpc.request["settings.set"]({ key: "ui_font_family", value: family }).catch(console.error);
|
||||||
|
rpc.request["settings.set"]({ key: "ui_font_size", value: String(size) }).catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTermFont(family: string, size: number): void {
|
||||||
|
applyTermCss(family, size);
|
||||||
|
if (rpc) {
|
||||||
|
rpc.request["settings.set"]({ key: "term_font_family", value: family }).catch(console.error);
|
||||||
|
rpc.request["settings.set"]({ key: "term_font_size", value: String(size) }).catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register a callback invoked whenever the terminal font changes. Returns unsubscribe fn. */
|
||||||
|
function onTermFontChange(cb: TermFontCallback): () => void {
|
||||||
|
termCallbacks.add(cb);
|
||||||
|
return () => termCallbacks.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load all persisted font settings on startup. Call once in App.svelte onMount. */
|
||||||
|
async function initFonts(rpcInstance: SettingsRpc): Promise<void> {
|
||||||
|
rpc = rpcInstance;
|
||||||
|
try {
|
||||||
|
const { settings } = await rpc.request["settings.getAll"]({});
|
||||||
|
|
||||||
|
const family = settings["ui_font_family"] ?? DEFAULTS.uiFontFamily;
|
||||||
|
const rawSize = parseInt(settings["ui_font_size"] ?? "", 10);
|
||||||
|
const size = isNaN(rawSize) ? DEFAULTS.uiFontSize : rawSize;
|
||||||
|
|
||||||
|
const tFamily = settings["term_font_family"] ?? DEFAULTS.termFontFamily;
|
||||||
|
const rawTSize = parseInt(settings["term_font_size"] ?? "", 10);
|
||||||
|
const tSize = isNaN(rawTSize) ? DEFAULTS.termFontSize : rawTSize;
|
||||||
|
|
||||||
|
applyUICss(family, size);
|
||||||
|
applyTermCss(tFamily, tSize);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[font-store] Failed to load font settings:", err);
|
||||||
|
applyUICss(DEFAULTS.uiFontFamily, DEFAULTS.uiFontSize);
|
||||||
|
applyTermCss(DEFAULTS.termFontFamily, DEFAULTS.termFontSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get uiFontFamily() { return uiFontFamily; },
|
||||||
|
get uiFontSize() { return uiFontSize; },
|
||||||
|
get termFontFamily() { return termFontFamily; },
|
||||||
|
get termFontSize() { return termFontSize; },
|
||||||
|
setUIFont,
|
||||||
|
setTermFont,
|
||||||
|
onTermFontChange,
|
||||||
|
initFonts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fontStore = createFontStore();
|
||||||
|
|
@ -33,6 +33,9 @@ const rpc = Electroview.defineRPC<PtyRPCSchema>({
|
||||||
|
|
||||||
export const electrobun = new Electroview({ rpc });
|
export const electrobun = new Electroview({ rpc });
|
||||||
|
|
||||||
|
/** Exported for use by stores that need RPC access (theme-store, font-store). */
|
||||||
|
export { rpc as appRpc };
|
||||||
|
|
||||||
const app = mount(App, {
|
const app = mount(App, {
|
||||||
target: document.getElementById("app")!,
|
target: document.getElementById("app")!,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
const THEMES = [
|
import { THEMES, THEME_GROUPS, type ThemeId } from '../themes.ts';
|
||||||
{ id: 'mocha', label: 'Catppuccin Mocha', group: 'Catppuccin' },
|
import { themeStore } from '../theme-store.svelte.ts';
|
||||||
{ id: 'macchiato', label: 'Catppuccin Macchiato', group: 'Catppuccin' },
|
import { fontStore } from '../font-store.svelte.ts';
|
||||||
{ id: 'frappe', label: 'Catppuccin Frappé', group: 'Catppuccin' },
|
|
||||||
{ id: 'latte', label: 'Catppuccin Latte', group: 'Catppuccin' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const UI_FONTS = [
|
const UI_FONTS = [
|
||||||
{ value: '', label: 'System Default' },
|
{ value: '', label: 'System Default' },
|
||||||
|
|
@ -25,30 +22,70 @@
|
||||||
{ value: 'monospace', label: 'monospace' },
|
{ value: 'monospace', label: 'monospace' },
|
||||||
];
|
];
|
||||||
|
|
||||||
let themeId = $state('mocha');
|
// ── Local reactive state (mirrors store) ───────────────────────────────────
|
||||||
let uiFont = $state('');
|
let themeId = $state<ThemeId>(themeStore.currentTheme);
|
||||||
let uiFontSize = $state(14);
|
let uiFont = $state(fontStore.uiFontFamily);
|
||||||
let termFont = $state('');
|
let uiFontSize = $state(fontStore.uiFontSize);
|
||||||
let termFontSize = $state(13);
|
let termFont = $state(fontStore.termFontFamily);
|
||||||
let cursorStyle = $state('block');
|
let termFontSize = $state(fontStore.termFontSize);
|
||||||
let cursorBlink = $state(true);
|
let cursorStyle = $state('block');
|
||||||
let scrollback = $state(1000);
|
let cursorBlink = $state(true);
|
||||||
|
let scrollback = $state(1000);
|
||||||
|
|
||||||
|
// Keep local state in sync when store changes (e.g. after initTheme)
|
||||||
|
$effect(() => { themeId = themeStore.currentTheme; });
|
||||||
|
$effect(() => { uiFont = fontStore.uiFontFamily; });
|
||||||
|
$effect(() => { uiFontSize = fontStore.uiFontSize; });
|
||||||
|
$effect(() => { termFont = fontStore.termFontFamily; });
|
||||||
|
$effect(() => { termFontSize = fontStore.termFontSize; });
|
||||||
|
|
||||||
|
// ── Dropdown open state ────────────────────────────────────────────────────
|
||||||
let themeOpen = $state(false);
|
let themeOpen = $state(false);
|
||||||
let uiFontOpen = $state(false);
|
let uiFontOpen = $state(false);
|
||||||
let termFontOpen = $state(false);
|
let termFontOpen = $state(false);
|
||||||
|
|
||||||
|
// ── Derived labels ─────────────────────────────────────────────────────────
|
||||||
let themeLabel = $derived(THEMES.find(t => t.id === themeId)?.label ?? 'Catppuccin Mocha');
|
let themeLabel = $derived(THEMES.find(t => t.id === themeId)?.label ?? 'Catppuccin Mocha');
|
||||||
let uiFontLabel = $derived(UI_FONTS.find(f => f.value === uiFont)?.label ?? 'System Default');
|
let uiFontLabel = $derived(UI_FONTS.find(f => f.value === uiFont)?.label ?? 'System Default');
|
||||||
let termFontLabel = $derived(TERM_FONTS.find(f => f.value === termFont)?.label ?? 'Default (JetBrains Mono)');
|
let termFontLabel = $derived(TERM_FONTS.find(f => f.value === termFont)?.label ?? 'Default (JetBrains Mono)');
|
||||||
|
|
||||||
function closeAll() { themeOpen = false; uiFontOpen = false; termFontOpen = false; }
|
// ── Actions ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function handleOutsideClick(e: MouseEvent) {
|
function selectTheme(id: ThemeId): void {
|
||||||
|
themeId = id;
|
||||||
|
themeOpen = false;
|
||||||
|
themeStore.setTheme(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectUIFont(value: string): void {
|
||||||
|
uiFont = value;
|
||||||
|
uiFontOpen = false;
|
||||||
|
fontStore.setUIFont(value, uiFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTermFont(value: string): void {
|
||||||
|
termFont = value;
|
||||||
|
termFontOpen = false;
|
||||||
|
fontStore.setTermFont(value, termFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustUISize(delta: number): void {
|
||||||
|
uiFontSize = Math.max(8, Math.min(24, uiFontSize + delta));
|
||||||
|
fontStore.setUIFont(uiFont, uiFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustTermSize(delta: number): void {
|
||||||
|
termFontSize = Math.max(8, Math.min(24, termFontSize + delta));
|
||||||
|
fontStore.setTermFont(termFont, termFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAll(): void { themeOpen = false; uiFontOpen = false; termFontOpen = false; }
|
||||||
|
|
||||||
|
function handleOutsideClick(e: MouseEvent): void {
|
||||||
if (!(e.target as HTMLElement).closest('.dd-wrap')) closeAll();
|
if (!(e.target as HTMLElement).closest('.dd-wrap')) closeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKey(e: KeyboardEvent) {
|
function handleKey(e: KeyboardEvent): void {
|
||||||
if (e.key === 'Escape') closeAll();
|
if (e.key === 'Escape') closeAll();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -64,12 +101,16 @@
|
||||||
</button>
|
</button>
|
||||||
{#if themeOpen}
|
{#if themeOpen}
|
||||||
<ul class="dd-list" role="listbox">
|
<ul class="dd-list" role="listbox">
|
||||||
{#each THEMES as t}
|
{#each THEME_GROUPS as group}
|
||||||
<li class="dd-item" class:sel={themeId === t.id} role="option" aria-selected={themeId === t.id}
|
<li class="dd-group-label" role="presentation">{group}</li>
|
||||||
tabindex="0"
|
{#each THEMES.filter(t => t.group === group) as t}
|
||||||
onclick={() => { themeId = t.id; themeOpen = false; }}
|
<li class="dd-item" class:sel={themeId === t.id}
|
||||||
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && (themeId = t.id, themeOpen = false)}
|
role="option" aria-selected={themeId === t.id}
|
||||||
>{t.label}</li>
|
tabindex="0"
|
||||||
|
onclick={() => selectTheme(t.id)}
|
||||||
|
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && selectTheme(t.id)}
|
||||||
|
>{t.label}</li>
|
||||||
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -86,19 +127,20 @@
|
||||||
{#if uiFontOpen}
|
{#if uiFontOpen}
|
||||||
<ul class="dd-list" role="listbox">
|
<ul class="dd-list" role="listbox">
|
||||||
{#each UI_FONTS as f}
|
{#each UI_FONTS as f}
|
||||||
<li class="dd-item" class:sel={uiFont === f.value} role="option" aria-selected={uiFont === f.value}
|
<li class="dd-item" class:sel={uiFont === f.value}
|
||||||
|
role="option" aria-selected={uiFont === f.value}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={() => { uiFont = f.value; uiFontOpen = false; }}
|
onclick={() => selectUIFont(f.value)}
|
||||||
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && (uiFont = f.value, uiFontOpen = false)}
|
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && selectUIFont(f.value)}
|
||||||
>{f.label}</li>
|
>{f.label}</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="stepper">
|
<div class="stepper">
|
||||||
<button onclick={() => uiFontSize = Math.max(8, uiFontSize - 1)} aria-label="Decrease UI font size">−</button>
|
<button onclick={() => adjustUISize(-1)} aria-label="Decrease UI font size">−</button>
|
||||||
<span>{uiFontSize}px</span>
|
<span>{uiFontSize}px</span>
|
||||||
<button onclick={() => uiFontSize = Math.min(24, uiFontSize + 1)} aria-label="Increase UI font size">+</button>
|
<button onclick={() => adjustUISize(1)} aria-label="Increase UI font size">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -112,19 +154,20 @@
|
||||||
{#if termFontOpen}
|
{#if termFontOpen}
|
||||||
<ul class="dd-list" role="listbox">
|
<ul class="dd-list" role="listbox">
|
||||||
{#each TERM_FONTS as f}
|
{#each TERM_FONTS as f}
|
||||||
<li class="dd-item" class:sel={termFont === f.value} role="option" aria-selected={termFont === f.value}
|
<li class="dd-item" class:sel={termFont === f.value}
|
||||||
|
role="option" aria-selected={termFont === f.value}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={() => { termFont = f.value; termFontOpen = false; }}
|
onclick={() => selectTermFont(f.value)}
|
||||||
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && (termFont = f.value, termFontOpen = false)}
|
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && selectTermFont(f.value)}
|
||||||
>{f.label}</li>
|
>{f.label}</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="stepper">
|
<div class="stepper">
|
||||||
<button onclick={() => termFontSize = Math.max(8, termFontSize - 1)} aria-label="Decrease terminal font size">−</button>
|
<button onclick={() => adjustTermSize(-1)} aria-label="Decrease terminal font size">−</button>
|
||||||
<span>{termFontSize}px</span>
|
<span>{termFontSize}px</span>
|
||||||
<button onclick={() => termFontSize = Math.min(24, termFontSize + 1)} aria-label="Increase terminal font size">+</button>
|
<button onclick={() => adjustTermSize(1)} aria-label="Increase terminal font size">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -169,9 +212,16 @@
|
||||||
position: absolute; top: calc(100% + 0.125rem); left: 0; right: 0; z-index: 50;
|
position: absolute; top: calc(100% + 0.125rem); left: 0; right: 0; z-index: 50;
|
||||||
list-style: none; margin: 0; padding: 0.25rem;
|
list-style: none; margin: 0; padding: 0.25rem;
|
||||||
background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.3rem;
|
background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.3rem;
|
||||||
max-height: 12rem; overflow-y: auto;
|
max-height: 14rem; overflow-y: auto;
|
||||||
box-shadow: 0 0.5rem 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
|
box-shadow: 0 0.5rem 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
.dd-group-label {
|
||||||
|
padding: 0.25rem 0.5rem 0.125rem;
|
||||||
|
font-size: 0.625rem; font-weight: 700; text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em; color: var(--ctp-overlay0);
|
||||||
|
border-top: 1px solid var(--ctp-surface0);
|
||||||
|
}
|
||||||
|
.dd-group-label:first-child { border-top: none; }
|
||||||
.dd-item {
|
.dd-item {
|
||||||
padding: 0.3rem 0.5rem; border-radius: 0.2rem; font-size: 0.8125rem; color: var(--ctp-subtext1);
|
padding: 0.3rem 0.5rem; border-radius: 0.2rem; font-size: 0.8125rem; color: var(--ctp-subtext1);
|
||||||
cursor: pointer; outline: none; list-style: none;
|
cursor: pointer; outline: none; list-style: none;
|
||||||
|
|
|
||||||
86
ui-electrobun/src/mainview/theme-store.svelte.ts
Normal file
86
ui-electrobun/src/mainview/theme-store.svelte.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* Svelte 5 rune-based theme store.
|
||||||
|
* Applies all 26 --ctp-* CSS vars to document.documentElement instantly.
|
||||||
|
* Persists selection via settings RPC.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { themeStore } from './theme-store.svelte';
|
||||||
|
* themeStore.setTheme('tokyo-night');
|
||||||
|
* await themeStore.initTheme(rpc);
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { applyCssVars, THEME_LIST, type ThemeId } from "./themes.ts";
|
||||||
|
|
||||||
|
const SETTING_KEY = "theme";
|
||||||
|
const DEFAULT_THEME: ThemeId = "mocha";
|
||||||
|
|
||||||
|
// ── Minimal RPC interface ─────────────────────────────────────────────────────
|
||||||
|
// Avoids importing the full Electroview class — just the subset we need.
|
||||||
|
|
||||||
|
interface SettingsRpc {
|
||||||
|
request: {
|
||||||
|
"settings.get"(p: { key: string }): Promise<{ value: string | null }>;
|
||||||
|
"settings.set"(p: { key: string; value: string }): Promise<{ ok: boolean }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function isValidThemeId(id: string): id is ThemeId {
|
||||||
|
return THEME_LIST.some((t) => t.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Store ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function createThemeStore() {
|
||||||
|
let currentThemeId = $state<ThemeId>(DEFAULT_THEME);
|
||||||
|
let rpc: SettingsRpc | null = null;
|
||||||
|
|
||||||
|
/** Apply CSS vars immediately — synchronous, no flash. */
|
||||||
|
function applyToDocument(id: ThemeId): void {
|
||||||
|
applyCssVars(id);
|
||||||
|
currentThemeId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the active theme.
|
||||||
|
* Applies CSS vars immediately and persists asynchronously.
|
||||||
|
*/
|
||||||
|
function setTheme(id: ThemeId): void {
|
||||||
|
applyToDocument(id);
|
||||||
|
if (rpc) {
|
||||||
|
rpc.request["settings.set"]({ key: SETTING_KEY, value: id }).catch((err) => {
|
||||||
|
console.error("[theme-store] Failed to persist theme:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load persisted theme from settings on startup.
|
||||||
|
* Call once in App.svelte onMount.
|
||||||
|
*/
|
||||||
|
async function initTheme(rpcInstance: SettingsRpc): Promise<void> {
|
||||||
|
rpc = rpcInstance;
|
||||||
|
try {
|
||||||
|
const result = await rpc.request["settings.get"]({ key: SETTING_KEY });
|
||||||
|
const saved = result.value;
|
||||||
|
if (saved && isValidThemeId(saved)) {
|
||||||
|
applyToDocument(saved);
|
||||||
|
} else {
|
||||||
|
// Apply default to ensure vars are set even when nothing was persisted.
|
||||||
|
applyToDocument(DEFAULT_THEME);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[theme-store] Failed to load theme from settings:", err);
|
||||||
|
applyToDocument(DEFAULT_THEME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get currentTheme() { return currentThemeId; },
|
||||||
|
setTheme,
|
||||||
|
initTheme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeStore = createThemeStore();
|
||||||
310
ui-electrobun/src/mainview/themes.ts
Normal file
310
ui-electrobun/src/mainview/themes.ts
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
/**
|
||||||
|
* Full 17-theme palette definitions.
|
||||||
|
* Ported from src/lib/styles/themes.ts — kept in sync manually.
|
||||||
|
* Each theme maps to the same 26 --ctp-* CSS custom property slots.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type ThemeId =
|
||||||
|
| "mocha" | "macchiato" | "frappe" | "latte"
|
||||||
|
| "vscode-dark" | "atom-one-dark" | "monokai" | "dracula"
|
||||||
|
| "nord" | "solarized-dark" | "github-dark"
|
||||||
|
| "tokyo-night" | "gruvbox-dark" | "ayu-dark" | "poimandres"
|
||||||
|
| "vesper" | "midnight";
|
||||||
|
|
||||||
|
export interface ThemePalette {
|
||||||
|
rosewater: string; flamingo: string; pink: string; mauve: string;
|
||||||
|
red: string; maroon: string; peach: string; yellow: string;
|
||||||
|
green: string; teal: string; sky: string; sapphire: string;
|
||||||
|
blue: string; lavender: string;
|
||||||
|
text: string; subtext1: string; subtext0: string;
|
||||||
|
overlay2: string; overlay1: string; overlay0: string;
|
||||||
|
surface2: string; surface1: string; surface0: string;
|
||||||
|
base: string; mantle: string; crust: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface XtermTheme {
|
||||||
|
background: string; foreground: string;
|
||||||
|
cursor: string; cursorAccent: string;
|
||||||
|
selectionBackground: string; selectionForeground: string;
|
||||||
|
black: string; red: string; green: string; yellow: string;
|
||||||
|
blue: string; magenta: string; cyan: string; white: string;
|
||||||
|
brightBlack: string; brightRed: string; brightGreen: string;
|
||||||
|
brightYellow: string; brightBlue: string; brightMagenta: string;
|
||||||
|
brightCyan: string; brightWhite: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeMeta {
|
||||||
|
id: ThemeId;
|
||||||
|
label: string;
|
||||||
|
group: string;
|
||||||
|
isDark: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Metadata list ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const THEME_LIST: ThemeMeta[] = [
|
||||||
|
{ id: "mocha", label: "Catppuccin Mocha", group: "Catppuccin", isDark: true },
|
||||||
|
{ id: "macchiato", label: "Catppuccin Macchiato",group: "Catppuccin", isDark: true },
|
||||||
|
{ id: "frappe", label: "Catppuccin Frappé", group: "Catppuccin", isDark: true },
|
||||||
|
{ id: "latte", label: "Catppuccin Latte", group: "Catppuccin", isDark: false },
|
||||||
|
{ id: "vscode-dark", label: "VSCode Dark+", group: "Editor", isDark: true },
|
||||||
|
{ id: "atom-one-dark", label: "Atom One Dark", group: "Editor", isDark: true },
|
||||||
|
{ id: "monokai", label: "Monokai", group: "Editor", isDark: true },
|
||||||
|
{ id: "dracula", label: "Dracula", group: "Editor", isDark: true },
|
||||||
|
{ id: "nord", label: "Nord", group: "Editor", isDark: true },
|
||||||
|
{ id: "solarized-dark", label: "Solarized Dark", group: "Editor", isDark: true },
|
||||||
|
{ id: "github-dark", label: "GitHub Dark", group: "Editor", isDark: true },
|
||||||
|
{ id: "tokyo-night", label: "Tokyo Night", group: "Deep Dark", isDark: true },
|
||||||
|
{ id: "gruvbox-dark", label: "Gruvbox Dark", group: "Deep Dark", isDark: true },
|
||||||
|
{ id: "ayu-dark", label: "Ayu Dark", group: "Deep Dark", isDark: true },
|
||||||
|
{ id: "poimandres", label: "Poimandres", group: "Deep Dark", isDark: true },
|
||||||
|
{ id: "vesper", label: "Vesper", group: "Deep Dark", isDark: true },
|
||||||
|
{ id: "midnight", label: "Midnight", group: "Deep Dark", isDark: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Unique group names in display order. */
|
||||||
|
export const THEME_GROUPS: string[] = [...new Set(THEME_LIST.map((t) => t.group))];
|
||||||
|
|
||||||
|
// ── Palettes ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PALETTES: Record<ThemeId, ThemePalette> = {
|
||||||
|
latte: {
|
||||||
|
rosewater: "#dc8a78", flamingo: "#dd7878", pink: "#ea76cb", mauve: "#8839ef",
|
||||||
|
red: "#d20f39", maroon: "#e64553", peach: "#fe640b", yellow: "#df8e1d",
|
||||||
|
green: "#40a02b", teal: "#179299", sky: "#04a5e5", sapphire: "#209fb5",
|
||||||
|
blue: "#1e66f5", lavender: "#7287fd",
|
||||||
|
text: "#4c4f69", subtext1: "#5c5f77", subtext0: "#6c6f85",
|
||||||
|
overlay2: "#7c7f93", overlay1: "#8c8fa1", overlay0: "#9ca0b0",
|
||||||
|
surface2: "#acb0be", surface1: "#bcc0cc", surface0: "#ccd0da",
|
||||||
|
base: "#eff1f5", mantle: "#e6e9ef", crust: "#dce0e8",
|
||||||
|
},
|
||||||
|
frappe: {
|
||||||
|
rosewater: "#f2d5cf", flamingo: "#eebebe", pink: "#f4b8e4", mauve: "#ca9ee6",
|
||||||
|
red: "#e78284", maroon: "#ea999c", peach: "#ef9f76", yellow: "#e5c890",
|
||||||
|
green: "#a6d189", teal: "#81c8be", sky: "#99d1db", sapphire: "#85c1dc",
|
||||||
|
blue: "#8caaee", lavender: "#babbf1",
|
||||||
|
text: "#c6d0f5", subtext1: "#b5bfe2", subtext0: "#a5adce",
|
||||||
|
overlay2: "#949cbb", overlay1: "#838ba7", overlay0: "#737994",
|
||||||
|
surface2: "#626880", surface1: "#51576d", surface0: "#414559",
|
||||||
|
base: "#303446", mantle: "#292c3c", crust: "#232634",
|
||||||
|
},
|
||||||
|
macchiato: {
|
||||||
|
rosewater: "#f4dbd6", flamingo: "#f0c6c6", pink: "#f5bde6", mauve: "#c6a0f6",
|
||||||
|
red: "#ed8796", maroon: "#ee99a0", peach: "#f5a97f", yellow: "#eed49f",
|
||||||
|
green: "#a6da95", teal: "#8bd5ca", sky: "#91d7e3", sapphire: "#7dc4e4",
|
||||||
|
blue: "#8aadf4", lavender: "#b7bdf8",
|
||||||
|
text: "#cad3f5", subtext1: "#b8c0e0", subtext0: "#a5adcb",
|
||||||
|
overlay2: "#939ab7", overlay1: "#8087a2", overlay0: "#6e738d",
|
||||||
|
surface2: "#5b6078", surface1: "#494d64", surface0: "#363a4f",
|
||||||
|
base: "#24273a", mantle: "#1e2030", crust: "#181926",
|
||||||
|
},
|
||||||
|
mocha: {
|
||||||
|
rosewater: "#f5e0dc", flamingo: "#f2cdcd", pink: "#f5c2e7", mauve: "#cba6f7",
|
||||||
|
red: "#f38ba8", maroon: "#eba0ac", peach: "#fab387", yellow: "#f9e2af",
|
||||||
|
green: "#a6e3a1", teal: "#94e2d5", sky: "#89dceb", sapphire: "#74c7ec",
|
||||||
|
blue: "#89b4fa", lavender: "#b4befe",
|
||||||
|
text: "#cdd6f4", subtext1: "#bac2de", subtext0: "#a6adc8",
|
||||||
|
overlay2: "#9399b2", overlay1: "#7f849c", overlay0: "#6c7086",
|
||||||
|
surface2: "#585b70", surface1: "#45475a", surface0: "#313244",
|
||||||
|
base: "#1e1e2e", mantle: "#181825", crust: "#11111b",
|
||||||
|
},
|
||||||
|
"vscode-dark": {
|
||||||
|
rosewater: "#d4a0a0", flamingo: "#cf8686", pink: "#c586c0", mauve: "#c586c0",
|
||||||
|
red: "#f44747", maroon: "#d16969", peach: "#ce9178", yellow: "#dcdcaa",
|
||||||
|
green: "#6a9955", teal: "#4ec9b0", sky: "#9cdcfe", sapphire: "#4fc1ff",
|
||||||
|
blue: "#569cd6", lavender: "#b4b4f7",
|
||||||
|
text: "#d4d4d4", subtext1: "#cccccc", subtext0: "#b0b0b0",
|
||||||
|
overlay2: "#858585", overlay1: "#6e6e6e", overlay0: "#5a5a5a",
|
||||||
|
surface2: "#3e3e42", surface1: "#333338", surface0: "#2d2d30",
|
||||||
|
base: "#1e1e1e", mantle: "#181818", crust: "#111111",
|
||||||
|
},
|
||||||
|
"atom-one-dark": {
|
||||||
|
rosewater: "#e5c07b", flamingo: "#e06c75", pink: "#c678dd", mauve: "#c678dd",
|
||||||
|
red: "#e06c75", maroon: "#be5046", peach: "#d19a66", yellow: "#e5c07b",
|
||||||
|
green: "#98c379", teal: "#56b6c2", sky: "#56b6c2", sapphire: "#61afef",
|
||||||
|
blue: "#61afef", lavender: "#c8ccd4",
|
||||||
|
text: "#abb2bf", subtext1: "#9da5b4", subtext0: "#8b92a0",
|
||||||
|
overlay2: "#7f848e", overlay1: "#636d83", overlay0: "#545862",
|
||||||
|
surface2: "#474b56", surface1: "#3b3f4c", surface0: "#333842",
|
||||||
|
base: "#282c34", mantle: "#21252b", crust: "#181a1f",
|
||||||
|
},
|
||||||
|
monokai: {
|
||||||
|
rosewater: "#f8f8f2", flamingo: "#f92672", pink: "#f92672", mauve: "#ae81ff",
|
||||||
|
red: "#f92672", maroon: "#f92672", peach: "#fd971f", yellow: "#e6db74",
|
||||||
|
green: "#a6e22e", teal: "#66d9ef", sky: "#66d9ef", sapphire: "#66d9ef",
|
||||||
|
blue: "#66d9ef", lavender: "#ae81ff",
|
||||||
|
text: "#f8f8f2", subtext1: "#e8e8e2", subtext0: "#cfcfc2",
|
||||||
|
overlay2: "#a8a8a2", overlay1: "#90908a", overlay0: "#75715e",
|
||||||
|
surface2: "#595950", surface1: "#49483e", surface0: "#3e3d32",
|
||||||
|
base: "#272822", mantle: "#1e1f1c", crust: "#141411",
|
||||||
|
},
|
||||||
|
dracula: {
|
||||||
|
rosewater: "#f1c4e0", flamingo: "#ff79c6", pink: "#ff79c6", mauve: "#bd93f9",
|
||||||
|
red: "#ff5555", maroon: "#ff6e6e", peach: "#ffb86c", yellow: "#f1fa8c",
|
||||||
|
green: "#50fa7b", teal: "#8be9fd", sky: "#8be9fd", sapphire: "#8be9fd",
|
||||||
|
blue: "#6272a4", lavender: "#bd93f9",
|
||||||
|
text: "#f8f8f2", subtext1: "#e8e8e2", subtext0: "#c0c0ba",
|
||||||
|
overlay2: "#a0a0a0", overlay1: "#7f7f7f", overlay0: "#6272a4",
|
||||||
|
surface2: "#555969", surface1: "#44475a", surface0: "#383a4a",
|
||||||
|
base: "#282a36", mantle: "#21222c", crust: "#191a21",
|
||||||
|
},
|
||||||
|
nord: {
|
||||||
|
rosewater: "#d08770", flamingo: "#bf616a", pink: "#b48ead", mauve: "#b48ead",
|
||||||
|
red: "#bf616a", maroon: "#bf616a", peach: "#d08770", yellow: "#ebcb8b",
|
||||||
|
green: "#a3be8c", teal: "#8fbcbb", sky: "#88c0d0", sapphire: "#81a1c1",
|
||||||
|
blue: "#5e81ac", lavender: "#b48ead",
|
||||||
|
text: "#eceff4", subtext1: "#e5e9f0", subtext0: "#d8dee9",
|
||||||
|
overlay2: "#a5adba", overlay1: "#8891a0", overlay0: "#6c7588",
|
||||||
|
surface2: "#4c566a", surface1: "#434c5e", surface0: "#3b4252",
|
||||||
|
base: "#2e3440", mantle: "#272c36", crust: "#20242c",
|
||||||
|
},
|
||||||
|
"solarized-dark": {
|
||||||
|
rosewater: "#d33682", flamingo: "#dc322f", pink: "#d33682", mauve: "#6c71c4",
|
||||||
|
red: "#dc322f", maroon: "#cb4b16", peach: "#cb4b16", yellow: "#b58900",
|
||||||
|
green: "#859900", teal: "#2aa198", sky: "#2aa198", sapphire: "#268bd2",
|
||||||
|
blue: "#268bd2", lavender: "#6c71c4",
|
||||||
|
text: "#839496", subtext1: "#93a1a1", subtext0: "#778a8b",
|
||||||
|
overlay2: "#657b83", overlay1: "#586e75", overlay0: "#4a6068",
|
||||||
|
surface2: "#1c4753", surface1: "#143845", surface0: "#073642",
|
||||||
|
base: "#002b36", mantle: "#00222b", crust: "#001a21",
|
||||||
|
},
|
||||||
|
"github-dark": {
|
||||||
|
rosewater: "#ffa198", flamingo: "#ff7b72", pink: "#f778ba", mauve: "#d2a8ff",
|
||||||
|
red: "#ff7b72", maroon: "#ffa198", peach: "#ffa657", yellow: "#e3b341",
|
||||||
|
green: "#7ee787", teal: "#56d4dd", sky: "#79c0ff", sapphire: "#79c0ff",
|
||||||
|
blue: "#58a6ff", lavender: "#d2a8ff",
|
||||||
|
text: "#c9d1d9", subtext1: "#b1bac4", subtext0: "#8b949e",
|
||||||
|
overlay2: "#6e7681", overlay1: "#565c64", overlay0: "#484f58",
|
||||||
|
surface2: "#373e47", surface1: "#30363d", surface0: "#21262d",
|
||||||
|
base: "#0d1117", mantle: "#090c10", crust: "#050608",
|
||||||
|
},
|
||||||
|
"tokyo-night": {
|
||||||
|
rosewater: "#f7768e", flamingo: "#ff9e64", pink: "#bb9af7", mauve: "#bb9af7",
|
||||||
|
red: "#f7768e", maroon: "#db4b4b", peach: "#ff9e64", yellow: "#e0af68",
|
||||||
|
green: "#9ece6a", teal: "#73daca", sky: "#7dcfff", sapphire: "#7aa2f7",
|
||||||
|
blue: "#7aa2f7", lavender: "#bb9af7",
|
||||||
|
text: "#c0caf5", subtext1: "#a9b1d6", subtext0: "#9aa5ce",
|
||||||
|
overlay2: "#787c99", overlay1: "#565f89", overlay0: "#414868",
|
||||||
|
surface2: "#3b4261", surface1: "#292e42", surface0: "#232433",
|
||||||
|
base: "#1a1b26", mantle: "#16161e", crust: "#101014",
|
||||||
|
},
|
||||||
|
"gruvbox-dark": {
|
||||||
|
rosewater: "#d65d0e", flamingo: "#cc241d", pink: "#d3869b", mauve: "#b16286",
|
||||||
|
red: "#fb4934", maroon: "#cc241d", peach: "#fe8019", yellow: "#fabd2f",
|
||||||
|
green: "#b8bb26", teal: "#8ec07c", sky: "#83a598", sapphire: "#83a598",
|
||||||
|
blue: "#458588", lavender: "#d3869b",
|
||||||
|
text: "#ebdbb2", subtext1: "#d5c4a1", subtext0: "#bdae93",
|
||||||
|
overlay2: "#a89984", overlay1: "#928374", overlay0: "#7c6f64",
|
||||||
|
surface2: "#504945", surface1: "#3c3836", surface0: "#32302f",
|
||||||
|
base: "#1d2021", mantle: "#191b1c", crust: "#141617",
|
||||||
|
},
|
||||||
|
"ayu-dark": {
|
||||||
|
rosewater: "#f07178", flamingo: "#f07178", pink: "#d2a6ff", mauve: "#d2a6ff",
|
||||||
|
red: "#f07178", maroon: "#f07178", peach: "#ff8f40", yellow: "#ffb454",
|
||||||
|
green: "#aad94c", teal: "#95e6cb", sky: "#73b8ff", sapphire: "#59c2ff",
|
||||||
|
blue: "#59c2ff", lavender: "#d2a6ff",
|
||||||
|
text: "#bfbdb6", subtext1: "#acaaa4", subtext0: "#9b9892",
|
||||||
|
overlay2: "#73726e", overlay1: "#5c5b57", overlay0: "#464542",
|
||||||
|
surface2: "#383838", surface1: "#2c2c2c", surface0: "#242424",
|
||||||
|
base: "#0b0e14", mantle: "#080a0f", crust: "#05070a",
|
||||||
|
},
|
||||||
|
poimandres: {
|
||||||
|
rosewater: "#d0679d", flamingo: "#d0679d", pink: "#fcc5e9", mauve: "#a6accd",
|
||||||
|
red: "#d0679d", maroon: "#d0679d", peach: "#e4f0fb", yellow: "#fffac2",
|
||||||
|
green: "#5de4c7", teal: "#5de4c7", sky: "#89ddff", sapphire: "#add7ff",
|
||||||
|
blue: "#91b4d5", lavender: "#a6accd",
|
||||||
|
text: "#e4f0fb", subtext1: "#d0d6e0", subtext0: "#a6accd",
|
||||||
|
overlay2: "#767c9d", overlay1: "#506477", overlay0: "#3e4f5e",
|
||||||
|
surface2: "#303340", surface1: "#252b37", surface0: "#1e2433",
|
||||||
|
base: "#1b1e28", mantle: "#171922", crust: "#12141c",
|
||||||
|
},
|
||||||
|
vesper: {
|
||||||
|
rosewater: "#de6e6e", flamingo: "#de6e6e", pink: "#c79bf0", mauve: "#c79bf0",
|
||||||
|
red: "#de6e6e", maroon: "#de6e6e", peach: "#ffcfa8", yellow: "#ffc799",
|
||||||
|
green: "#7cb37c", teal: "#6bccb0", sky: "#8abeb7", sapphire: "#6eb4bf",
|
||||||
|
blue: "#6eb4bf", lavender: "#c79bf0",
|
||||||
|
text: "#b8b5ad", subtext1: "#a09d95", subtext0: "#878480",
|
||||||
|
overlay2: "#6e6b66", overlay1: "#55524d", overlay0: "#3d3a36",
|
||||||
|
surface2: "#302e2a", surface1: "#252320", surface0: "#1c1a17",
|
||||||
|
base: "#101010", mantle: "#0a0a0a", crust: "#050505",
|
||||||
|
},
|
||||||
|
midnight: {
|
||||||
|
rosewater: "#e8a0bf", flamingo: "#ea6f91", pink: "#e8a0bf", mauve: "#c4a7e7",
|
||||||
|
red: "#eb6f92", maroon: "#ea6f91", peach: "#f6c177", yellow: "#ebbcba",
|
||||||
|
green: "#9ccfd8", teal: "#9ccfd8", sky: "#a4d4e4", sapphire: "#8bbee8",
|
||||||
|
blue: "#7ba4cc", lavender: "#c4a7e7",
|
||||||
|
text: "#c4c4c4", subtext1: "#a8a8a8", subtext0: "#8c8c8c",
|
||||||
|
overlay2: "#6e6e6e", overlay1: "#525252", overlay0: "#383838",
|
||||||
|
surface2: "#262626", surface1: "#1a1a1a", surface0: "#111111",
|
||||||
|
base: "#000000", mantle: "#000000", crust: "#000000",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const THEMES = THEME_LIST;
|
||||||
|
|
||||||
|
export function getTheme(id: ThemeId): ThemeMeta {
|
||||||
|
return THEME_LIST.find((t) => t.id === id) ?? THEME_LIST[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPalette(id: ThemeId): ThemePalette {
|
||||||
|
return PALETTES[id] ?? PALETTES.mocha;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build xterm.js ITheme from a named theme. */
|
||||||
|
export function getXtermTheme(id: ThemeId): XtermTheme {
|
||||||
|
const p = getPalette(id);
|
||||||
|
return {
|
||||||
|
background: p.base,
|
||||||
|
foreground: p.text,
|
||||||
|
cursor: p.rosewater,
|
||||||
|
cursorAccent: p.base,
|
||||||
|
selectionBackground: p.surface1,
|
||||||
|
selectionForeground: p.text,
|
||||||
|
black: p.surface1,
|
||||||
|
red: p.red,
|
||||||
|
green: p.green,
|
||||||
|
yellow: p.yellow,
|
||||||
|
blue: p.blue,
|
||||||
|
magenta: p.pink,
|
||||||
|
cyan: p.teal,
|
||||||
|
white: p.subtext1,
|
||||||
|
brightBlack: p.surface2,
|
||||||
|
brightRed: p.red,
|
||||||
|
brightGreen: p.green,
|
||||||
|
brightYellow: p.yellow,
|
||||||
|
brightBlue: p.blue,
|
||||||
|
brightMagenta: p.pink,
|
||||||
|
brightCyan: p.teal,
|
||||||
|
brightWhite: p.subtext0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CSS custom property name → palette key mapping (26 vars). */
|
||||||
|
export const CSS_VAR_MAP: [string, keyof ThemePalette][] = [
|
||||||
|
["--ctp-rosewater", "rosewater"], ["--ctp-flamingo", "flamingo"],
|
||||||
|
["--ctp-pink", "pink"], ["--ctp-mauve", "mauve"],
|
||||||
|
["--ctp-red", "red"], ["--ctp-maroon", "maroon"],
|
||||||
|
["--ctp-peach", "peach"], ["--ctp-yellow", "yellow"],
|
||||||
|
["--ctp-green", "green"], ["--ctp-teal", "teal"],
|
||||||
|
["--ctp-sky", "sky"], ["--ctp-sapphire", "sapphire"],
|
||||||
|
["--ctp-blue", "blue"], ["--ctp-lavender", "lavender"],
|
||||||
|
["--ctp-text", "text"], ["--ctp-subtext1", "subtext1"],
|
||||||
|
["--ctp-subtext0", "subtext0"], ["--ctp-overlay2", "overlay2"],
|
||||||
|
["--ctp-overlay1", "overlay1"], ["--ctp-overlay0", "overlay0"],
|
||||||
|
["--ctp-surface2", "surface2"], ["--ctp-surface1", "surface1"],
|
||||||
|
["--ctp-surface0", "surface0"], ["--ctp-base", "base"],
|
||||||
|
["--ctp-mantle", "mantle"], ["--ctp-crust", "crust"],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Apply all 26 --ctp-* CSS custom properties to document.documentElement. */
|
||||||
|
export function applyCssVars(id: ThemeId): void {
|
||||||
|
const p = getPalette(id);
|
||||||
|
const style = document.documentElement.style;
|
||||||
|
for (const [varName, key] of CSS_VAR_MAP) {
|
||||||
|
style.setProperty(varName, p[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,34 @@ export type PtyRPCRequests = {
|
||||||
params: { sessionId: string };
|
params: { sessionId: string };
|
||||||
response: { ok: boolean };
|
response: { ok: boolean };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Settings RPC ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get a single setting value by key. Returns null if not set. */
|
||||||
|
"settings.get": {
|
||||||
|
params: { key: string };
|
||||||
|
response: { value: string | null };
|
||||||
|
};
|
||||||
|
/** Persist a setting key/value pair. */
|
||||||
|
"settings.set": {
|
||||||
|
params: { key: string; value: string };
|
||||||
|
response: { ok: boolean };
|
||||||
|
};
|
||||||
|
/** Return all settings as a flat object. */
|
||||||
|
"settings.getAll": {
|
||||||
|
params: Record<string, never>;
|
||||||
|
response: { settings: Record<string, string> };
|
||||||
|
};
|
||||||
|
/** Return all persisted projects. */
|
||||||
|
"settings.getProjects": {
|
||||||
|
params: Record<string, never>;
|
||||||
|
response: { projects: Array<{ id: string; config: string }> };
|
||||||
|
};
|
||||||
|
/** Persist a project config (JSON-serialised on the caller side). */
|
||||||
|
"settings.setProject": {
|
||||||
|
params: { id: string; config: string };
|
||||||
|
response: { ok: boolean };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Messages (Bun → WebView, fire-and-forget) ────────────────────────────────
|
// ── Messages (Bun → WebView, fire-and-forget) ────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue