Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/app/src/components/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,13 @@ export const Terminal = (props: TerminalProps) => {
return true
}

// allow for toggle terminal keybinds in parent
// Let terminal-toggle keybind bubble to parent command handlers.
// Returning false prevents xterm from consuming the key event.
const config = settings.keybinds.get(TOGGLE_TERMINAL_ID) ?? DEFAULT_TOGGLE_TERMINAL_KEYBIND
const keybinds = parseKeybind(config)
if (matchKeybind(keybinds, event)) return false

return matchKeybind(keybinds, event)
return true
})

const fit = new mod.FitAddon()
Expand Down
13 changes: 10 additions & 3 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EmptyBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { useTuiConfig } from "@tui/context/tui-config"
import { Identifier } from "@/id/id"
import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind"
Expand All @@ -34,6 +35,7 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
import { resolveTextareaCursor } from "../../util/textarea-cursor"

export type PromptProps = {
sessionID?: string
Expand Down Expand Up @@ -78,6 +80,8 @@ export function Prompt(props: PromptProps) {
const renderer = useRenderer()
const { theme, syntax } = useTheme()
const kv = useKV()
const tuiConfig = useTuiConfig()
const textareaCursor = createMemo(() => resolveTextareaCursor(theme, tuiConfig))

function promptModelWarning() {
toast.show({
Expand Down Expand Up @@ -110,8 +114,9 @@ export function Prompt(props: PromptProps) {
})

createEffect(() => {
input.cursorStyle = textareaCursor().cursorStyle
if (props.disabled) input.cursorColor = theme.backgroundElement
if (!props.disabled) input.cursorColor = theme.text
if (!props.disabled) input.cursorColor = textareaCursor().cursorColor
})

const lastUserMessage = createMemo(() => {
Expand Down Expand Up @@ -1004,12 +1009,14 @@ export function Prompt(props: PromptProps) {
setTimeout(() => {
// setTimeout is a workaround and needs to be addressed properly
if (!input || input.isDestroyed) return
input.cursorColor = theme.text
input.cursorColor = textareaCursor().cursorColor
input.cursorStyle = textareaCursor().cursorStyle
}, 0)
}}
onMouseDown={(r: MouseEvent) => r.target?.focus()}
focusedBackgroundColor={theme.backgroundElement}
cursorColor={theme.text}
cursorColor={textareaCursor().cursorColor}
cursorStyle={textareaCursor().cursorStyle}
syntaxStyle={syntax()}
/>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Locale } from "@/util/locale"
import { Global } from "@/global"
import { useDialog } from "../../ui/dialog"
import { useTuiConfig } from "../../context/tui-config"
import { resolveTextareaCursor } from "../../util/textarea-cursor"

type PermissionStage = "permission" | "always" | "reject"

Expand Down Expand Up @@ -473,6 +474,8 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
const dimensions = useTerminalDimensions()
const narrow = createMemo(() => dimensions().width < 80)
const dialog = useDialog()
const tuiConfig = useTuiConfig()
const cursor = createMemo(() => resolveTextareaCursor(theme, tuiConfig, theme.primary))

useKeyboard((evt) => {
if (dialog.stack.length > 0) return
Expand Down Expand Up @@ -521,7 +524,8 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
focused
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.primary}
cursorColor={cursor().cursorColor}
cursorStyle={cursor().cursorStyle}
keyBindings={textareaKeybindings()}
/>
<box flexDirection="row" gap={2} flexShrink={0}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../component/border"
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import { useDialog } from "../../ui/dialog"
import { useTuiConfig } from "../../context/tui-config"
import { resolveTextareaCursor } from "../../util/textarea-cursor"

export function QuestionPrompt(props: { request: QuestionRequest }) {
const sdk = useSDK()
const { theme } = useTheme()
const keybind = useKeybind()
const bindings = useTextareaKeybindings()
const tuiConfig = useTuiConfig()
const cursor = createMemo(() => resolveTextareaCursor(theme, tuiConfig, theme.primary))

const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
Expand Down Expand Up @@ -391,7 +395,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
maxHeight={6}
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.primary}
cursorColor={cursor().cursorColor}
cursorStyle={cursor().cursorStyle}
keyBindings={bindings()}
/>
</box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { onMount, Show, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useTuiConfig } from "../context/tui-config"
import { resolveTextareaCursor } from "../util/textarea-cursor"

export type DialogExportOptionsProps = {
defaultFilename: string
Expand All @@ -24,6 +26,8 @@ export type DialogExportOptionsProps = {
export function DialogExportOptions(props: DialogExportOptionsProps) {
const dialog = useDialog()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const cursor = () => resolveTextareaCursor(theme, tuiConfig)
let textarea: TextareaRenderable
const [store, setStore] = createStore({
thinking: props.defaultThinking,
Expand Down Expand Up @@ -105,7 +109,8 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
placeholder="Enter filename"
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.text}
cursorColor={cursor().cursorColor}
cursorStyle={cursor().cursorStyle}
/>
</box>
<box flexDirection="column">
Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { onMount, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useTuiConfig } from "../context/tui-config"
import { resolveTextareaCursor } from "../util/textarea-cursor"

export type DialogPromptProps = {
title: string
Expand All @@ -16,6 +18,8 @@ export type DialogPromptProps = {
export function DialogPrompt(props: DialogPromptProps) {
const dialog = useDialog()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const cursor = () => resolveTextareaCursor(theme, tuiConfig)
let textarea: TextareaRenderable

useKeyboard((evt) => {
Expand Down Expand Up @@ -56,7 +60,8 @@ export function DialogPrompt(props: DialogPromptProps) {
placeholder={props.placeholder ?? "Enter text"}
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.text}
cursorColor={cursor().cursorColor}
cursorStyle={cursor().cursorStyle}
/>
</box>
<box paddingBottom={1} gap={1} flexDirection="row">
Expand Down
30 changes: 30 additions & 0 deletions packages/opencode/src/cli/cmd/tui/util/textarea-cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RGBA, type CursorStyle, type CursorStyleOptions } from "@opentui/core"
import type { TuiConfig } from "@/config/tui"

type CursorConfig = TuiConfig.Info

type CursorTheme = {
text: RGBA
[key: string]: unknown
}

export function resolveTextareaCursor(theme: CursorTheme, tui?: CursorConfig, fallbackColor: RGBA = theme.text): {
cursorColor: RGBA
cursorStyle: CursorStyleOptions
} {
const cursorColor = resolveCursorColor(theme, tui?.cursor_color) ?? fallbackColor
return {
cursorColor,
cursorStyle: {
style: (tui?.cursor_style ?? "block") as CursorStyle,
blinking: tui?.cursor_blink ?? true,
},
}
}

function resolveCursorColor(theme: CursorTheme, color?: string): RGBA | undefined {
if (!color) return
if (color.startsWith("#")) return RGBA.fromHex(color)
const maybeThemeColor = theme[color]
if (maybeThemeColor instanceof RGBA) return maybeThemeColor
}
23 changes: 23 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,29 @@ export namespace Config {
ref: "KeybindsConfig",
})

export const TUI = z.object({
scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
scroll_acceleration: z
.object({
enabled: z.boolean().describe("Enable scroll acceleration"),
})
.optional()
.describe("Scroll acceleration settings"),
diff_style: z
.enum(["auto", "stacked"])
.optional()
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
cursor_style: z
.enum(["block", "line", "underline"])
.optional()
.describe("Cursor style for TUI textareas"),
cursor_blink: z.boolean().optional().describe("Whether TUI textarea cursor blinks"),
cursor_color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$|^[a-zA-Z][a-zA-Z0-9_]*$/, "Invalid cursor color format")
.optional()
.describe("Cursor color for TUI textareas. Supports hex (#RRGGBB) or theme color name."),
})
export const Server = z
.object({
port: z.number().int().positive().optional().describe("Port to listen on"),
Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/config/tui-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export const TuiOptions = z.object({
.enum(["auto", "stacked"])
.optional()
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
cursor_style: z.enum(["block", "line", "underline"]).optional().describe("Cursor style for TUI textareas"),
cursor_blink: z.boolean().optional().describe("Whether TUI textarea cursor blinks"),
cursor_color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$|^[a-zA-Z][a-zA-Z0-9_]*$/, "Invalid cursor color format")
.optional()
.describe("Cursor color for TUI textareas. Supports hex (#RRGGBB) or theme color name."),
})

export const TuiInfo = z
Expand Down
12 changes: 11 additions & 1 deletion packages/web/src/content/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,24 @@ Use a dedicated `tui.json` (or `tui.jsonc`) file for TUI-specific settings.
"scroll_acceleration": {
"enabled": true
},
"diff_style": "auto"
"diff_style": "auto",
"cursor_style": "line",
"cursor_blink": false,
"cursor_color": "primary"
}
```

Use `OPENCODE_TUI_CONFIG` to point to a custom TUI config file.

Legacy `theme`, `keybinds`, and `tui` keys in `opencode.json` are deprecated and automatically migrated when possible.

- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.**
- `scroll_speed` - Custom scroll speed multiplier (default: `3`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`.
- `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column.
- `cursor_style` - Cursor shape for TUI textareas. One of `"block"`, `"line"`, or `"underline"` (default: `"block"`).
- `cursor_blink` - Whether the TUI textarea cursor blinks (default: `true`).
- `cursor_color` - Cursor color for TUI textareas. Accepts a hex color like `"#FF5733"` or a theme color name like `"primary"`.

[Learn more about TUI configuration here](/docs/tui#configure).

---
Expand Down
8 changes: 7 additions & 1 deletion packages/web/src/content/docs/tui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,10 @@ You can customize TUI behavior through `tui.json` (or `tui.jsonc`).
"scroll_acceleration": {
"enabled": true
},
"diff_style": "auto"
"diff_style": "auto",
"cursor_style": "line",
"cursor_blink": false,
"cursor_color": "primary"
}
```

Expand All @@ -381,6 +384,9 @@ This is separate from `opencode.json`, which configures server/runtime behavior.
- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.**
- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `0.001`, supports decimal values). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.**
- `diff_style` - Controls diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows a single-column layout.
- `cursor_style` - Cursor shape for TUI textareas. One of `"block"`, `"line"`, or `"underline"`.
- `cursor_blink` - Toggle cursor blinking for TUI textareas.
- `cursor_color` - Cursor color for TUI textareas. Supports hex (for example `"#FF5733"`) or theme color names (for example `"primary"`).

Use `OPENCODE_TUI_CONFIG` to load a custom TUI config path.

Expand Down
Loading