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
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