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:
Hibryda 2026-03-20 05:29:03 +01:00
parent 0b9e8b305a
commit 6002a379e4
13 changed files with 1043 additions and 53 deletions

View file

@ -1,6 +1,7 @@
import path from "path";
import { BrowserWindow, BrowserView, Updater } from "electrobun/bun";
import { PtyClient } from "./pty-client.ts";
import { settingsDb } from "./settings-db.ts";
import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts";
const DEV_SERVER_PORT = 9760; // Project convention: 9700+ range
@ -96,6 +97,60 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
} catch { /* ignore */ }
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: {

View 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();