Commit 5e606a67172a

Vincent Demeester <vincent@sbr.pm>
2026-02-06 22:13:58
feat(pi): add auto-theme extension for system color scheme sync
Added extension that automatically syncs pi theme with system color scheme. Features: - Reads system color scheme from dconf on startup - Watches for dbus signals when color scheme changes - Automatically updates pi theme setting - Compatible with toggle-color-scheme script Commands: - /theme-sync - Manually sync with system color scheme - /theme-status - Show current theme and system scheme - /theme-watch - Toggle watching for changes System Integration: - Reads: /org/gnome/desktop/interface/color-scheme - Values: prefer-dark or prefer-light - Watches: ca.desrt.dconf Writer Notify signals - Updates: ~/.pi/agent/settings.json Implementation: - Uses dbus-monitor to watch for color-scheme changes - Modifies settings.json directly (theme applies on restart) - Debounced updates (500ms) to avoid rapid switching - Graceful cleanup on exit Inspired by: - pkgs/toggle-color-scheme/toggle-color-scheme.sh - dots/config/emacs/init.el (vde/color-scheme-sync) Note: Theme changes apply on pi restart. Future enhancement could reload theme dynamically if pi API supports it.
1 parent 06d309f
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/auto-theme.ts
@@ -0,0 +1,239 @@
+/**
+ * Auto Theme Extension - Sync pi theme with system color scheme
+ * 
+ * Automatically switches between light/dark theme based on system preference.
+ * 
+ * Features:
+ * - Reads system color scheme from dconf on startup
+ * - Watches for dbus signals when color scheme changes
+ * - Automatically updates pi theme
+ * - Commands: /theme-sync, /theme-status
+ * 
+ * System integration:
+ * - Reads: /org/gnome/desktop/interface/color-scheme (prefer-dark or prefer-light)
+ * - Watches: ca.desrt.dconf Writer Notify signals
+ * - Compatible with toggle-color-scheme script
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { exec } from "node:child_process";
+import { promisify } from "node:util";
+import { promises as fs } from "node:fs";
+import * as path from "node:path";
+import * as os from "node:os";
+
+const execAsync = promisify(exec);
+
+// State
+let isWatching = false;
+let watchProcess: any = null;
+
+// =============================================================================
+// System Color Scheme Detection
+// =============================================================================
+
+async function getSystemColorScheme(): Promise<"dark" | "light" | null> {
+	try {
+		const { stdout } = await execAsync("dconf read /org/gnome/desktop/interface/color-scheme");
+		const scheme = stdout.trim().replace(/'/g, "");
+		
+		if (scheme.includes("prefer-dark")) {
+			return "dark";
+		} else if (scheme.includes("prefer-light")) {
+			return "light";
+		}
+		
+		return null;
+	} catch (error) {
+		console.error("Failed to read system color scheme:", error);
+		return null;
+	}
+}
+
+async function applyTheme(pi: ExtensionAPI, theme: "dark" | "light"): Promise<void> {
+	try {
+		// Update settings.json directly
+		const settingsPath = path.join(os.homedir(), ".pi", "agent", "settings.json");
+		
+		// Read current settings
+		let settings: any = {};
+		try {
+			const content = await fs.readFile(settingsPath, "utf-8");
+			settings = JSON.parse(content);
+		} catch {
+			// File might not exist yet
+		}
+		
+		// Update theme
+		settings.theme = theme;
+		
+		// Write back
+		await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
+		
+		console.log(`✓ Theme setting updated to: ${theme} (will apply on next restart or /reload)`);
+	} catch (error) {
+		console.error("Failed to update theme:", error);
+		throw error;
+	}
+}
+
+async function syncTheme(pi: ExtensionAPI): Promise<"dark" | "light" | null> {
+	const systemScheme = await getSystemColorScheme();
+	
+	if (systemScheme) {
+		await applyTheme(pi, systemScheme);
+		return systemScheme;
+	}
+	
+	return null;
+}
+
+// =============================================================================
+// D-Bus Watcher
+// =============================================================================
+
+function startWatching(pi: ExtensionAPI): void {
+	if (isWatching) {
+		console.log("Already watching for color scheme changes");
+		return;
+	}
+
+	// Use dbus-monitor to watch for color-scheme changes
+	// This is simpler than using dbus library and works reliably
+	const dbusMonitor = exec(
+		"dbus-monitor --session \"type='signal',interface='ca.desrt.dconf.Writer',member='Notify'\"",
+		{ maxBuffer: 1024 * 1024 }
+	);
+
+	let buffer = "";
+
+	dbusMonitor.stdout?.on("data", (data: Buffer) => {
+		buffer += data.toString();
+		const lines = buffer.split("\n");
+		buffer = lines.pop() || "";
+
+		for (const line of lines) {
+			// Look for color-scheme in the signal
+			if (line.includes("color-scheme")) {
+				console.log("Color scheme changed, syncing theme...");
+				// Debounce: wait a bit for dconf to settle
+				setTimeout(() => {
+					syncTheme(pi).then((theme) => {
+						if (theme) {
+							console.log(`Theme synced to: ${theme}`);
+						}
+					});
+				}, 500);
+				break;
+			}
+		}
+	});
+
+	dbusMonitor.on("error", (error) => {
+		console.error("D-Bus monitor error:", error);
+		isWatching = false;
+		watchProcess = null;
+	});
+
+	dbusMonitor.on("exit", (code) => {
+		console.log(`D-Bus monitor exited with code ${code}`);
+		isWatching = false;
+		watchProcess = null;
+	});
+
+	watchProcess = dbusMonitor;
+	isWatching = true;
+	console.log("✓ Watching for system color scheme changes");
+}
+
+function stopWatching(): void {
+	if (watchProcess) {
+		watchProcess.kill();
+		watchProcess = null;
+		isWatching = false;
+		console.log("✓ Stopped watching for color scheme changes");
+	}
+}
+
+// =============================================================================
+// Extension
+// =============================================================================
+
+export default function (pi: ExtensionAPI) {
+	// Sync theme on startup
+	syncTheme(pi).then((theme) => {
+		if (theme) {
+			console.log(`✓ Initial theme set to: ${theme} (from system)`);
+		} else {
+			console.log("Could not detect system color scheme, using current theme");
+		}
+	});
+
+	// Start watching for changes
+	startWatching(pi);
+
+	// Cleanup on extension unload
+	process.on("exit", () => {
+		stopWatching();
+	});
+
+	process.on("SIGINT", () => {
+		stopWatching();
+		process.exit();
+	});
+
+	// Command: /theme-sync
+	pi.registerCommand("theme-sync", {
+		description: "Sync pi theme with system color scheme (applies on next restart)",
+		handler: async (_args, ctx) => {
+			const theme = await syncTheme(pi);
+			if (theme) {
+				ctx.ui.notify(`Theme set to: ${theme} (restart pi to apply)`, "success");
+			} else {
+				ctx.ui.notify("Could not detect system color scheme", "error");
+			}
+		},
+	});
+
+	// Command: /theme-status
+	pi.registerCommand("theme-status", {
+		description: "Show current theme sync status",
+		handler: async (_args, ctx) => {
+			const systemScheme = await getSystemColorScheme();
+			
+			// Read current theme from settings.json
+			let currentTheme = "default";
+			try {
+				const settingsPath = path.join(os.homedir(), ".pi", "agent", "settings.json");
+				const content = await fs.readFile(settingsPath, "utf-8");
+				const settings = JSON.parse(content);
+				currentTheme = settings.theme || "default";
+			} catch {
+				// Ignore
+			}
+
+			const status = `
+System color scheme: ${systemScheme || "unknown"}
+Pi theme (in settings.json): ${currentTheme}
+Watching for changes: ${isWatching ? "yes" : "no"}
+			`.trim();
+
+			ctx.ui.notify(status, "info");
+			console.log(status);
+		},
+	});
+
+	// Command: /theme-watch
+	pi.registerCommand("theme-watch", {
+		description: "Toggle watching for system color scheme changes",
+		handler: async (_args, ctx) => {
+			if (isWatching) {
+				stopWatching();
+				ctx.ui.notify("Stopped watching for color scheme changes", "info");
+			} else {
+				startWatching(pi);
+				ctx.ui.notify("Started watching for color scheme changes", "info");
+			}
+		},
+	});
+}