stash
parent
27090c122d
commit
9a5cf7dfe5
|
|
@ -1,7 +1,7 @@
|
|||
import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { Selection } from "@tui/util/selection"
|
||||
import { MouseButton, TextAttributes } from "@opentui/core"
|
||||
import { createCliRenderer, MouseButton, TextAttributes, type CliRendererConfig } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
|
||||
import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32"
|
||||
|
|
@ -103,6 +103,43 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
|||
|
||||
import type { EventSource } from "./context/sdk"
|
||||
|
||||
function rendererConfig(config: TuiConfig.Info): CliRendererConfig {
|
||||
const input = config.tui?.renderer
|
||||
const kitty = input?.use_kitty_keyboard
|
||||
|
||||
return {
|
||||
targetFps: input?.target_fps ?? 60,
|
||||
maxFps: input?.max_fps,
|
||||
gatherStats: input?.gather_stats ?? false,
|
||||
exitOnCtrlC: false,
|
||||
useMouse: input?.use_mouse,
|
||||
enableMouseMovement: input?.enable_mouse_movement,
|
||||
useAlternateScreen: input?.use_alternate_screen,
|
||||
autoFocus: input?.auto_focus ?? false,
|
||||
useKittyKeyboard:
|
||||
kitty === undefined || kitty === true
|
||||
? {}
|
||||
: kitty === false
|
||||
? null
|
||||
: {
|
||||
disambiguate: kitty.disambiguate,
|
||||
alternateKeys: kitty.alternate_keys,
|
||||
events: kitty.events,
|
||||
allKeysAsEscapes: kitty.all_keys_as_escapes,
|
||||
reportText: kitty.report_text,
|
||||
},
|
||||
openConsoleOnError: input?.open_console_on_error ?? false,
|
||||
consoleOptions: {
|
||||
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
|
||||
onCopySelection: (text) => {
|
||||
Clipboard.copy(text).catch((error) => {
|
||||
console.error(`Failed to copy console selection to clipboard: ${error}`)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function tui(input: {
|
||||
url: string
|
||||
args: Args
|
||||
|
|
@ -130,73 +167,58 @@ export function tui(input: {
|
|||
resolve()
|
||||
}
|
||||
|
||||
render(
|
||||
() => {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
|
||||
>
|
||||
<ArgsProvider {...input.args}>
|
||||
<ExitProvider onExit={onExit}>
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider>
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<SDKProvider
|
||||
url={input.url}
|
||||
directory={input.directory}
|
||||
fetch={input.fetch}
|
||||
headers={input.headers}
|
||||
events={input.events}
|
||||
>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
</TuiConfigProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
</KVProvider>
|
||||
</ExitProvider>
|
||||
</ArgsProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
},
|
||||
{
|
||||
targetFps: 60,
|
||||
gatherStats: false,
|
||||
exitOnCtrlC: false,
|
||||
useKittyKeyboard: {},
|
||||
autoFocus: false,
|
||||
openConsoleOnError: false,
|
||||
consoleOptions: {
|
||||
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
|
||||
onCopySelection: (text) => {
|
||||
Clipboard.copy(text).catch((error) => {
|
||||
console.error(`Failed to copy console selection to clipboard: ${error}`)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
const renderer = await createCliRenderer(rendererConfig(input.config))
|
||||
|
||||
await render(() => {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
|
||||
>
|
||||
<ArgsProvider {...input.args}>
|
||||
<ExitProvider onExit={onExit}>
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider>
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<SDKProvider
|
||||
url={input.url}
|
||||
renderer={renderer}
|
||||
directory={input.directory}
|
||||
fetch={input.fetch}
|
||||
headers={input.headers}
|
||||
events={input.events}
|
||||
>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
</TuiConfigProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
</KVProvider>
|
||||
</ExitProvider>
|
||||
</ArgsProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}, renderer)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createSimpleContext } from "./helper"
|
|||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
import { TuiPlugin } from "../plugin"
|
||||
import type { CliRenderer } from "@opentui/core"
|
||||
|
||||
export type EventSource = {
|
||||
on: (handler: (event: Event) => void) => () => void
|
||||
|
|
@ -12,6 +13,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
name: "SDK",
|
||||
init: (props: {
|
||||
url: string
|
||||
renderer: CliRenderer
|
||||
directory?: string
|
||||
fetch?: typeof fetch
|
||||
headers?: RequestInit["headers"]
|
||||
|
|
@ -35,6 +37,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
event: emitter,
|
||||
url: props.url,
|
||||
directory: props.directory,
|
||||
renderer: props.renderer,
|
||||
}).catch((error) => {
|
||||
console.error("Failed to load TUI plugins", error)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -920,6 +920,34 @@ export namespace Config {
|
|||
ref: "KeybindsConfig",
|
||||
})
|
||||
|
||||
const TUIRenderer = z
|
||||
.object({
|
||||
target_fps: z.number().int().positive().optional().describe("Target FPS for the renderer"),
|
||||
max_fps: z.number().int().positive().optional().describe("Maximum FPS for immediate rerenders"),
|
||||
gather_stats: z.boolean().optional().describe("Enable renderer frame statistics collection"),
|
||||
use_mouse: z.boolean().optional().describe("Enable mouse tracking"),
|
||||
enable_mouse_movement: z.boolean().optional().describe("Track mouse movement events"),
|
||||
auto_focus: z.boolean().optional().describe("Auto focus nearest focusable item on click"),
|
||||
use_alternate_screen: z.boolean().optional().describe("Use alternate screen buffer"),
|
||||
open_console_on_error: z.boolean().optional().describe("Open renderer console on uncaught errors"),
|
||||
use_kitty_keyboard: z
|
||||
.union([
|
||||
z.boolean(),
|
||||
z
|
||||
.object({
|
||||
disambiguate: z.boolean().optional(),
|
||||
alternate_keys: z.boolean().optional(),
|
||||
events: z.boolean().optional(),
|
||||
all_keys_as_escapes: z.boolean().optional(),
|
||||
report_text: z.boolean().optional(),
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
.optional()
|
||||
.describe("Kitty keyboard protocol settings. true enables defaults, false disables it."),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const TUI = z.object({
|
||||
scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
|
||||
scroll_acceleration: z
|
||||
|
|
@ -932,6 +960,7 @@ export namespace Config {
|
|||
.enum(["auto", "stacked"])
|
||||
.optional()
|
||||
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
|
||||
renderer: TUIRenderer.optional().describe("Renderer options for the terminal UI"),
|
||||
})
|
||||
|
||||
export const Server = z
|
||||
|
|
|
|||
|
|
@ -81,3 +81,42 @@ test("only reads plugin list from tui.json", async () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("parses renderer options from tui config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
tui: {
|
||||
renderer: {
|
||||
target_fps: 75,
|
||||
auto_focus: true,
|
||||
use_kitty_keyboard: {
|
||||
events: true,
|
||||
report_text: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.tui?.renderer?.target_fps).toBe(75)
|
||||
expect(config.tui?.renderer?.auto_focus).toBe(true)
|
||||
expect(config.tui?.renderer?.use_kitty_keyboard).toEqual({
|
||||
events: true,
|
||||
report_text: true,
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -66,16 +66,19 @@ export type TuiEventBus = {
|
|||
) => () => void
|
||||
}
|
||||
|
||||
export type TuiPluginInput = {
|
||||
export type TuiPluginInput<Renderer = unknown> = {
|
||||
client: ReturnType<typeof createOpencodeClientV2>
|
||||
event: TuiEventBus
|
||||
url: string
|
||||
directory?: string
|
||||
renderer: Renderer
|
||||
}
|
||||
|
||||
export type TuiPlugin = (input: TuiPluginInput, options?: PluginOptions) => Promise<void>
|
||||
export type TuiPlugin<Renderer = unknown> = (input: TuiPluginInput<Renderer>, options?: PluginOptions) => Promise<void>
|
||||
|
||||
export type PluginModule = Plugin | { server?: Plugin; tui?: TuiPlugin; themes?: Record<string, ThemeJson> }
|
||||
export type PluginModule<Renderer = unknown> =
|
||||
| Plugin
|
||||
| { server?: Plugin; tui?: TuiPlugin<Renderer>; themes?: Record<string, ThemeJson> }
|
||||
|
||||
export type AuthHook = {
|
||||
provider: string
|
||||
|
|
|
|||
|
|
@ -164,7 +164,12 @@ You can configure TUI-specific settings through the `tui` option.
|
|||
"scroll_acceleration": {
|
||||
"enabled": true
|
||||
},
|
||||
"diff_style": "auto"
|
||||
"diff_style": "auto",
|
||||
"renderer": {
|
||||
"target_fps": 60,
|
||||
"auto_focus": false,
|
||||
"use_kitty_keyboard": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -174,6 +179,7 @@ Available options:
|
|||
- `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.
|
||||
- `renderer` - Renderer startup options such as `target_fps`, `max_fps`, `gather_stats`, `use_mouse`, `enable_mouse_movement`, `use_alternate_screen`, `auto_focus`, `open_console_on_error`, and `use_kitty_keyboard`.
|
||||
|
||||
[Learn more about using the TUI here](/docs/tui).
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ TUI input includes:
|
|||
|
||||
- `client`: the SDK client for the connected server
|
||||
- `event`: an event bus for server events
|
||||
- `renderer`: the active OpenTUI renderer instance
|
||||
- `url`: server URL
|
||||
- `directory`: optional working directory
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue