summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-15 23:56:51 -0400
committerGitHub <[email protected]>2026-04-16 03:56:51 +0000
commitf6cc228684ef9022c93a158b3fd1cd69c677ec1a (patch)
tree7c12718f21b55ad7a8f1b12890f4e6feb8f31a5d
parent9f4b73b6a330dc606faa22e44454638fa45e49ba (diff)
downloadopencode-f6cc228684ef9022c93a158b3fd1cd69c677ec1a.tar.gz
opencode-f6cc228684ef9022c93a158b3fd1cd69c677ec1a.zip
feat: unwrap cli-tui namespaces to flat exports + barrel (#22759)
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx6
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/error-component.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/logo.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx4
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx4
-rw-r--r--packages/opencode/src/cli/cmd/tui/ui/dialog.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/clipboard.ts274
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/editor.ts46
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/index.ts5
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/selection.ts20
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/sound.ts194
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/terminal.ts230
14 files changed, 394 insertions, 399 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 9e96d5dcb..5102169b5 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -1,7 +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 * as Clipboard from "@tui/util/clipboard"
+import * as Selection from "@tui/util/selection"
+import * as Terminal from "@tui/util/terminal"
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route"
import {
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
index c0e39e0e2..8e24ffb1b 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -11,7 +11,7 @@ import { TextAttributes } from "@opentui/core"
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
import { DialogModel } from "./dialog-model"
import { useKeyboard } from "@opentui/solid"
-import { Clipboard } from "@tui/util/clipboard"
+import * as Clipboard from "@tui/util/clipboard"
import { useToast } from "../ui/toast"
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
diff --git a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx
index e8758b3d7..38df35a04 100644
--- a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx
@@ -1,6 +1,6 @@
import { TextAttributes } from "@opentui/core"
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
-import { Clipboard } from "@tui/util/clipboard"
+import * as Clipboard from "@tui/util/clipboard"
import { createSignal } from "solid-js"
import { Installation } from "@/installation"
import { win32FlushInputBuffer } from "../win32"
diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
index d41d36a6e..e53974871 100644
--- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx
@@ -1,7 +1,7 @@
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
-import { Sound } from "@tui/util/sound"
+import * as Sound from "@tui/util/sound"
import { logo } from "@/cli/logo"
// Shadow markers (rendered chars in parens):
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index b80c32243..20003d846 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -21,9 +21,9 @@ import { DialogStash } from "../dialog-stash"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
import { useRenderer, type JSX } from "@opentui/solid"
-import { Editor } from "@tui/util/editor"
+import * as Editor from "@tui/util/editor"
import { useExit } from "../../context/exit"
-import { Clipboard } from "../../util/clipboard"
+import * as Clipboard from "../../util/clipboard"
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
import { TuiEvent } from "../../event"
import { iife } from "@/util/iife"
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
index 835ac8f5d..412b4d87e 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
@@ -3,7 +3,7 @@ import { useSync } from "@tui/context/sync"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
-import { Clipboard } from "@tui/util/clipboard"
+import * as Clipboard from "@tui/util/clipboard"
import type { PromptInfo } from "@tui/component/prompt/history"
import { strip } from "@tui/component/prompt/part"
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 2ea936c89..75098b608 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -66,10 +66,10 @@ import { SubagentFooter } from "./subagent-footer.tsx"
import { Flag } from "@/flag/flag"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts"
-import { Clipboard } from "../../util/clipboard"
+import * as Clipboard from "../../util/clipboard"
import { Toast, useToast } from "../../ui/toast"
import { useKV } from "../../context/kv.tsx"
-import { Editor } from "../../util/editor"
+import * as Editor from "../../util/editor"
import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt"
import { useExit } from "../../context/exit"
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
index 11c43fe24..29eb6fd4c 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
@@ -5,7 +5,7 @@ import { MouseButton, Renderable, RGBA } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useToast } from "./toast"
import { Flag } from "@/flag/flag"
-import { Selection } from "@tui/util/selection"
+import * as Selection from "@tui/util/selection"
export function Dialog(
props: ParentProps<{
diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
index a67eb04f6..6968b07eb 100644
--- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts
@@ -22,171 +22,169 @@ function writeOsc52(text: string): void {
process.stdout.write(sequence)
}
-export namespace Clipboard {
- export interface Content {
- data: string
- mime: string
- }
+export interface Content {
+ data: string
+ mime: string
+}
- // Checks clipboard for images first, then falls back to text.
- //
- // On Windows prompt/ can call this from multiple paste signals because
- // terminals surface image paste differently:
- // 1. A forwarded Ctrl+V keypress
- // 2. An empty bracketed-paste hint for image-only clipboard in Windows
- // Terminal <1.25
- // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
- export async function read(): Promise<Content | undefined> {
- const os = platform()
+// Checks clipboard for images first, then falls back to text.
+//
+// On Windows prompt/ can call this from multiple paste signals because
+// terminals surface image paste differently:
+// 1. A forwarded Ctrl+V keypress
+// 2. An empty bracketed-paste hint for image-only clipboard in Windows
+// Terminal <1.25
+// 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
+export async function read(): Promise<Content | undefined> {
+ const os = platform()
- if (os === "darwin") {
- const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
- try {
- await Process.run(
- [
- "osascript",
- "-e",
- 'set imageData to the clipboard as "PNGf"',
- "-e",
- `set fileRef to open for access POSIX file "${tmpfile}" with write permission`,
- "-e",
- "set eof fileRef to 0",
- "-e",
- "write imageData to fileRef",
- "-e",
- "close access fileRef",
- ],
- { nothrow: true },
- )
- const buffer = await Filesystem.readBytes(tmpfile)
- return { data: buffer.toString("base64"), mime: "image/png" }
- } catch {
- } finally {
- await fs.rm(tmpfile, { force: true }).catch(() => {})
- }
+ if (os === "darwin") {
+ const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
+ try {
+ await Process.run(
+ [
+ "osascript",
+ "-e",
+ 'set imageData to the clipboard as "PNGf"',
+ "-e",
+ `set fileRef to open for access POSIX file "${tmpfile}" with write permission`,
+ "-e",
+ "set eof fileRef to 0",
+ "-e",
+ "write imageData to fileRef",
+ "-e",
+ "close access fileRef",
+ ],
+ { nothrow: true },
+ )
+ const buffer = await Filesystem.readBytes(tmpfile)
+ return { data: buffer.toString("base64"), mime: "image/png" }
+ } catch {
+ } finally {
+ await fs.rm(tmpfile, { force: true }).catch(() => {})
}
+ }
- // Windows/WSL: probe clipboard for images via PowerShell.
- // Bracketed paste can't carry image data so we read it directly.
- if (os === "win32" || release().includes("WSL")) {
- const script =
- "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
- const base64 = await Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
- nothrow: true,
- })
- if (base64.text) {
- const imageBuffer = Buffer.from(base64.text.trim(), "base64")
- if (imageBuffer.length > 0) {
- return { data: imageBuffer.toString("base64"), mime: "image/png" }
- }
+ // Windows/WSL: probe clipboard for images via PowerShell.
+ // Bracketed paste can't carry image data so we read it directly.
+ if (os === "win32" || release().includes("WSL")) {
+ const script =
+ "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
+ const base64 = await Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
+ nothrow: true,
+ })
+ if (base64.text) {
+ const imageBuffer = Buffer.from(base64.text.trim(), "base64")
+ if (imageBuffer.length > 0) {
+ return { data: imageBuffer.toString("base64"), mime: "image/png" }
}
}
+ }
- if (os === "linux") {
- const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true })
- if (wayland.stdout.byteLength > 0) {
- return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
- }
- const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
- nothrow: true,
- })
- if (x11.stdout.byteLength > 0) {
- return { data: Buffer.from(x11.stdout).toString("base64"), mime: "image/png" }
- }
+ if (os === "linux") {
+ const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true })
+ if (wayland.stdout.byteLength > 0) {
+ return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
}
-
- const text = await clipboardy.read().catch(() => {})
- if (text) {
- return { data: text, mime: "text/plain" }
+ const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
+ nothrow: true,
+ })
+ if (x11.stdout.byteLength > 0) {
+ return { data: Buffer.from(x11.stdout).toString("base64"), mime: "image/png" }
}
}
- const getCopyMethod = lazy(() => {
- const os = platform()
+ const text = await clipboardy.read().catch(() => {})
+ if (text) {
+ return { data: text, mime: "text/plain" }
+ }
+}
- if (os === "darwin" && which("osascript")) {
- console.log("clipboard: using osascript")
- return async (text: string) => {
- const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
- await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
- }
+const getCopyMethod = lazy(() => {
+ const os = platform()
+
+ if (os === "darwin" && which("osascript")) {
+ console.log("clipboard: using osascript")
+ return async (text: string) => {
+ const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
+ await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
}
+ }
- if (os === "linux") {
- if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
- console.log("clipboard: using wl-copy")
- return async (text: string) => {
- const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
- if (!proc.stdin) return
- proc.stdin.write(text)
- proc.stdin.end()
- await proc.exited.catch(() => {})
- }
- }
- if (which("xclip")) {
- console.log("clipboard: using xclip")
- return async (text: string) => {
- const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
- stdin: "pipe",
- stdout: "ignore",
- stderr: "ignore",
- })
- if (!proc.stdin) return
- proc.stdin.write(text)
- proc.stdin.end()
- await proc.exited.catch(() => {})
- }
+ if (os === "linux") {
+ if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
+ console.log("clipboard: using wl-copy")
+ return async (text: string) => {
+ const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
+ if (!proc.stdin) return
+ proc.stdin.write(text)
+ proc.stdin.end()
+ await proc.exited.catch(() => {})
}
- if (which("xsel")) {
- console.log("clipboard: using xsel")
- return async (text: string) => {
- const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
- stdin: "pipe",
- stdout: "ignore",
- stderr: "ignore",
- })
- if (!proc.stdin) return
- proc.stdin.write(text)
- proc.stdin.end()
- await proc.exited.catch(() => {})
- }
+ }
+ if (which("xclip")) {
+ console.log("clipboard: using xclip")
+ return async (text: string) => {
+ const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
+ stdin: "pipe",
+ stdout: "ignore",
+ stderr: "ignore",
+ })
+ if (!proc.stdin) return
+ proc.stdin.write(text)
+ proc.stdin.end()
+ await proc.exited.catch(() => {})
}
}
-
- if (os === "win32") {
- console.log("clipboard: using powershell")
+ if (which("xsel")) {
+ console.log("clipboard: using xsel")
return async (text: string) => {
- // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
- const proc = Process.spawn(
- [
- "powershell.exe",
- "-NonInteractive",
- "-NoProfile",
- "-Command",
- "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
- ],
- {
- stdin: "pipe",
- stdout: "ignore",
- stderr: "ignore",
- },
- )
-
+ const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
+ stdin: "pipe",
+ stdout: "ignore",
+ stderr: "ignore",
+ })
if (!proc.stdin) return
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
}
}
+ }
- console.log("clipboard: no native support")
+ if (os === "win32") {
+ console.log("clipboard: using powershell")
return async (text: string) => {
- await clipboardy.write(text).catch(() => {})
+ // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
+ const proc = Process.spawn(
+ [
+ "powershell.exe",
+ "-NonInteractive",
+ "-NoProfile",
+ "-Command",
+ "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
+ ],
+ {
+ stdin: "pipe",
+ stdout: "ignore",
+ stderr: "ignore",
+ },
+ )
+
+ if (!proc.stdin) return
+ proc.stdin.write(text)
+ proc.stdin.end()
+ await proc.exited.catch(() => {})
}
- })
+ }
- export async function copy(text: string): Promise<void> {
- writeOsc52(text)
- await getCopyMethod()(text)
+ console.log("clipboard: no native support")
+ return async (text: string) => {
+ await clipboardy.write(text).catch(() => {})
}
+})
+
+export async function copy(text: string): Promise<void> {
+ writeOsc52(text)
+ await getCopyMethod()(text)
}
diff --git a/packages/opencode/src/cli/cmd/tui/util/editor.ts b/packages/opencode/src/cli/cmd/tui/util/editor.ts
index 540cf6f49..26e595dfb 100644
--- a/packages/opencode/src/cli/cmd/tui/util/editor.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/editor.ts
@@ -6,32 +6,30 @@ import { CliRenderer } from "@opentui/core"
import { Filesystem } from "@/util"
import { Process } from "@/util"
-export namespace Editor {
- export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
- const editor = process.env["VISUAL"] || process.env["EDITOR"]
- if (!editor) return
+export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
+ const editor = process.env["VISUAL"] || process.env["EDITOR"]
+ if (!editor) return
- const filepath = join(tmpdir(), `${Date.now()}.md`)
- await using _ = defer(async () => rm(filepath, { force: true }))
+ const filepath = join(tmpdir(), `${Date.now()}.md`)
+ await using _ = defer(async () => rm(filepath, { force: true }))
- await Filesystem.write(filepath, opts.value)
- opts.renderer.suspend()
+ await Filesystem.write(filepath, opts.value)
+ opts.renderer.suspend()
+ opts.renderer.currentRenderBuffer.clear()
+ try {
+ const parts = editor.split(" ")
+ const proc = Process.spawn([...parts, filepath], {
+ stdin: "inherit",
+ stdout: "inherit",
+ stderr: "inherit",
+ shell: process.platform === "win32",
+ })
+ await proc.exited
+ const content = await Filesystem.readText(filepath)
+ return content || undefined
+ } finally {
opts.renderer.currentRenderBuffer.clear()
- try {
- const parts = editor.split(" ")
- const proc = Process.spawn([...parts, filepath], {
- stdin: "inherit",
- stdout: "inherit",
- stderr: "inherit",
- shell: process.platform === "win32",
- })
- await proc.exited
- const content = await Filesystem.readText(filepath)
- return content || undefined
- } finally {
- opts.renderer.currentRenderBuffer.clear()
- opts.renderer.resume()
- opts.renderer.requestRender()
- }
+ opts.renderer.resume()
+ opts.renderer.requestRender()
}
}
diff --git a/packages/opencode/src/cli/cmd/tui/util/index.ts b/packages/opencode/src/cli/cmd/tui/util/index.ts
new file mode 100644
index 000000000..a0bdbc3c2
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/util/index.ts
@@ -0,0 +1,5 @@
+export * as Editor from "./editor"
+export * as Selection from "./selection"
+export * as Sound from "./sound"
+export * as Terminal from "./terminal"
+export * as Clipboard from "./clipboard"
diff --git a/packages/opencode/src/cli/cmd/tui/util/selection.ts b/packages/opencode/src/cli/cmd/tui/util/selection.ts
index 1230852dc..d677972ee 100644
--- a/packages/opencode/src/cli/cmd/tui/util/selection.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/selection.ts
@@ -1,4 +1,4 @@
-import { Clipboard } from "./clipboard"
+import * as Clipboard from "./clipboard"
type Toast = {
show: (input: { message: string; variant: "info" | "success" | "warning" | "error" }) => void
@@ -10,16 +10,14 @@ type Renderer = {
clearSelection: () => void
}
-export namespace Selection {
- export function copy(renderer: Renderer, toast: Toast): boolean {
- const text = renderer.getSelection()?.getSelectedText()
- if (!text) return false
+export function copy(renderer: Renderer, toast: Toast): boolean {
+ const text = renderer.getSelection()?.getSelectedText()
+ if (!text) return false
- Clipboard.copy(text)
- .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
- .catch(toast.error)
+ Clipboard.copy(text)
+ .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
+ .catch(toast.error)
- renderer.clearSelection()
- return true
- }
+ renderer.clearSelection()
+ return true
}
diff --git a/packages/opencode/src/cli/cmd/tui/util/sound.ts b/packages/opencode/src/cli/cmd/tui/util/sound.ts
index 1be35eecb..e0a15c1a7 100644
--- a/packages/opencode/src/cli/cmd/tui/util/sound.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/sound.ts
@@ -43,114 +43,112 @@ function args(kind: Kind, file: string, volume: number) {
return [kind, "-c", `(New-Object Media.SoundPlayer '${file.replace(/'/g, "''")}').PlaySync()`]
}
-export namespace Sound {
- let item: Player | null | undefined
- let kind: Kind | null | undefined
- let proc: Process.Child | undefined
- let tail: ReturnType<typeof setTimeout> | undefined
- let cache: Promise<{ hum: string; pulse: string[] }> | undefined
- let seq = 0
- let shot = 0
-
- function load() {
- if (item !== undefined) return item
- try {
- item = new Player({ volume: 0.35 })
- } catch {
- item = null
- }
- return item
+let item: Player | null | undefined
+let kind: Kind | null | undefined
+let proc: Process.Child | undefined
+let tail: ReturnType<typeof setTimeout> | undefined
+let cache: Promise<{ hum: string; pulse: string[] }> | undefined
+let seq = 0
+let shot = 0
+
+function load() {
+ if (item !== undefined) return item
+ try {
+ item = new Player({ volume: 0.35 })
+ } catch {
+ item = null
}
+ return item
+}
- async function file(path: string) {
- mkdirSync(DIR, { recursive: true })
- const next = join(DIR, basename(path))
- const out = Bun.file(next)
- if (await out.exists()) return next
- await Bun.write(out, Bun.file(path))
- return next
- }
+async function file(path: string) {
+ mkdirSync(DIR, { recursive: true })
+ const next = join(DIR, basename(path))
+ const out = Bun.file(next)
+ if (await out.exists()) return next
+ await Bun.write(out, Bun.file(path))
+ return next
+}
- function asset() {
- cache ??= Promise.all([file(HUM), Promise.all(FILE.map(file))]).then(([hum, pulse]) => ({ hum, pulse }))
- return cache
- }
+function asset() {
+ cache ??= Promise.all([file(HUM), Promise.all(FILE.map(file))]).then(([hum, pulse]) => ({ hum, pulse }))
+ return cache
+}
- function pick() {
- if (kind !== undefined) return kind
- kind = LIST.find((item) => which(item)) ?? null
- return kind
- }
+function pick() {
+ if (kind !== undefined) return kind
+ kind = LIST.find((item) => which(item)) ?? null
+ return kind
+}
- function run(file: string, volume: number) {
- const kind = pick()
- if (!kind) return
- return Process.spawn(args(kind, file, volume), {
- stdin: "ignore",
- stdout: "ignore",
- stderr: "ignore",
- })
- }
+function run(file: string, volume: number) {
+ const kind = pick()
+ if (!kind) return
+ return Process.spawn(args(kind, file, volume), {
+ stdin: "ignore",
+ stdout: "ignore",
+ stderr: "ignore",
+ })
+}
- function clear() {
- if (!tail) return
- clearTimeout(tail)
- tail = undefined
- }
+function clear() {
+ if (!tail) return
+ clearTimeout(tail)
+ tail = undefined
+}
- function play(file: string, volume: number) {
- const item = load()
- if (!item) return run(file, volume)?.exited
- return item.play(file, { volume }).catch(() => run(file, volume)?.exited)
- }
+function play(file: string, volume: number) {
+ const item = load()
+ if (!item) return run(file, volume)?.exited
+ return item.play(file, { volume }).catch(() => run(file, volume)?.exited)
+}
- export function start() {
- stop()
- const id = ++seq
- void asset().then(({ hum }) => {
- if (id !== seq) return
- const next = run(hum, 0.24)
- if (!next) return
- proc = next
- void next.exited.then(
- () => {
- if (id !== seq) return
- if (proc === next) proc = undefined
- },
- () => {
- if (id !== seq) return
- if (proc === next) proc = undefined
- },
- )
- })
- }
+export function start() {
+ stop()
+ const id = ++seq
+ void asset().then(({ hum }) => {
+ if (id !== seq) return
+ const next = run(hum, 0.24)
+ if (!next) return
+ proc = next
+ void next.exited.then(
+ () => {
+ if (id !== seq) return
+ if (proc === next) proc = undefined
+ },
+ () => {
+ if (id !== seq) return
+ if (proc === next) proc = undefined
+ },
+ )
+ })
+}
- export function stop(delay = 0) {
- seq++
- clear()
- if (!proc) return
- const next = proc
- if (delay <= 0) {
- proc = undefined
- void Process.stop(next).catch(() => undefined)
- return
- }
- tail = setTimeout(() => {
- tail = undefined
- if (proc === next) proc = undefined
- void Process.stop(next).catch(() => undefined)
- }, delay)
+export function stop(delay = 0) {
+ seq++
+ clear()
+ if (!proc) return
+ const next = proc
+ if (delay <= 0) {
+ proc = undefined
+ void Process.stop(next).catch(() => undefined)
+ return
}
+ tail = setTimeout(() => {
+ tail = undefined
+ if (proc === next) proc = undefined
+ void Process.stop(next).catch(() => undefined)
+ }, delay)
+}
- export function pulse(scale = 1) {
- stop(140)
- const index = shot++ % FILE.length
- void asset()
- .then(({ pulse }) => play(pulse[index], 0.26 + 0.14 * scale))
- .catch(() => undefined)
- }
+export function pulse(scale = 1) {
+ stop(140)
+ const index = shot++ % FILE.length
+ void asset()
+ .then(({ pulse }) => play(pulse[index], 0.26 + 0.14 * scale))
+ .catch(() => undefined)
+}
- export function dispose() {
- stop()
- }
+export function dispose() {
+ stop()
}
diff --git a/packages/opencode/src/cli/cmd/tui/util/terminal.ts b/packages/opencode/src/cli/cmd/tui/util/terminal.ts
index 97b51fb4c..46cf4635a 100644
--- a/packages/opencode/src/cli/cmd/tui/util/terminal.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/terminal.ts
@@ -1,137 +1,135 @@
import { RGBA } from "@opentui/core"
-export namespace Terminal {
- export type Colors = Awaited<ReturnType<typeof colors>>
+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 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)
}
-
- 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"
+ 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
+}
- /**
- * Query terminal colors including background, foreground, and palette (0-15).
- * Uses OSC escape sequences to retrieve actual terminal color values.
- *
- * Note: OSC 4 (palette) queries may not work through tmux as responses are filtered.
- * OSC 10/11 (foreground/background) typically work in most environments.
- *
- * Returns an object with background, foreground, and colors array.
- * Any query that fails will be null/empty.
- */
- export async function colors(): Promise<{
- background: RGBA | null
- foreground: RGBA | null
- colors: RGBA[]
- }> {
- if (!process.stdin.isTTY) return { background: null, foreground: null, colors: [] }
-
- return new Promise((resolve) => {
- let background: RGBA | null = null
- let foreground: RGBA | null = null
- const paletteColors: RGBA[] = []
- let timeout: NodeJS.Timeout
-
- const cleanup = () => {
- process.stdin.setRawMode(false)
- process.stdin.removeListener("data", handler)
- clearTimeout(timeout)
- }
+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.
+ *
+ * Note: OSC 4 (palette) queries may not work through tmux as responses are filtered.
+ * OSC 10/11 (foreground/background) typically work in most environments.
+ *
+ * Returns an object with background, foreground, and colors array.
+ * Any query that fails will be null/empty.
+ */
+export async function colors(): Promise<{
+ background: RGBA | null
+ foreground: RGBA | null
+ colors: RGBA[]
+}> {
+ if (!process.stdin.isTTY) return { background: null, foreground: null, colors: [] }
+
+ return new Promise((resolve) => {
+ let background: RGBA | null = null
+ let foreground: RGBA | null = null
+ const paletteColors: RGBA[] = []
+ 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 handler = (data: Buffer) => {
- const str = data.toString()
-
- // Match OSC 11 (background color)
- const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
- if (bgMatch) {
- background = parse(bgMatch[1])
- }
-
- // Match OSC 10 (foreground color)
- const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
- if (fgMatch) {
- 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 = parse(match[2])
- if (color) paletteColors[index] = color
- }
-
- // Return immediately if we have all 16 palette colors
- if (paletteColors.filter((c) => c !== undefined).length === 16) {
- cleanup()
- resolve({ background, foreground, colors: paletteColors })
- }
+ // Match OSC 11 (background color)
+ const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
+ if (bgMatch) {
+ background = parse(bgMatch[1])
}
- process.stdin.setRawMode(true)
- process.stdin.on("data", handler)
+ // Match OSC 10 (foreground color)
+ const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
+ if (fgMatch) {
+ foreground = parse(fgMatch[1])
+ }
- // Query background (OSC 11)
- process.stdout.write("\x1b]11;?\x07")
- // Query foreground (OSC 10)
- process.stdout.write("\x1b]10;?\x07")
- // Query palette colors 0-15 (OSC 4)
- for (let i = 0; i < 16; i++) {
- process.stdout.write(`\x1b]4;${i};?\x07`)
+ // 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 = parse(match[2])
+ if (color) paletteColors[index] = color
}
- timeout = setTimeout(() => {
+ // Return immediately if we have all 16 palette colors
+ if (paletteColors.filter((c) => c !== undefined).length === 16) {
cleanup()
resolve({ background, foreground, colors: paletteColors })
- }, 1000)
- })
- }
+ }
+ }
- // 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"> {
- if (!process.stdin.isTTY) return "dark"
+ process.stdin.setRawMode(true)
+ process.stdin.on("data", handler)
- return new Promise((resolve) => {
- let timeout: NodeJS.Timeout
+ // Query background (OSC 11)
+ process.stdout.write("\x1b]11;?\x07")
+ // Query foreground (OSC 10)
+ process.stdout.write("\x1b]10;?\x07")
+ // Query palette colors 0-15 (OSC 4)
+ for (let i = 0; i < 16; i++) {
+ process.stdout.write(`\x1b]4;${i};?\x07`)
+ }
- const cleanup = () => {
- process.stdin.setRawMode(false)
- process.stdin.removeListener("data", handler)
- clearTimeout(timeout)
- }
+ timeout = setTimeout(() => {
+ cleanup()
+ resolve({ background, foreground, colors: paletteColors })
+ }, 1000)
+ })
+}
- const handler = (data: Buffer) => {
- const match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/)
- if (!match) return
- cleanup()
- resolve(mode(parse(match[1])))
- }
+// 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"> {
+ if (!process.stdin.isTTY) return "dark"
- process.stdin.setRawMode(true)
- process.stdin.on("data", handler)
- process.stdout.write("\x1b]11;?\x07")
+ return new Promise((resolve) => {
+ let timeout: NodeJS.Timeout
- timeout = setTimeout(() => {
- cleanup()
- resolve("dark")
- }, 1000)
- })
- }
+ 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)
+ })
}