tui: fix Windows terminal suspend and input undo keybindings
On Windows, native terminals don't support POSIX suspend (ctrl+z), so we now assign ctrl+z to input undo instead of terminal suspend. Terminal suspend is disabled on Windows to avoid conflicts with the undo functionality.
This commit is contained in:
@@ -26,7 +26,6 @@ const TuiLegacy = z
|
|||||||
interface MigrateInput {
|
interface MigrateInput {
|
||||||
cwd: string
|
cwd: string
|
||||||
directories: string[]
|
directories: string[]
|
||||||
custom?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version"
|
|||||||
import { makeRuntime } from "@/cli/effect/runtime"
|
import { makeRuntime } from "@/cli/effect/runtime"
|
||||||
import { Filesystem, Log } from "@/util"
|
import { Filesystem, Log } from "@/util"
|
||||||
|
|
||||||
|
export namespace TuiConfig {
|
||||||
const log = Log.create({ service: "tui.config" })
|
const log = Log.create({ service: "tui.config" })
|
||||||
|
|
||||||
export const Info = TuiInfo
|
export const Info = TuiInfo
|
||||||
@@ -48,10 +49,6 @@ function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Sco
|
|||||||
return "global"
|
return "global"
|
||||||
}
|
}
|
||||||
|
|
||||||
function customPath() {
|
|
||||||
return Flag.OPENCODE_TUI_CONFIG
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalize(raw: Record<string, unknown>) {
|
function normalize(raw: Record<string, unknown>) {
|
||||||
const data = { ...raw }
|
const data = { ...raw }
|
||||||
if (!("tui" in data)) return data
|
if (!("tui" in data)) return data
|
||||||
@@ -91,30 +88,40 @@ async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadState(ctx: { directory: string }) {
|
async function loadState(ctx: { directory: string }) {
|
||||||
let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
|
// Every config dir we may read from: global config dir, any `.opencode`
|
||||||
|
// folders between cwd and home, and OPENCODE_CONFIG_DIR.
|
||||||
const directories = await ConfigPaths.directories(ctx.directory)
|
const directories = await ConfigPaths.directories(ctx.directory)
|
||||||
const custom = customPath()
|
// One-time migration: extract tui keys (theme/keybinds/tui) from existing
|
||||||
await migrateTuiConfig({ directories, custom, cwd: ctx.directory })
|
// opencode.json files into sibling tui.json files.
|
||||||
// Re-compute after migration since migrateTuiConfig may have created new tui.json files
|
await migrateTuiConfig({ directories, cwd: ctx.directory })
|
||||||
projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
|
|
||||||
|
const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
||||||
|
? []
|
||||||
|
: await ConfigPaths.projectFiles("tui", ctx.directory)
|
||||||
|
|
||||||
const acc: Acc = {
|
const acc: Acc = {
|
||||||
result: {},
|
result: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Global tui config (lowest precedence).
|
||||||
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
|
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
|
||||||
await mergeFile(acc, file, ctx)
|
await mergeFile(acc, file, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (custom) {
|
// 2. Explicit OPENCODE_TUI_CONFIG override, if set.
|
||||||
await mergeFile(acc, custom, ctx)
|
if (Flag.OPENCODE_TUI_CONFIG) {
|
||||||
log.debug("loaded custom tui config", { path: custom })
|
await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx)
|
||||||
|
log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Project tui files, applied root-first so the closest file wins.
|
||||||
for (const file of projectFiles) {
|
for (const file of projectFiles) {
|
||||||
await mergeFile(acc, file, ctx)
|
await mergeFile(acc, file, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
|
||||||
|
// walking up the tree. Also returned below so callers can install plugin
|
||||||
|
// dependencies from each location.
|
||||||
const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
|
const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
|
||||||
|
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
@@ -209,5 +216,4 @@ async function load(text: string, configFilepath: string): Promise<Info> {
|
|||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export * as TuiConfig from "./tui"
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { GlobalBus } from "@/bus/global"
|
|||||||
import { Event } from "../server/event"
|
import { Event } from "../server/event"
|
||||||
import { Account } from "@/account"
|
import { Account } from "@/account"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
import { InvalidError, JsonError } from "./error"
|
|
||||||
import type { ConsoleState } from "./console-state"
|
import type { ConsoleState } from "./console-state"
|
||||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
|
|||||||
@@ -106,7 +106,12 @@ export const Keybinds = z
|
|||||||
input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
|
input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
|
||||||
input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
|
input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
|
||||||
input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
|
input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
|
||||||
input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
|
input_undo: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
// On Windows prepend ctrl+z since terminal_suspend releases the binding.
|
||||||
|
.default(process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z")
|
||||||
|
.describe("Undo in input"),
|
||||||
input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
|
input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
|
||||||
input_word_forward: z
|
input_word_forward: z
|
||||||
.string()
|
.string()
|
||||||
@@ -144,7 +149,12 @@ export const Keybinds = z
|
|||||||
session_child_cycle: z.string().optional().default("right").describe("Go to next child session"),
|
session_child_cycle: z.string().optional().default("right").describe("Go to next child session"),
|
||||||
session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"),
|
session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"),
|
||||||
session_parent: z.string().optional().default("up").describe("Go to parent session"),
|
session_parent: z.string().optional().default("up").describe("Go to parent session"),
|
||||||
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
|
terminal_suspend: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("ctrl+z")
|
||||||
|
.transform((v) => (process.platform === "win32" ? "none" : v))
|
||||||
|
.describe("Suspend terminal"),
|
||||||
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
|
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
|
||||||
tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
|
tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
|
||||||
plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"),
|
plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"),
|
||||||
|
|||||||
Reference in New Issue
Block a user