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
|
|
@ -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: {
|
||||
|
|
|
|||
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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue