瀏覽代碼

refactor: share TUI terminal background detection (#22297)

Kit Langton 4 天之前
父節點
當前提交
3eb6508a64
共有 2 個文件被更改,包括 55 次插入91 次删除
  1. 2 61
      packages/opencode/src/cli/cmd/tui/app.tsx
  2. 53 30
      packages/opencode/src/cli/cmd/tui/util/terminal.ts

+ 2 - 61
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -1,6 +1,7 @@
 import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
 import { Clipboard } from "@tui/util/clipboard"
 import { Selection } from "@tui/util/selection"
+import { Terminal } from "@tui/util/terminal"
 import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
 import { RouteProvider, useRoute } from "@tui/context/route"
 import {
@@ -60,66 +61,6 @@ import { TuiConfig } from "@/config/tui"
 import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
 import { FormatError, FormatUnknownError } from "@/cli/error"
 
-async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
-  // can't set raw mode if not a TTY
-  if (!process.stdin.isTTY) return "dark"
-
-  return new Promise((resolve) => {
-    let timeout: NodeJS.Timeout
-
-    const cleanup = () => {
-      process.stdin.setRawMode(false)
-      process.stdin.removeListener("data", handler)
-      clearTimeout(timeout)
-    }
-
-    const handler = (data: Buffer) => {
-      const str = data.toString()
-      const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
-      if (match) {
-        cleanup()
-        const color = match[1]
-        // Parse RGB values from color string
-        // Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
-        let r = 0,
-          g = 0,
-          b = 0
-
-        if (color.startsWith("rgb:")) {
-          const parts = color.substring(4).split("/")
-          r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
-          g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
-          b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
-        } else if (color.startsWith("#")) {
-          r = parseInt(color.substring(1, 3), 16)
-          g = parseInt(color.substring(3, 5), 16)
-          b = parseInt(color.substring(5, 7), 16)
-        } else if (color.startsWith("rgb(")) {
-          const parts = color.substring(4, color.length - 1).split(",")
-          r = parseInt(parts[0])
-          g = parseInt(parts[1])
-          b = parseInt(parts[2])
-        }
-
-        // Calculate luminance using relative luminance formula
-        const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
-
-        // Determine if dark or light based on luminance threshold
-        resolve(luminance > 0.5 ? "light" : "dark")
-      }
-    }
-
-    process.stdin.setRawMode(true)
-    process.stdin.on("data", handler)
-    process.stdout.write("\x1b]11;?\x07")
-
-    timeout = setTimeout(() => {
-      cleanup()
-      resolve("dark")
-    }, 1000)
-  })
-}
-
 import type { EventSource } from "./context/sdk"
 import { DialogVariant } from "./component/dialog-variant"
 
@@ -178,7 +119,7 @@ export function tui(input: {
     const unguard = win32InstallCtrlCGuard()
     win32DisableProcessedInput()
 
-    const mode = await getTerminalBackgroundColor()
+    const mode = await Terminal.getTerminalBackgroundColor()
 
     // Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
     // the original console mode which re-enables ENABLE_PROCESSED_INPUT.

+ 53 - 30
packages/opencode/src/cli/cmd/tui/util/terminal.ts

@@ -2,6 +2,28 @@ import { RGBA } from "@opentui/core"
 
 export namespace Terminal {
   export type Colors = Awaited<ReturnType<typeof colors>>
+
+  function parse(color: string): RGBA | null {
+    if (color.startsWith("rgb:")) {
+      const parts = color.substring(4).split("/")
+      return RGBA.fromInts(parseInt(parts[0], 16) >> 8, parseInt(parts[1], 16) >> 8, parseInt(parts[2], 16) >> 8, 255)
+    }
+    if (color.startsWith("#")) {
+      return RGBA.fromHex(color)
+    }
+    if (color.startsWith("rgb(")) {
+      const parts = color.substring(4, color.length - 1).split(",")
+      return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
+    }
+    return null
+  }
+
+  function mode(bg: RGBA | null): "dark" | "light" {
+    if (!bg) return "dark"
+    const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255
+    return luminance > 0.5 ? "light" : "dark"
+  }
+
   /**
    * Query terminal colors including background, foreground, and palette (0-15).
    * Uses OSC escape sequences to retrieve actual terminal color values.
@@ -31,46 +53,26 @@ export namespace Terminal {
         clearTimeout(timeout)
       }
 
-      const parseColor = (colorStr: string): RGBA | null => {
-        if (colorStr.startsWith("rgb:")) {
-          const parts = colorStr.substring(4).split("/")
-          return RGBA.fromInts(
-            parseInt(parts[0], 16) >> 8, // Convert 16-bit to 8-bit
-            parseInt(parts[1], 16) >> 8,
-            parseInt(parts[2], 16) >> 8,
-            255,
-          )
-        }
-        if (colorStr.startsWith("#")) {
-          return RGBA.fromHex(colorStr)
-        }
-        if (colorStr.startsWith("rgb(")) {
-          const parts = colorStr.substring(4, colorStr.length - 1).split(",")
-          return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
-        }
-        return null
-      }
-
       const handler = (data: Buffer) => {
         const str = data.toString()
 
         // Match OSC 11 (background color)
         const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
         if (bgMatch) {
-          background = parseColor(bgMatch[1])
+          background = parse(bgMatch[1])
         }
 
         // Match OSC 10 (foreground color)
         const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
         if (fgMatch) {
-          foreground = parseColor(fgMatch[1])
+          foreground = parse(fgMatch[1])
         }
 
         // Match OSC 4 (palette colors)
         const paletteMatches = str.matchAll(/\x1b]4;(\d+);([^\x07\x1b]+)/g)
         for (const match of paletteMatches) {
           const index = parseInt(match[1])
-          const color = parseColor(match[2])
+          const color = parse(match[2])
           if (color) paletteColors[index] = color
         }
 
@@ -100,15 +102,36 @@ export namespace Terminal {
     })
   }
 
+  // Keep startup mode detection separate from `colors()`: the TUI boot path only
+  // needs OSC 11 and should resolve on the first background response instead of
+  // waiting on the full palette query used by system theme generation.
   export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
-    const result = await colors()
-    if (!result.background) return "dark"
+    if (!process.stdin.isTTY) return "dark"
 
-    const { r, g, b } = result.background
-    // Calculate luminance using relative luminance formula
-    const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
+    return new Promise((resolve) => {
+      let timeout: NodeJS.Timeout
 
-    // Determine if dark or light based on luminance threshold
-    return luminance > 0.5 ? "light" : "dark"
+      const cleanup = () => {
+        process.stdin.setRawMode(false)
+        process.stdin.removeListener("data", handler)
+        clearTimeout(timeout)
+      }
+
+      const handler = (data: Buffer) => {
+        const match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/)
+        if (!match) return
+        cleanup()
+        resolve(mode(parse(match[1])))
+      }
+
+      process.stdin.setRawMode(true)
+      process.stdin.on("data", handler)
+      process.stdout.write("\x1b]11;?\x07")
+
+      timeout = setTimeout(() => {
+        cleanup()
+        resolve("dark")
+      }, 1000)
+    })
   }
 }