@levu/snap 0.2.0 → 0.3.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/README.md +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/tui/component-adapters/multiline-text.d.ts +13 -0
- package/dist/tui/component-adapters/multiline-text.js +166 -0
- package/dist/tui/component-adapters/spinner.d.ts +11 -0
- package/dist/tui/component-adapters/spinner.js +42 -1
- package/dist/tui/component-adapters/text.d.ts +4 -0
- package/dist/tui/component-adapters/text.js +18 -0
- package/docs/component-reference.md +84 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ For module/tool authors, Snap also exposes optional DX helper groups:
|
|
|
19
19
|
- Enforces action triad at registration: `tui + commandline + help`
|
|
20
20
|
- Uses one runtime engine for TUI and CLI paths
|
|
21
21
|
- Uses Clack-powered prompt adapters for interactive TUI (`select`, `text`, `confirm`, `multiselect`)
|
|
22
|
+
- Text prompts support clipboard paste and multiline input for pasting multiple lines
|
|
22
23
|
- Supports workflow transitions: `next`, `back`, `jump`, `exit`
|
|
23
24
|
- Supports resume checkpoints for interrupted flows
|
|
24
25
|
- Produces stable help output hierarchy:
|
package/dist/index.d.ts
CHANGED
|
@@ -16,4 +16,6 @@ export { createSpinner, spinner } from './tui/component-adapters/spinner.js';
|
|
|
16
16
|
export type { Spinner, SpinnerOptions } from './tui/component-adapters/spinner.js';
|
|
17
17
|
export { runPasswordPrompt } from './tui/component-adapters/password.js';
|
|
18
18
|
export type { PasswordPromptInput } from './tui/component-adapters/password.js';
|
|
19
|
+
export { createMultilineTextPrompt } from './tui/component-adapters/multiline-text.js';
|
|
20
|
+
export type { MultilineTextOptions } from './tui/component-adapters/multiline-text.js';
|
|
19
21
|
export declare const createRegistry: (modules: ModuleContract[]) => ActionRegistry;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { createPromptToolkit } from './tui/prompt-toolkit.js';
|
|
|
10
10
|
export { runCustomPrompt, createCustomPromptRunner } from './tui/custom/index.js';
|
|
11
11
|
export { createSpinner, spinner } from './tui/component-adapters/spinner.js';
|
|
12
12
|
export { runPasswordPrompt } from './tui/component-adapters/password.js';
|
|
13
|
+
export { createMultilineTextPrompt } from './tui/component-adapters/multiline-text.js';
|
|
13
14
|
export const createRegistry = (modules) => {
|
|
14
15
|
const registry = new ActionRegistry();
|
|
15
16
|
for (const moduleContract of modules) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
export interface MultilineTextOptions {
|
|
4
|
+
message: string;
|
|
5
|
+
initialValue?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
validate?: (value: string | undefined) => string | Error | undefined;
|
|
8
|
+
allowPaste?: boolean;
|
|
9
|
+
input?: Readable;
|
|
10
|
+
output?: Writable;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
export declare const createMultilineTextPrompt: () => (opts: MultilineTextOptions) => Promise<string | symbol>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import * as pc from 'picocolors';
|
|
3
|
+
export const createMultilineTextPrompt = () => {
|
|
4
|
+
return async (opts) => {
|
|
5
|
+
const { message, initialValue = '', placeholder = '', validate, allowPaste = false, input = process.stdin, output = process.stdout, signal, } = opts;
|
|
6
|
+
// Use standard text prompt for single line paste
|
|
7
|
+
if (!allowPaste) {
|
|
8
|
+
const { text: textPrompt } = await import('@clack/prompts');
|
|
9
|
+
return textPrompt({
|
|
10
|
+
message,
|
|
11
|
+
initialValue,
|
|
12
|
+
placeholder,
|
|
13
|
+
validate,
|
|
14
|
+
input,
|
|
15
|
+
output,
|
|
16
|
+
signal,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// For multiline paste support, use a custom readline-based approach
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const rl = createInterface({
|
|
22
|
+
input,
|
|
23
|
+
output,
|
|
24
|
+
terminal: true,
|
|
25
|
+
});
|
|
26
|
+
let value = initialValue;
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
rl.close();
|
|
30
|
+
};
|
|
31
|
+
const submit = (val) => {
|
|
32
|
+
cleanup();
|
|
33
|
+
resolve(val);
|
|
34
|
+
};
|
|
35
|
+
const doCancel = () => {
|
|
36
|
+
cancelled = true;
|
|
37
|
+
cleanup();
|
|
38
|
+
const { isCancel: cancelSymbol } = require('@clack/prompts');
|
|
39
|
+
resolve(cancelSymbol);
|
|
40
|
+
};
|
|
41
|
+
// Show instructions
|
|
42
|
+
output.write(`\n${pc.cyan('○')} ${pc.bold(message)}\n`);
|
|
43
|
+
if (allowPaste) {
|
|
44
|
+
output.write(pc.dim(` Paste support: Ctrl+V to paste (macOS/Linux: Cmd+Shift+V)\n`));
|
|
45
|
+
}
|
|
46
|
+
output.write(pc.dim(` Press Enter twice or Alt+Enter to submit\n`));
|
|
47
|
+
const lines = value.split('\n');
|
|
48
|
+
let currentLine = lines.length > 0 ? lines.pop() : '';
|
|
49
|
+
const showPrompt = () => {
|
|
50
|
+
output.write(`\n${pc.dim('> ')}${currentLine}`);
|
|
51
|
+
};
|
|
52
|
+
showPrompt();
|
|
53
|
+
// Handle paste from clipboard
|
|
54
|
+
const handlePaste = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const { execSync } = await import('node:child_process');
|
|
57
|
+
const platform = process.platform;
|
|
58
|
+
if (platform === 'darwin') {
|
|
59
|
+
return execSync('pbpaste', { encoding: 'utf-8' });
|
|
60
|
+
}
|
|
61
|
+
else if (platform === 'win32') {
|
|
62
|
+
return execSync('powershell -command "Get-Clipboard"', { encoding: 'utf-8', shell: true }).trim();
|
|
63
|
+
}
|
|
64
|
+
else if (platform === 'linux') {
|
|
65
|
+
try {
|
|
66
|
+
return execSync('xclip -selection clipboard -o', {
|
|
67
|
+
encoding: 'utf-8',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
try {
|
|
72
|
+
return execSync('xsel --clipboard --output', {
|
|
73
|
+
encoding: 'utf-8',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Silent fail if clipboard is unavailable
|
|
84
|
+
}
|
|
85
|
+
return '';
|
|
86
|
+
};
|
|
87
|
+
let lastEnterTime = 0;
|
|
88
|
+
const DOUBLE_ENTER_TIMEOUT = 500; // ms
|
|
89
|
+
rl.on('line', (line) => {
|
|
90
|
+
if (cancelled)
|
|
91
|
+
return;
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
// Check for double Enter to submit
|
|
94
|
+
if (line === '' && now - lastEnterTime < DOUBLE_ENTER_TIMEOUT) {
|
|
95
|
+
submit(lines.join('\n') + currentLine);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
lastEnterTime = now;
|
|
99
|
+
if (line.trim() === '') {
|
|
100
|
+
// Empty line - add to lines
|
|
101
|
+
if (currentLine !== '') {
|
|
102
|
+
lines.push(currentLine);
|
|
103
|
+
currentLine = '';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Non-empty line
|
|
108
|
+
if (currentLine !== '') {
|
|
109
|
+
lines.push(currentLine);
|
|
110
|
+
}
|
|
111
|
+
currentLine = line;
|
|
112
|
+
}
|
|
113
|
+
showPrompt();
|
|
114
|
+
});
|
|
115
|
+
// Handle SIGINT (Ctrl+C)
|
|
116
|
+
rl.on('SIGINT', () => {
|
|
117
|
+
doCancel();
|
|
118
|
+
});
|
|
119
|
+
// Handle signal
|
|
120
|
+
if (signal) {
|
|
121
|
+
signal.addEventListener('abort', () => {
|
|
122
|
+
doCancel();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Handle paste via keyboard shortcut
|
|
126
|
+
if (allowPaste && input.setRawMode) {
|
|
127
|
+
input.setRawMode(true);
|
|
128
|
+
input.resume();
|
|
129
|
+
input.on('keypress', async (str, key) => {
|
|
130
|
+
if (cancelled)
|
|
131
|
+
return;
|
|
132
|
+
// Detect Ctrl+V or Cmd+V for paste
|
|
133
|
+
if ((key.ctrl && key.name === 'v') || (key.meta && key.name === 'v')) {
|
|
134
|
+
const pasted = await handlePaste();
|
|
135
|
+
if (pasted) {
|
|
136
|
+
// Clear current line and show pasted content
|
|
137
|
+
output.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
138
|
+
const pastedLines = pasted.split('\n');
|
|
139
|
+
if (pastedLines.length > 1) {
|
|
140
|
+
// Multiline paste
|
|
141
|
+
lines.push(...pastedLines.slice(0, -1));
|
|
142
|
+
currentLine = pastedLines[pastedLines.length - 1];
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Single line paste
|
|
146
|
+
currentLine += pasted;
|
|
147
|
+
}
|
|
148
|
+
output.write(`${pc.dim('> ')}${currentLine}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (key.alt && key.name === 'enter') {
|
|
152
|
+
// Alt+Enter to submit
|
|
153
|
+
submit(lines.join('\n') + currentLine);
|
|
154
|
+
}
|
|
155
|
+
else if (key.name === 'escape') {
|
|
156
|
+
doCancel();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// Handle non-interactive terminal
|
|
161
|
+
if (!input.isTTY) {
|
|
162
|
+
submit(initialValue);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
};
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export interface SpinnerOptions {
|
|
2
2
|
message?: string;
|
|
3
|
+
indicator?: 'dots' | 'timer';
|
|
4
|
+
onCancel?: () => void;
|
|
5
|
+
cancelMessage?: string;
|
|
6
|
+
errorMessage?: string;
|
|
7
|
+
frames?: string[];
|
|
8
|
+
delay?: number;
|
|
9
|
+
styleFrame?: (frame: string) => string;
|
|
3
10
|
}
|
|
4
11
|
export interface Spinner {
|
|
5
12
|
start(message?: string): void;
|
|
6
13
|
stop(message?: string): void;
|
|
14
|
+
cancel(message?: string): void;
|
|
15
|
+
error(message?: string): void;
|
|
7
16
|
message(message: string): void;
|
|
17
|
+
clear(): void;
|
|
18
|
+
readonly isCancelled: boolean;
|
|
8
19
|
}
|
|
9
20
|
export declare const createSpinner: (options?: SpinnerOptions) => Spinner;
|
|
10
21
|
export declare const spinner: (options?: SpinnerOptions) => Spinner;
|
|
@@ -4,6 +4,7 @@ export const createSpinner = (options = {}) => {
|
|
|
4
4
|
// Non-interactive fallback
|
|
5
5
|
if (!isInteractiveTerminal()) {
|
|
6
6
|
let currentMessage = options.message ?? '';
|
|
7
|
+
let cancelled = false;
|
|
7
8
|
return {
|
|
8
9
|
start(message) {
|
|
9
10
|
currentMessage = message ?? currentMessage;
|
|
@@ -16,13 +17,28 @@ export const createSpinner = (options = {}) => {
|
|
|
16
17
|
process.stdout.write(`${message}\n`);
|
|
17
18
|
}
|
|
18
19
|
},
|
|
20
|
+
cancel(message) {
|
|
21
|
+
cancelled = true;
|
|
22
|
+
const msg = message || options.cancelMessage || 'Cancelled';
|
|
23
|
+
process.stdout.write(`${msg}\n`);
|
|
24
|
+
},
|
|
25
|
+
error(message) {
|
|
26
|
+
const msg = message || options.errorMessage || 'Error';
|
|
27
|
+
process.stderr.write(`${msg}\n`);
|
|
28
|
+
},
|
|
19
29
|
message(newMessage) {
|
|
20
30
|
currentMessage = newMessage;
|
|
31
|
+
},
|
|
32
|
+
clear() {
|
|
33
|
+
// No-op in non-interactive mode
|
|
34
|
+
},
|
|
35
|
+
get isCancelled() {
|
|
36
|
+
return cancelled;
|
|
21
37
|
}
|
|
22
38
|
};
|
|
23
39
|
}
|
|
24
40
|
// Interactive spinner using @clack/prompts
|
|
25
|
-
const internalSpinner = clackSpinner();
|
|
41
|
+
const internalSpinner = clackSpinner(options);
|
|
26
42
|
return {
|
|
27
43
|
start(message) {
|
|
28
44
|
if (message) {
|
|
@@ -31,6 +47,9 @@ export const createSpinner = (options = {}) => {
|
|
|
31
47
|
else if (options.message) {
|
|
32
48
|
internalSpinner.start(options.message);
|
|
33
49
|
}
|
|
50
|
+
else {
|
|
51
|
+
internalSpinner.start();
|
|
52
|
+
}
|
|
34
53
|
},
|
|
35
54
|
stop(message) {
|
|
36
55
|
if (message) {
|
|
@@ -40,8 +59,30 @@ export const createSpinner = (options = {}) => {
|
|
|
40
59
|
internalSpinner.stop();
|
|
41
60
|
}
|
|
42
61
|
},
|
|
62
|
+
cancel(message) {
|
|
63
|
+
if (message) {
|
|
64
|
+
internalSpinner.cancel(message);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
internalSpinner.cancel();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
error(message) {
|
|
71
|
+
if (message) {
|
|
72
|
+
internalSpinner.error(message);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
internalSpinner.error();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
43
78
|
message(newMessage) {
|
|
44
79
|
internalSpinner.message(newMessage);
|
|
80
|
+
},
|
|
81
|
+
clear() {
|
|
82
|
+
internalSpinner.clear();
|
|
83
|
+
},
|
|
84
|
+
get isCancelled() {
|
|
85
|
+
return internalSpinner.isCancelled;
|
|
45
86
|
}
|
|
46
87
|
};
|
|
47
88
|
};
|
|
@@ -4,5 +4,9 @@ export interface TextPromptInput {
|
|
|
4
4
|
required?: boolean;
|
|
5
5
|
placeholder?: string;
|
|
6
6
|
validate?: (value: string) => string | Error | undefined;
|
|
7
|
+
/** Enable paste support for text input. When true, allows pasting single or multiple lines of text. */
|
|
8
|
+
paste?: boolean;
|
|
9
|
+
/** When paste is enabled, allow multiple lines of input. Defaults to true when paste is enabled. */
|
|
10
|
+
multiline?: boolean;
|
|
7
11
|
}
|
|
8
12
|
export declare const runTextPrompt: (input: TextPromptInput) => Promise<string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { text } from '@clack/prompts';
|
|
2
2
|
import { isInteractiveTerminal } from './readline-utils.js';
|
|
3
3
|
import { unwrapClackResult } from './cancel.js';
|
|
4
|
+
import { createMultilineTextPrompt } from './multiline-text.js';
|
|
4
5
|
export const runTextPrompt = async (input) => {
|
|
5
6
|
const fallbackValue = input.initialValue ?? '';
|
|
6
7
|
if (!isInteractiveTerminal()) {
|
|
@@ -9,6 +10,23 @@ export const runTextPrompt = async (input) => {
|
|
|
9
10
|
}
|
|
10
11
|
return fallbackValue;
|
|
11
12
|
}
|
|
13
|
+
// Use multiline prompt when paste is enabled or multiline is explicitly requested
|
|
14
|
+
if (input.paste || input.multiline) {
|
|
15
|
+
const multilinePrompt = createMultilineTextPrompt();
|
|
16
|
+
const value = await multilinePrompt({
|
|
17
|
+
message: input.message,
|
|
18
|
+
initialValue: input.initialValue,
|
|
19
|
+
placeholder: input.placeholder,
|
|
20
|
+
validate: (raw) => {
|
|
21
|
+
if (input.required && (!raw || raw.trim().length === 0)) {
|
|
22
|
+
return `Required text value missing: ${input.message}`;
|
|
23
|
+
}
|
|
24
|
+
return input.validate?.(raw ?? '');
|
|
25
|
+
},
|
|
26
|
+
allowPaste: input.paste ?? false
|
|
27
|
+
});
|
|
28
|
+
return unwrapClackResult(value);
|
|
29
|
+
}
|
|
12
30
|
const value = await text({
|
|
13
31
|
message: input.message,
|
|
14
32
|
initialValue: input.initialValue,
|
|
@@ -18,7 +18,7 @@ interface PromptToolkit {
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
### text
|
|
21
|
-
Single-line text input with validation.
|
|
21
|
+
Single-line text input with validation and optional paste/multiline support.
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
24
|
const name = await context.prompts.text({
|
|
@@ -32,6 +32,34 @@ const name = await context.prompts.text({
|
|
|
32
32
|
});
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
**With Paste Support:**
|
|
36
|
+
```typescript
|
|
37
|
+
const description = await context.prompts.text({
|
|
38
|
+
message: 'Enter a description:',
|
|
39
|
+
paste: true, // Enable clipboard paste (Ctrl+V / Cmd+V)
|
|
40
|
+
multiline: true, // Allow multiple lines of input
|
|
41
|
+
placeholder: 'Paste or type your description here...'
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Options:**
|
|
46
|
+
- `message` (required): The prompt message
|
|
47
|
+
- `placeholder`: Placeholder text when input is empty
|
|
48
|
+
- `defaultValue` / `initialValue`: Initial value
|
|
49
|
+
- `required`: Whether input is required (default: false)
|
|
50
|
+
- `validate`: Custom validation function
|
|
51
|
+
- `paste`: Enable clipboard paste support (default: false)
|
|
52
|
+
- `multiline`: Allow multiple lines of input (default: false, implied by paste: true)
|
|
53
|
+
|
|
54
|
+
**Paste Support Details:**
|
|
55
|
+
When `paste` is enabled, users can paste from clipboard using:
|
|
56
|
+
- macOS/Linux: `Cmd+V` or `Ctrl+V`
|
|
57
|
+
- Windows: `Ctrl+V`
|
|
58
|
+
|
|
59
|
+
Multi-line paste is automatically supported. Submit with:
|
|
60
|
+
- `Alt+Enter` or `Cmd+Enter`
|
|
61
|
+
- Double `Enter` (press Enter twice quickly)
|
|
62
|
+
|
|
35
63
|
### confirm
|
|
36
64
|
Yes/no confirmation prompt.
|
|
37
65
|
|
|
@@ -125,12 +153,66 @@ spinner.message('Still working...');
|
|
|
125
153
|
spinner.stop('Complete!');
|
|
126
154
|
```
|
|
127
155
|
|
|
156
|
+
**⚠️ Important: Avoid Rapid Message Updates**
|
|
157
|
+
|
|
158
|
+
Updating the spinner message too frequently (multiple times per second) can cause UI tearing and rendering issues. The spinner uses ANSI escape codes to update in-place, and rapid updates can overwhelm the terminal.
|
|
159
|
+
|
|
160
|
+
**❌ AVOID - This causes UI tearing:**
|
|
161
|
+
```typescript
|
|
162
|
+
// DON'T: Update in a tight loop
|
|
163
|
+
for (let i = 0; i < 1000; i++) {
|
|
164
|
+
spinner.message(`Processing item ${i}`); // Too fast!
|
|
165
|
+
await processItem(i);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**✅ RECOMMENDED - Batch updates or throttle:**
|
|
170
|
+
```typescript
|
|
171
|
+
// DO: Update at reasonable intervals
|
|
172
|
+
for (let i = 0; i < 1000; i++) {
|
|
173
|
+
// Only update every 100 items
|
|
174
|
+
if (i % 100 === 0) {
|
|
175
|
+
spinner.message(`Processing item ${i}`);
|
|
176
|
+
}
|
|
177
|
+
await processItem(i);
|
|
178
|
+
}
|
|
179
|
+
spinner.stop(`Processed ${1000} items`);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**✅ Alternative - Use percentage for progress:**
|
|
183
|
+
```typescript
|
|
184
|
+
const total = 1000;
|
|
185
|
+
for (let i = 0; i < total; i++) {
|
|
186
|
+
// Update every 5% progress
|
|
187
|
+
if (i % Math.floor(total * 0.05) === 0) {
|
|
188
|
+
const percent = Math.floor((i / total) * 100);
|
|
189
|
+
spinner.message(`Processing... ${percent}%`);
|
|
190
|
+
}
|
|
191
|
+
await processItem(i);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
128
195
|
**Interface:**
|
|
129
196
|
```typescript
|
|
130
197
|
interface Spinner {
|
|
131
198
|
start(message?: string): void;
|
|
132
199
|
stop(message?: string): void;
|
|
200
|
+
cancel(message?: string): void;
|
|
201
|
+
error(message?: string): void;
|
|
133
202
|
message(message: string): void;
|
|
203
|
+
clear(): void;
|
|
204
|
+
readonly isCancelled: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface SpinnerOptions {
|
|
208
|
+
message?: string;
|
|
209
|
+
indicator?: 'dots' | 'timer';
|
|
210
|
+
onCancel?: () => void;
|
|
211
|
+
cancelMessage?: string;
|
|
212
|
+
errorMessage?: string;
|
|
213
|
+
frames?: string[];
|
|
214
|
+
delay?: number;
|
|
215
|
+
styleFrame?: (frame: string) => string;
|
|
134
216
|
}
|
|
135
217
|
```
|
|
136
218
|
|
|
@@ -328,6 +410,7 @@ context.terminal.error('Error message');
|
|
|
328
410
|
| Component | Import From | Also Available Via |
|
|
329
411
|
|-----------|-------------|-------------------|
|
|
330
412
|
| text, confirm, select, multiselect, group, custom | `createPromptToolkit()` | `context.prompts` |
|
|
413
|
+
| text with paste/multiline support | `'snap-framework'` | `context.prompts.text({ paste: true, multiline: true })` |
|
|
331
414
|
| createSpinner, spinner | `'snap-framework'` | `SnapTui.createSpinner` |
|
|
332
415
|
| runPasswordPrompt | `'snap-framework'` | Direct import only |
|
|
333
416
|
| createProgress, progress | - | `SnapTui.createProgress` |
|