Compare commits

...

1 Commits

Author SHA1 Message Date
vimtor 569b85dc0c feat(plugin): add select dialog shortcuts 2026-09-09 10:39:24 +00:00
4 changed files with 93 additions and 0 deletions
+8
View File
@@ -358,9 +358,17 @@ export interface DialogSelectOption<Value> {
readonly disabled?: boolean
}
export interface DialogSelectShortcut {
readonly title: string
readonly key: string
readonly side?: "left" | "right"
}
export interface DialogSelectOptions<Value> {
readonly title: string
readonly placeholder?: string
/** Shortcut hints shown beneath the options. */
readonly shortcuts?: readonly DialogSelectShortcut[]
readonly options: readonly DialogSelectOption<Value>[]
readonly current?: Value
}
+5
View File
@@ -320,6 +320,11 @@ export function createDialogApi(
<DialogSelect
title={options.title}
placeholder={options.placeholder}
footerHints={options.shortcuts?.map((item) => ({
title: item.title,
label: item.key,
side: item.side,
}))}
options={options.options.map((option) => ({ ...option }))}
current={options.current}
onSelect={(option) => {
@@ -166,6 +166,66 @@ async function mountSelect<T>(
return { app, moved, replaceOptions, replaceCurrent, selected }
}
async function mountPluginSelect(root: string) {
const state = path.join(root, "state")
await mkdir(state, { recursive: true })
const config = createTuiResolvedConfig()
const [
{ ConfigProvider },
{ ThemeProvider },
{ Keymap },
{ DialogProvider, useDialog },
{ ToastProvider },
{ createDialogApi },
] = await Promise.all([
import("../../../src/config"),
import("../../../src/context/theme"),
import("../../../src/context/keymap"),
import("../../../src/ui/dialog"),
import("../../../src/ui/toast"),
import("../../../src/plugin/api"),
])
function Harness() {
function Select() {
const dialog = useDialog()
onMount(() => {
void createDialogApi(dialog, (render) => render()).select({
title: "Plugin items",
shortcuts: [
{ title: "delete", key: "ctrl+d" },
{ title: "rename", key: "ctrl+r" },
{ title: "current directory", key: "ctrl+a", side: "right" },
],
options: [{ title: "Alpha", value: "alpha" }],
})
})
return null
}
return (
<TestTuiContexts directory={root} paths={{ home: root, state, worktree: root }}>
<ConfigProvider config={config}>
<Keymap.Provider>
<ThemeProvider mode="dark" source={emptyThemeSource}>
<ToastProvider>
<DialogProvider>
<Select />
</DialogProvider>
</ToastProvider>
</ThemeProvider>
</Keymap.Provider>
</ConfigProvider>
</TestTuiContexts>
)
}
const app = await testRender(() => <Harness />, { width: 80, height: 20, kittyKeyboard: true })
app.renderer.start()
await app.waitForFrame((frame) => frame.includes("current directory"))
return app
}
test.each([true, false])("filter refs are ready when published with renderFilter=%s", async (renderFilter) => {
await using tmp = await tmpdir()
const queries: string[] = []
@@ -302,6 +362,22 @@ test("renders the complete truncated footer within the option row", async () =>
}
})
test("renders plugin select shortcuts on both sides", async () => {
await using tmp = await tmpdir()
const app = await mountPluginSelect(tmp.path)
try {
const footer = app
.captureCharFrame()
.split("\n")
.find((line) => line.includes("delete ctrl+d"))
expect(footer).toContain("rename ctrl+r")
expect(footer).toContain("current directory ctrl+a")
} finally {
app.renderer.destroy()
}
})
test("renders actions with a current selection", async () => {
await using tmp = await tmpdir()
const app = await renderSelect(
@@ -326,6 +326,10 @@ const name = await context.ui.dialog.prompt({ title: "Name", placeholder: "relea
const mode = await context.ui.dialog.select({
title: "Mode",
current: "safe",
shortcuts: [
{ title: "select", key: "enter" },
{ title: "close", key: "esc", side: "right" },
],
options: [
{ title: "Safe", value: "safe", description: "Ask before changes" },
{ title: "Fast", value: "fast", disabled: false, category: "Advanced" },