Эх сурвалжийг харах

fix(tui): add macOS system appearance fallback for theme detection

Terminal.app does not set $COLORFGBG. Fall back to macOS
AppleInterfaceStyle via `defaults read` to correctly detect
light/dark mode on Terminal.app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fanghanjun 7 сар өмнө
parent
commit
432283a256

+ 20 - 19
packages/cli/src/tui/__tests__/theme.test.ts

@@ -1,8 +1,9 @@
-import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
+import { describe, expect, it, vi, afterEach } from "vitest";
 import type { TerminalTheme } from "../theme.js";
 
 describe("detectTerminalTheme", () => {
   const originalEnv = process.env.COLORFGBG;
+  const originalPlatform = process.platform;
 
   afterEach(() => {
     if (originalEnv === undefined) {
@@ -10,44 +11,44 @@ describe("detectTerminalTheme", () => {
     } else {
       process.env.COLORFGBG = originalEnv;
     }
+    Object.defineProperty(process, "platform", { value: originalPlatform });
     vi.resetModules();
   });
 
-  async function detect(colorfgbg?: string): Promise<TerminalTheme> {
-    if (colorfgbg !== undefined) {
-      process.env.COLORFGBG = colorfgbg;
+  async function detect(opts?: { colorfgbg?: string; platform?: string }): Promise<TerminalTheme> {
+    if (opts?.colorfgbg !== undefined) {
+      process.env.COLORFGBG = opts.colorfgbg;
     } else {
       delete process.env.COLORFGBG;
     }
+    if (opts?.platform) {
+      Object.defineProperty(process, "platform", { value: opts.platform });
+    }
     const mod = await import("../theme.js");
     return mod.detectTerminalTheme();
   }
 
-  it("defaults to dark when COLORFGBG is not set", async () => {
-    expect(await detect()).toBe("dark");
-  });
-
-  it("detects dark background from COLORFGBG with bg index 0", async () => {
-    expect(await detect("15;0")).toBe("dark");
+  it("detects dark background from COLORFGBG bg index 0", async () => {
+    expect(await detect({ colorfgbg: "15;0" })).toBe("dark");
   });
 
-  it("detects dark background from three-part COLORFGBG", async () => {
-    expect(await detect("15;0;0")).toBe("dark");
+  it("detects dark from three-part COLORFGBG", async () => {
+    expect(await detect({ colorfgbg: "15;0;0" })).toBe("dark");
   });
 
-  it("detects light background from COLORFGBG with bg index 15", async () => {
-    expect(await detect("0;15")).toBe("light");
+  it("detects light background from COLORFGBG bg index 15", async () => {
+    expect(await detect({ colorfgbg: "0;15" })).toBe("light");
   });
 
-  it("detects light background from bg index 7", async () => {
-    expect(await detect("0;7")).toBe("light");
+  it("detects light from bg index 7", async () => {
+    expect(await detect({ colorfgbg: "0;7" })).toBe("light");
   });
 
   it("treats index 8 as dark", async () => {
-    expect(await detect("15;8")).toBe("dark");
+    expect(await detect({ colorfgbg: "15;8" })).toBe("dark");
   });
 
-  it("defaults to dark for unparseable values", async () => {
-    expect(await detect("garbage")).toBe("dark");
+  it("defaults to dark on non-darwin without COLORFGBG", async () => {
+    expect(await detect({ platform: "linux" })).toBe("dark");
   });
 });

+ 32 - 8
packages/cli/src/tui/theme.ts

@@ -1,17 +1,41 @@
+import { execSync } from "node:child_process";
+
 export type TerminalTheme = "dark" | "light";
 
 /**
- * Detect terminal background via $COLORFGBG (format: "fg;bg" or "fg;other;bg").
- * ANSI color indices 0-6 and 8 are dark; 7 and 9-15 are light.
- * Falls back to "dark" when the variable is missing or unparseable.
+ * Detect terminal background color.
+ *
+ * 1. $COLORFGBG (iTerm2, rxvt, etc.) — format: "fg;bg" or "fg;other;bg"
+ * 2. macOS system appearance via `defaults read` (for Terminal.app which lacks COLORFGBG)
+ * 3. Default: "dark"
  */
 export function detectTerminalTheme(): TerminalTheme {
+  // Method 1: COLORFGBG
   const raw = process.env.COLORFGBG;
-  if (!raw) return "dark";
-  const parts = raw.split(";");
-  const bg = Number(parts[parts.length - 1]);
-  if (Number.isNaN(bg)) return "dark";
-  return bg <= 6 || bg === 8 ? "dark" : "light";
+  if (raw) {
+    const parts = raw.split(";");
+    const bg = Number(parts[parts.length - 1]);
+    if (!Number.isNaN(bg)) {
+      return bg <= 6 || bg === 8 ? "dark" : "light";
+    }
+  }
+
+  // Method 2: macOS system appearance (AppleInterfaceStyle = "Dark" in dark mode)
+  if (process.platform === "darwin") {
+    try {
+      const result = execSync("defaults read -g AppleInterfaceStyle", {
+        encoding: "utf8",
+        timeout: 500,
+        stdio: ["pipe", "pipe", "pipe"],
+      }).trim();
+      return result === "Dark" ? "dark" : "light";
+    } catch {
+      // Command fails when light mode is active (key doesn't exist)
+      return "light";
+    }
+  }
+
+  return "dark";
 }
 
 interface ThemePalette {