@markjaquith/agency 2.35.0 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { cleanupTempDir, createTempDir } from "../test-utils"
|
|
4
|
+
|
|
5
|
+
const projectRoot = join(import.meta.dir, "../..")
|
|
6
|
+
const cliPath = join(projectRoot, "cli.ts")
|
|
7
|
+
const tempDirs: string[] = []
|
|
8
|
+
|
|
9
|
+
afterEach(() => Promise.all(tempDirs.splice(0).map(cleanupTempDir)))
|
|
10
|
+
|
|
11
|
+
const modes = (terminal: Bun.Terminal) => ({
|
|
12
|
+
input: terminal.inputFlags,
|
|
13
|
+
output: terminal.outputFlags,
|
|
14
|
+
local: terminal.localFlags,
|
|
15
|
+
control: terminal.controlFlags,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const waitFor = async (condition: () => boolean, output: () => string) => {
|
|
19
|
+
const deadline = Date.now() + 8_000
|
|
20
|
+
while (!condition()) {
|
|
21
|
+
if (Date.now() >= deadline) {
|
|
22
|
+
throw new Error(`Timed out waiting for terminal output:\n${output()}`)
|
|
23
|
+
}
|
|
24
|
+
await Bun.sleep(10)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const waitForExit = (subprocess: Bun.Subprocess, output: () => string) =>
|
|
29
|
+
new Promise<number>((resolve, reject) => {
|
|
30
|
+
const timeout = setTimeout(() => {
|
|
31
|
+
reject(new Error(`Timed out waiting for CLI exit:\n${output()}`))
|
|
32
|
+
}, 8_000)
|
|
33
|
+
subprocess.exited.then(
|
|
34
|
+
(exitCode) => {
|
|
35
|
+
clearTimeout(timeout)
|
|
36
|
+
resolve(exitCode)
|
|
37
|
+
},
|
|
38
|
+
(error) => {
|
|
39
|
+
clearTimeout(timeout)
|
|
40
|
+
reject(error)
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const createWorkbase = async () => {
|
|
46
|
+
const root = await createTempDir()
|
|
47
|
+
tempDirs.push(root)
|
|
48
|
+
const initialized = Bun.spawnSync(
|
|
49
|
+
[process.execPath, cliPath, "init", root, "--silent"],
|
|
50
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
51
|
+
)
|
|
52
|
+
if (initialized.exitCode !== 0) {
|
|
53
|
+
throw new Error(new TextDecoder().decode(initialized.stderr))
|
|
54
|
+
}
|
|
55
|
+
return root
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const runPrompt = async (
|
|
59
|
+
drive: (terminal: Bun.Terminal, output: () => string) => Promise<void>,
|
|
60
|
+
) => {
|
|
61
|
+
const root = await createWorkbase()
|
|
62
|
+
const decoder = new TextDecoder()
|
|
63
|
+
let output = ""
|
|
64
|
+
const terminal = new Bun.Terminal({
|
|
65
|
+
cols: 80,
|
|
66
|
+
rows: 24,
|
|
67
|
+
data: (_terminal, bytes) => {
|
|
68
|
+
output += decoder.decode(bytes, { stream: true })
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
const initialModes = modes(terminal)
|
|
72
|
+
const subprocess = Bun.spawn([process.execPath, cliPath, "task", "new"], {
|
|
73
|
+
cwd: root,
|
|
74
|
+
env: { ...process.env, TERM: "xterm-256color" },
|
|
75
|
+
terminal,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await waitFor(
|
|
80
|
+
() => output.includes("Task ID:"),
|
|
81
|
+
() => output,
|
|
82
|
+
)
|
|
83
|
+
const activeModes = modes(terminal)
|
|
84
|
+
expect(activeModes).not.toEqual(initialModes)
|
|
85
|
+
await drive(terminal, () => output)
|
|
86
|
+
const exitCode = await waitForExit(subprocess, () => output)
|
|
87
|
+
output += decoder.decode()
|
|
88
|
+
expect(exitCode).toBe(1)
|
|
89
|
+
expect(modes(terminal)).toEqual(initialModes)
|
|
90
|
+
expect(output.lastIndexOf("\x1b[?25h")).toBeGreaterThan(
|
|
91
|
+
output.lastIndexOf("\x1b[?25l"),
|
|
92
|
+
)
|
|
93
|
+
expect(output.lastIndexOf("\x1b[?2004l")).toBeGreaterThan(
|
|
94
|
+
output.lastIndexOf("\x1b[?2004h"),
|
|
95
|
+
)
|
|
96
|
+
return output
|
|
97
|
+
} finally {
|
|
98
|
+
if (subprocess.exitCode === null) {
|
|
99
|
+
subprocess.kill("SIGKILL")
|
|
100
|
+
await subprocess.exited
|
|
101
|
+
}
|
|
102
|
+
terminal.close()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("interactive CLI terminal restoration", () => {
|
|
107
|
+
test("restores terminal state after submission, resize, and escape", async () => {
|
|
108
|
+
const output = await runPrompt(async (terminal, currentOutput) => {
|
|
109
|
+
terminal.resize(30, 8)
|
|
110
|
+
terminal.write("pty-contract\r")
|
|
111
|
+
await waitFor(
|
|
112
|
+
() => currentOutput().includes("Ticket URL (optional):"),
|
|
113
|
+
currentOutput,
|
|
114
|
+
)
|
|
115
|
+
terminal.write("\x1b")
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(output).toContain("Failed to read task input")
|
|
119
|
+
}, 12_000)
|
|
120
|
+
|
|
121
|
+
test("restores terminal state after ctrl-c", async () => {
|
|
122
|
+
const output = await runPrompt(async (terminal) => {
|
|
123
|
+
terminal.write("\x03")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(output).toContain("Failed to read task input")
|
|
127
|
+
}, 12_000)
|
|
128
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { KeyCodes, type MockInput } from "@opentui/core/testing"
|
|
2
3
|
import { testRender } from "@opentui/solid"
|
|
3
4
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
4
5
|
import { tmpdir } from "node:os"
|
|
@@ -10,6 +11,34 @@ import {
|
|
|
10
11
|
interactiveRendererConfig,
|
|
11
12
|
} from "./interactive"
|
|
12
13
|
|
|
14
|
+
const submitEditedText = async (
|
|
15
|
+
edit: (input: MockInput) => void | Promise<void>,
|
|
16
|
+
) => {
|
|
17
|
+
let submitted: string | null | undefined
|
|
18
|
+
const setup = await testRender(
|
|
19
|
+
() => (
|
|
20
|
+
<InteractiveTextPrompt
|
|
21
|
+
prompt="Text"
|
|
22
|
+
onDone={(value) => {
|
|
23
|
+
submitted = value
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
),
|
|
27
|
+
{ width: 60, height: 4 },
|
|
28
|
+
)
|
|
29
|
+
try {
|
|
30
|
+
await setup.renderer.setupTerminal()
|
|
31
|
+
await setup.renderOnce()
|
|
32
|
+
await Bun.sleep(0)
|
|
33
|
+
await edit(setup.mockInput)
|
|
34
|
+
setup.mockInput.pressEnter()
|
|
35
|
+
await setup.waitFor(() => submitted !== undefined)
|
|
36
|
+
return submitted
|
|
37
|
+
} finally {
|
|
38
|
+
setup.renderer.destroy()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
describe("OpenTUI interaction", () => {
|
|
14
43
|
test("selects the Solid JSX runtime without the project preload", async () => {
|
|
15
44
|
const source = await Bun.file(
|
|
@@ -109,8 +138,8 @@ describe("OpenTUI interaction", () => {
|
|
|
109
138
|
expect(await selectAfter("down")).toBe("web")
|
|
110
139
|
})
|
|
111
140
|
|
|
112
|
-
test("uses printable j, k, and
|
|
113
|
-
for (const query of ["j", "k", "q"]) {
|
|
141
|
+
test("uses printable j, k, q, and ? characters as the fuzzy query", async () => {
|
|
142
|
+
for (const query of ["j", "k", "q", "?"]) {
|
|
114
143
|
let selected: string | null | undefined
|
|
115
144
|
const setup = await testRender(
|
|
116
145
|
() => (
|
|
@@ -141,6 +170,163 @@ describe("OpenTUI interaction", () => {
|
|
|
141
170
|
}
|
|
142
171
|
})
|
|
143
172
|
|
|
173
|
+
test("supports the readline movement and deletion contract", async () => {
|
|
174
|
+
const cases = [
|
|
175
|
+
{
|
|
176
|
+
name: "left arrow",
|
|
177
|
+
expected: "aXb",
|
|
178
|
+
edit: async (input: MockInput) => {
|
|
179
|
+
await input.typeText("ab")
|
|
180
|
+
input.pressArrow("left")
|
|
181
|
+
await input.typeText("X")
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "home and end",
|
|
186
|
+
expected: "XabY",
|
|
187
|
+
edit: async (input: MockInput) => {
|
|
188
|
+
await input.typeText("ab")
|
|
189
|
+
input.pressKey(KeyCodes.HOME)
|
|
190
|
+
await input.typeText("X")
|
|
191
|
+
input.pressKey(KeyCodes.END)
|
|
192
|
+
await input.typeText("Y")
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "backspace and delete",
|
|
197
|
+
expected: "a",
|
|
198
|
+
edit: async (input: MockInput) => {
|
|
199
|
+
await input.typeText("abc")
|
|
200
|
+
input.pressBackspace()
|
|
201
|
+
input.pressArrow("left")
|
|
202
|
+
input.pressKey(KeyCodes.DELETE)
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "ctrl-a and ctrl-e",
|
|
207
|
+
expected: "XabY",
|
|
208
|
+
edit: async (input: MockInput) => {
|
|
209
|
+
await input.typeText("ab")
|
|
210
|
+
input.pressKey("a", { ctrl: true })
|
|
211
|
+
await input.typeText("X")
|
|
212
|
+
input.pressKey("e", { ctrl: true })
|
|
213
|
+
await input.typeText("Y")
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "ctrl-b and ctrl-f",
|
|
218
|
+
expected: "aXb",
|
|
219
|
+
edit: async (input: MockInput) => {
|
|
220
|
+
await input.typeText("ab")
|
|
221
|
+
input.pressKey("b", { ctrl: true })
|
|
222
|
+
input.pressKey("b", { ctrl: true })
|
|
223
|
+
input.pressKey("f", { ctrl: true })
|
|
224
|
+
await input.typeText("X")
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "meta-b and meta-f",
|
|
229
|
+
expected: "one Xtwo",
|
|
230
|
+
edit: async (input: MockInput) => {
|
|
231
|
+
await input.typeText("one two")
|
|
232
|
+
input.pressKey("b", { meta: true })
|
|
233
|
+
input.pressKey("b", { meta: true })
|
|
234
|
+
input.pressKey("f", { meta: true })
|
|
235
|
+
await input.typeText("X")
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "ctrl-u",
|
|
240
|
+
expected: "c",
|
|
241
|
+
edit: async (input: MockInput) => {
|
|
242
|
+
await input.typeText("abc")
|
|
243
|
+
input.pressArrow("left")
|
|
244
|
+
input.pressKey("u", { ctrl: true })
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "ctrl-k",
|
|
249
|
+
expected: "ab",
|
|
250
|
+
edit: async (input: MockInput) => {
|
|
251
|
+
await input.typeText("abc")
|
|
252
|
+
input.pressArrow("left")
|
|
253
|
+
input.pressKey("k", { ctrl: true })
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "ctrl-w",
|
|
258
|
+
expected: "one ",
|
|
259
|
+
edit: async (input: MockInput) => {
|
|
260
|
+
await input.typeText("one two")
|
|
261
|
+
input.pressKey("w", { ctrl: true })
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "ctrl-d",
|
|
266
|
+
expected: "ab",
|
|
267
|
+
edit: async (input: MockInput) => {
|
|
268
|
+
await input.typeText("abc")
|
|
269
|
+
input.pressArrow("left")
|
|
270
|
+
input.pressKey("d", { ctrl: true })
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "ctrl-h",
|
|
275
|
+
expected: "ab",
|
|
276
|
+
edit: async (input: MockInput) => {
|
|
277
|
+
await input.typeText("abc")
|
|
278
|
+
input.pressKey("h", { ctrl: true })
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
for (const contract of cases) {
|
|
284
|
+
expect(await submitEditedText(contract.edit), contract.name).toBe(
|
|
285
|
+
contract.expected,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test("yanks the most recently killed text in both prompt inputs", async () => {
|
|
291
|
+
expect(
|
|
292
|
+
await submitEditedText(async (input) => {
|
|
293
|
+
await input.typeText("one two")
|
|
294
|
+
input.pressKey("w", { ctrl: true })
|
|
295
|
+
input.pressKey("y", { ctrl: true })
|
|
296
|
+
}),
|
|
297
|
+
).toBe("one two")
|
|
298
|
+
|
|
299
|
+
let selected: string | null | undefined
|
|
300
|
+
const setup = await testRender(
|
|
301
|
+
() => (
|
|
302
|
+
<InteractiveSelectPrompt
|
|
303
|
+
prompt="Repository"
|
|
304
|
+
choices={[
|
|
305
|
+
{ key: "agency", label: "agency" },
|
|
306
|
+
{ key: "web", label: "web" },
|
|
307
|
+
]}
|
|
308
|
+
onDone={(value) => {
|
|
309
|
+
selected = value
|
|
310
|
+
}}
|
|
311
|
+
/>
|
|
312
|
+
),
|
|
313
|
+
{ width: 60, height: 4 },
|
|
314
|
+
)
|
|
315
|
+
try {
|
|
316
|
+
await setup.renderer.setupTerminal()
|
|
317
|
+
await setup.renderOnce()
|
|
318
|
+
await Bun.sleep(0)
|
|
319
|
+
await setup.mockInput.typeText("web")
|
|
320
|
+
setup.mockInput.pressKey("u", { ctrl: true })
|
|
321
|
+
setup.mockInput.pressKey("y", { ctrl: true })
|
|
322
|
+
setup.mockInput.pressEnter()
|
|
323
|
+
await setup.waitFor(() => selected !== undefined)
|
|
324
|
+
expect(selected).toBe("web")
|
|
325
|
+
} finally {
|
|
326
|
+
setup.renderer.destroy()
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
144
330
|
test("does not select an empty result and supports ctrl-u editing", async () => {
|
|
145
331
|
let selected: string | null | undefined
|
|
146
332
|
const setup = await testRender(
|
|
@@ -232,6 +418,98 @@ describe("OpenTUI interaction", () => {
|
|
|
232
418
|
}
|
|
233
419
|
})
|
|
234
420
|
|
|
421
|
+
test("keeps empty and single-item list boundaries safe", async () => {
|
|
422
|
+
let emptyResult: string | null | undefined
|
|
423
|
+
const empty = await testRender(
|
|
424
|
+
() => (
|
|
425
|
+
<InteractiveSelectPrompt
|
|
426
|
+
prompt="Empty"
|
|
427
|
+
choices={[]}
|
|
428
|
+
onDone={(value) => {
|
|
429
|
+
emptyResult = value
|
|
430
|
+
}}
|
|
431
|
+
/>
|
|
432
|
+
),
|
|
433
|
+
{ width: 60, height: 4 },
|
|
434
|
+
)
|
|
435
|
+
try {
|
|
436
|
+
await empty.renderer.setupTerminal()
|
|
437
|
+
await empty.renderOnce()
|
|
438
|
+
await Bun.sleep(0)
|
|
439
|
+
empty.mockInput.pressArrow("up")
|
|
440
|
+
empty.mockInput.pressArrow("down")
|
|
441
|
+
empty.mockInput.pressKey("p", { ctrl: true })
|
|
442
|
+
empty.mockInput.pressKey("n", { ctrl: true })
|
|
443
|
+
empty.mockInput.pressEnter()
|
|
444
|
+
await Bun.sleep(0)
|
|
445
|
+
expect(emptyResult).toBeUndefined()
|
|
446
|
+
expect(empty.captureCharFrame()).toContain("No matches")
|
|
447
|
+
} finally {
|
|
448
|
+
empty.renderer.destroy()
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (const move of ["up", "down"] as const) {
|
|
452
|
+
let singleResult: string | null | undefined
|
|
453
|
+
const single = await testRender(
|
|
454
|
+
() => (
|
|
455
|
+
<InteractiveSelectPrompt
|
|
456
|
+
prompt="Single"
|
|
457
|
+
choices={[{ key: "one", label: "One" }]}
|
|
458
|
+
onDone={(value) => {
|
|
459
|
+
singleResult = value
|
|
460
|
+
}}
|
|
461
|
+
/>
|
|
462
|
+
),
|
|
463
|
+
{ width: 60, height: 4 },
|
|
464
|
+
)
|
|
465
|
+
try {
|
|
466
|
+
await single.renderer.setupTerminal()
|
|
467
|
+
await single.renderOnce()
|
|
468
|
+
await Bun.sleep(0)
|
|
469
|
+
single.mockInput.pressArrow(move)
|
|
470
|
+
single.mockInput.pressEnter()
|
|
471
|
+
await single.waitFor(() => singleResult !== undefined)
|
|
472
|
+
expect(singleResult).toBe("one")
|
|
473
|
+
} finally {
|
|
474
|
+
single.renderer.destroy()
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test("remains usable after a narrow terminal resize", async () => {
|
|
480
|
+
let selected: string | null | undefined
|
|
481
|
+
const setup = await testRender(
|
|
482
|
+
() => (
|
|
483
|
+
<InteractiveSelectPrompt
|
|
484
|
+
prompt="Repository with a long prompt"
|
|
485
|
+
choices={[
|
|
486
|
+
{ key: "agency", label: "agency" },
|
|
487
|
+
{ key: "docs", label: "docs" },
|
|
488
|
+
]}
|
|
489
|
+
onDone={(value) => {
|
|
490
|
+
selected = value
|
|
491
|
+
}}
|
|
492
|
+
/>
|
|
493
|
+
),
|
|
494
|
+
{ width: 60, height: 4 },
|
|
495
|
+
)
|
|
496
|
+
try {
|
|
497
|
+
await setup.renderer.setupTerminal()
|
|
498
|
+
await setup.renderOnce()
|
|
499
|
+
await Bun.sleep(0)
|
|
500
|
+
setup.resize(18, 4)
|
|
501
|
+
await setup.renderOnce()
|
|
502
|
+
await setup.mockInput.typeText("docs")
|
|
503
|
+
await setup.renderOnce()
|
|
504
|
+
expect(setup.captureCharFrame()).toContain("> docs")
|
|
505
|
+
setup.mockInput.pressEnter()
|
|
506
|
+
await setup.waitFor(() => selected !== undefined)
|
|
507
|
+
expect(selected).toBe("docs")
|
|
508
|
+
} finally {
|
|
509
|
+
setup.renderer.destroy()
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
|
|
235
513
|
test("submits text and cancels with ctrl-c or escape", async () => {
|
|
236
514
|
let submitted: string | null | undefined
|
|
237
515
|
const input = await testRender(
|
|
@@ -34,9 +34,77 @@ interface PromptProps<T> {
|
|
|
34
34
|
const isCancel = (key: { name: string; ctrl: boolean }) =>
|
|
35
35
|
key.name === "escape" || (key.ctrl && key.name === "c")
|
|
36
36
|
|
|
37
|
+
interface PromptKey {
|
|
38
|
+
readonly name: string
|
|
39
|
+
readonly ctrl: boolean
|
|
40
|
+
preventDefault(): void
|
|
41
|
+
stopPropagation(): void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const removedText = (before: string, after: string) => {
|
|
45
|
+
let start = 0
|
|
46
|
+
while (
|
|
47
|
+
start < before.length &&
|
|
48
|
+
start < after.length &&
|
|
49
|
+
before[start] === after[start]
|
|
50
|
+
) {
|
|
51
|
+
start++
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let end = 0
|
|
55
|
+
while (
|
|
56
|
+
before[before.length - end - 1] === after[after.length - end - 1] &&
|
|
57
|
+
end < before.length - start &&
|
|
58
|
+
end < after.length - start
|
|
59
|
+
) {
|
|
60
|
+
end++
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return before.slice(start, before.length - end)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const createReadlineEditing = (
|
|
67
|
+
getInput: () => TextareaRenderable | undefined,
|
|
68
|
+
onInput?: (value: string) => void,
|
|
69
|
+
) => {
|
|
70
|
+
let value = ""
|
|
71
|
+
let killBuffer = ""
|
|
72
|
+
let beforeKill: string | undefined
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
get value() {
|
|
76
|
+
return value
|
|
77
|
+
},
|
|
78
|
+
handleInput(next: string) {
|
|
79
|
+
if (beforeKill !== undefined) {
|
|
80
|
+
const killed = removedText(beforeKill, next)
|
|
81
|
+
if (killed) killBuffer = killed
|
|
82
|
+
beforeKill = undefined
|
|
83
|
+
}
|
|
84
|
+
value = next
|
|
85
|
+
onInput?.(next)
|
|
86
|
+
},
|
|
87
|
+
handleKey(key: PromptKey) {
|
|
88
|
+
const current = getInput()?.plainText
|
|
89
|
+
if (current !== undefined && current !== value) this.handleInput(current)
|
|
90
|
+
if (!key.ctrl) return false
|
|
91
|
+
if (key.name === "y") {
|
|
92
|
+
key.preventDefault()
|
|
93
|
+
key.stopPropagation()
|
|
94
|
+
if (killBuffer) getInput()?.insertText(killBuffer)
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
if (key.name === "u" || key.name === "k" || key.name === "w") {
|
|
98
|
+
beforeKill = value
|
|
99
|
+
}
|
|
100
|
+
return false
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
37
105
|
export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
38
106
|
let input: TextareaRenderable | undefined
|
|
39
|
-
|
|
107
|
+
const editing = createReadlineEditing(() => input)
|
|
40
108
|
useKeyboard((key) => {
|
|
41
109
|
if (isCancel(key)) {
|
|
42
110
|
key.preventDefault()
|
|
@@ -44,10 +112,11 @@ export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
|
44
112
|
props.onDone(null)
|
|
45
113
|
return
|
|
46
114
|
}
|
|
115
|
+
if (editing.handleKey(key)) return
|
|
47
116
|
if (key.name !== "return") return
|
|
48
117
|
key.preventDefault()
|
|
49
118
|
key.stopPropagation()
|
|
50
|
-
props.onDone(value)
|
|
119
|
+
props.onDone(editing.value)
|
|
51
120
|
})
|
|
52
121
|
|
|
53
122
|
return (
|
|
@@ -59,7 +128,7 @@ export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
|
59
128
|
wrapMode="word"
|
|
60
129
|
keyBindings={[{ name: "return", action: "submit" }]}
|
|
61
130
|
onContentChange={() => {
|
|
62
|
-
|
|
131
|
+
editing.handleInput(input?.plainText ?? "")
|
|
63
132
|
}}
|
|
64
133
|
ref={(next) => {
|
|
65
134
|
input = next
|
|
@@ -68,7 +137,9 @@ export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
|
68
137
|
})
|
|
69
138
|
}}
|
|
70
139
|
/>
|
|
71
|
-
<text fg="#6c7086"
|
|
140
|
+
<text fg="#6c7086" wrapMode="none">
|
|
141
|
+
enter submit | esc cancel | ctrl-y yank
|
|
142
|
+
</text>
|
|
72
143
|
</box>
|
|
73
144
|
)
|
|
74
145
|
}
|
|
@@ -145,6 +216,7 @@ export const fuzzyChoices = (
|
|
|
145
216
|
export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
146
217
|
let input: TextareaRenderable | undefined
|
|
147
218
|
const [query, setQuery] = createSignal("")
|
|
219
|
+
const editing = createReadlineEditing(() => input, setQuery)
|
|
148
220
|
const [selected, setSelected] = createSignal(0)
|
|
149
221
|
const choices = createMemo(() => fuzzyChoices(props.choices, query()))
|
|
150
222
|
const move = (offset: -1 | 1) => {
|
|
@@ -171,6 +243,7 @@ export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
|
171
243
|
move(1)
|
|
172
244
|
return
|
|
173
245
|
}
|
|
246
|
+
if (editing.handleKey(key)) return
|
|
174
247
|
if (key.name !== "return") return
|
|
175
248
|
key.preventDefault()
|
|
176
249
|
key.stopPropagation()
|
|
@@ -207,7 +280,7 @@ export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
|
207
280
|
placeholder="filter"
|
|
208
281
|
keyBindings={[{ name: "return", action: "submit" }]}
|
|
209
282
|
onContentChange={() => {
|
|
210
|
-
|
|
283
|
+
editing.handleInput(input?.plainText ?? "")
|
|
211
284
|
setSelected(0)
|
|
212
285
|
}}
|
|
213
286
|
ref={(next) => {
|
|
@@ -232,7 +305,7 @@ export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
|
232
305
|
</For>
|
|
233
306
|
</box>
|
|
234
307
|
<text fg="#6c7086" wrapMode="none">
|
|
235
|
-
enter select | esc cancel | ctrl-n/p
|
|
308
|
+
enter select | esc cancel | arrows/ctrl-n/p | ctrl-y yank
|
|
236
309
|
</text>
|
|
237
310
|
</box>
|
|
238
311
|
)
|