Commit 5e606a67172a
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");
+ }
+ },
+ });
+}