fix(tui): handle form clipboard shortcut (#43526)

This commit is contained in:
Kit Langton
2026-08-19 18:10:44 -04:00
committed by GitHub
parent 98a9d864e6
commit c4afbc4aae
2 changed files with 78 additions and 7 deletions
+42 -5
View File
@@ -340,16 +340,36 @@ export function FormPrompt(props: {
pick(row.value)
}
function pasteCustom(value: string) {
const current = answerField()
if (!current || textual() || !custom() || confirm()) return false
setStore("selected", rows().length)
updateCustom(current, input() + value)
setStore("editing", true)
return true
}
usePaste((event) => {
if (keymap.mode.current() !== FORM_MODE) return
const current = answerField()
if (!current || textual() || !custom() || confirm()) return
if (!pasteCustom(stripAnsiSequences(decodePasteBytes(event.bytes)).replace(/\r\n?/g, "\n"))) return
event.preventDefault()
setStore("selected", rows().length)
updateCustom(current, input() + stripAnsiSequences(decodePasteBytes(event.bytes)).replace(/\r\n?/g, "\n"))
setStore("editing", true)
})
function pasteClipboard() {
return clipboard
.read()
.then((content) => {
if (content?.mime !== "text/plain") return
const value = stripAnsiSequences(content.data).replace(/\r\n?/g, "\n")
if (store.editing || textual()) {
textarea?.insertText(value)
return
}
pasteCustom(value)
})
.catch(toast.error)
}
function commitInput(text: string) {
const current = answerField()
if (!current) return false
@@ -505,6 +525,23 @@ export function FormPrompt(props: {
onMount(() => onCleanup(keymap.mode.push(FORM_MODE)))
Keymap.createLayer(() => ({
mode: FORM_MODE,
enabled: !confirm() && answerField() !== undefined,
commands: [
{
id: "prompt.paste",
title: "Paste from clipboard",
group: "Form",
run: (_input, event) => {
event?.preventDefault()
event?.stopPropagation()
return pasteClipboard()
},
},
],
}))
Keymap.createLayer(() => ({
mode: FORM_MODE,
priority: 1,
+36 -2
View File
@@ -14,7 +14,13 @@ import { TestTuiContexts } from "../../fixture/tui-environment"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { createApi, createEventStream, createFetch } from "../../fixture/tui-client"
async function mountForm(root: string, width = 80, fields?: FormWithLocation["fields"], height = 20) {
async function mountForm(
root: string,
width = 80,
fields?: FormWithLocation["fields"],
height = 20,
clipboardText?: string,
) {
const state = path.join(root, "state")
await mkdir(state, { recursive: true })
@@ -58,7 +64,7 @@ async function mountForm(root: string, width = 80, fields?: FormWithLocation["fi
}}
clipboard={{
async read() {
return undefined
return clipboardText === undefined ? undefined : { data: clipboardText, mime: "text/plain" }
},
write(text) {
copied.push(text)
@@ -217,6 +223,34 @@ test("pasting on a custom choice opens its editor without submitting", async ()
}
})
test("clipboard shortcut opens a custom choice editor without submitting", async () => {
await using tmp = await tmpdir()
const prompt = await mountForm(
tmp.path,
80,
[
{
key: "target",
type: "string",
options: [{ value: "staging", label: "Staging" }],
custom: true,
},
],
20,
"production\nwest",
)
try {
prompt.app.mockInput.pressKey("v", { ctrl: true })
await prompt.app.waitFor(() => prompt.app.renderer.currentFocusedEditor?.plainText === "production\nwest")
await prompt.app.waitForFrame((frame) => frame.includes("production"))
expect(prompt.app.captureCharFrame()).not.toContain("Type your own answer")
expect(prompt.replies).toEqual([])
} finally {
prompt.app.renderer.destroy()
}
})
test("typing a custom multiselect answer selects it before commit", async () => {
await using tmp = await tmpdir()
const prompt = await mountForm(tmp.path, 80, [