@levu/snap 0.3.0 → 0.3.2
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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.2] - 2026-02-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Added Ghostty macOS fallback handling for `Shift+Enter` escape sequence variants like `13~`, so multiline prompts insert newline instead of echoing sequence text.
|
|
12
|
+
- Added regression coverage for the `13~` modified-enter sequence path.
|
|
13
|
+
|
|
14
|
+
## [0.3.1] - 2026-02-26
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Multiline text prompt now submits on `Enter` in raw input mode, restoring submit behavior in macOS terminals like JetBrains and Ghostty.
|
|
18
|
+
- Added `Shift+Enter` (and `Alt+Enter` fallback) support to insert newline in multiline prompt mode.
|
|
19
|
+
- Restored terminal state reliably by disabling raw mode and detaching keypress listeners on prompt cleanup.
|
|
20
|
+
- Added regression tests covering `Enter` submit and `Shift+Enter` newline behavior for multiline prompts.
|
|
21
|
+
|
|
8
22
|
## [0.2.0] - 2025-02-24
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
1
|
+
import { createInterface, emitKeypressEvents } from 'node:readline';
|
|
2
2
|
import * as pc from 'picocolors';
|
|
3
3
|
export const createMultilineTextPrompt = () => {
|
|
4
4
|
return async (opts) => {
|
|
@@ -25,7 +25,18 @@ export const createMultilineTextPrompt = () => {
|
|
|
25
25
|
});
|
|
26
26
|
let value = initialValue;
|
|
27
27
|
let cancelled = false;
|
|
28
|
+
let rawModeEnabled = false;
|
|
29
|
+
let keypressListener;
|
|
30
|
+
let ignoreNextLineEvent = false;
|
|
31
|
+
let expectingGhosttySequenceEcho = false;
|
|
28
32
|
const cleanup = () => {
|
|
33
|
+
if (keypressListener) {
|
|
34
|
+
input.off('keypress', keypressListener);
|
|
35
|
+
}
|
|
36
|
+
if (rawModeEnabled && input.setRawMode) {
|
|
37
|
+
input.setRawMode(false);
|
|
38
|
+
rawModeEnabled = false;
|
|
39
|
+
}
|
|
29
40
|
rl.close();
|
|
30
41
|
};
|
|
31
42
|
const submit = (val) => {
|
|
@@ -43,9 +54,39 @@ export const createMultilineTextPrompt = () => {
|
|
|
43
54
|
if (allowPaste) {
|
|
44
55
|
output.write(pc.dim(` Paste support: Ctrl+V to paste (macOS/Linux: Cmd+Shift+V)\n`));
|
|
45
56
|
}
|
|
46
|
-
output.write(pc.dim(` Press Enter
|
|
57
|
+
output.write(pc.dim(` Press Enter to submit; Shift+Enter for newline (Alt+Enter fallback)\n`));
|
|
47
58
|
const lines = value.split('\n');
|
|
48
59
|
let currentLine = lines.length > 0 ? lines.pop() : '';
|
|
60
|
+
const getLiveLine = () => {
|
|
61
|
+
const rlLine = typeof rl.line === 'string' ? rl.line : '';
|
|
62
|
+
if (rlLine.length > 0)
|
|
63
|
+
return rlLine;
|
|
64
|
+
return currentLine;
|
|
65
|
+
};
|
|
66
|
+
const isGhosttyShiftEnter = (str, key) => {
|
|
67
|
+
const sequence = String(key?.sequence ?? '');
|
|
68
|
+
if (sequence.includes('[13;2u'))
|
|
69
|
+
return true;
|
|
70
|
+
if (sequence.includes('[27;2;13~'))
|
|
71
|
+
return true;
|
|
72
|
+
if (sequence.endsWith('13~'))
|
|
73
|
+
return true;
|
|
74
|
+
if (sequence === '13~')
|
|
75
|
+
return true;
|
|
76
|
+
if (str === '~13')
|
|
77
|
+
return true;
|
|
78
|
+
if (str === '13~')
|
|
79
|
+
return true;
|
|
80
|
+
if (str === '\u001b[13;2u')
|
|
81
|
+
return true;
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
const insertNewline = () => {
|
|
85
|
+
lines.push(getLiveLine());
|
|
86
|
+
currentLine = '';
|
|
87
|
+
rl.line = '';
|
|
88
|
+
output.write(`\n${pc.dim('> ')}`);
|
|
89
|
+
};
|
|
49
90
|
const showPrompt = () => {
|
|
50
91
|
output.write(`\n${pc.dim('> ')}${currentLine}`);
|
|
51
92
|
};
|
|
@@ -89,10 +130,21 @@ export const createMultilineTextPrompt = () => {
|
|
|
89
130
|
rl.on('line', (line) => {
|
|
90
131
|
if (cancelled)
|
|
91
132
|
return;
|
|
133
|
+
if (ignoreNextLineEvent) {
|
|
134
|
+
ignoreNextLineEvent = false;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (expectingGhosttySequenceEcho) {
|
|
138
|
+
if (line === '13~' || line === '~13' || line === '[13;2u' || line === '\u001b[13;2u') {
|
|
139
|
+
expectingGhosttySequenceEcho = false;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
expectingGhosttySequenceEcho = false;
|
|
143
|
+
}
|
|
92
144
|
const now = Date.now();
|
|
93
145
|
// Check for double Enter to submit
|
|
94
146
|
if (line === '' && now - lastEnterTime < DOUBLE_ENTER_TIMEOUT) {
|
|
95
|
-
submit(lines.join('\n')
|
|
147
|
+
submit(lines.concat(getLiveLine()).join('\n'));
|
|
96
148
|
return;
|
|
97
149
|
}
|
|
98
150
|
lastEnterTime = now;
|
|
@@ -124,9 +176,11 @@ export const createMultilineTextPrompt = () => {
|
|
|
124
176
|
}
|
|
125
177
|
// Handle paste via keyboard shortcut
|
|
126
178
|
if (allowPaste && input.setRawMode) {
|
|
179
|
+
emitKeypressEvents(input);
|
|
127
180
|
input.setRawMode(true);
|
|
181
|
+
rawModeEnabled = true;
|
|
128
182
|
input.resume();
|
|
129
|
-
|
|
183
|
+
keypressListener = async (str, key) => {
|
|
130
184
|
if (cancelled)
|
|
131
185
|
return;
|
|
132
186
|
// Detect Ctrl+V or Cmd+V for paste
|
|
@@ -145,17 +199,28 @@ export const createMultilineTextPrompt = () => {
|
|
|
145
199
|
// Single line paste
|
|
146
200
|
currentLine += pasted;
|
|
147
201
|
}
|
|
202
|
+
rl.line = currentLine;
|
|
148
203
|
output.write(`${pc.dim('> ')}${currentLine}`);
|
|
149
204
|
}
|
|
150
205
|
}
|
|
151
|
-
else if (
|
|
152
|
-
|
|
153
|
-
|
|
206
|
+
else if (isGhosttyShiftEnter(str, key)) {
|
|
207
|
+
expectingGhosttySequenceEcho = true;
|
|
208
|
+
insertNewline();
|
|
209
|
+
}
|
|
210
|
+
else if (key.name === 'enter' || key.name === 'return') {
|
|
211
|
+
ignoreNextLineEvent = true;
|
|
212
|
+
if (key.shift || key.alt) {
|
|
213
|
+
// Shift+Enter / Alt+Enter inserts a new line.
|
|
214
|
+
insertNewline();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
submit(lines.concat(getLiveLine()).join('\n'));
|
|
154
218
|
}
|
|
155
219
|
else if (key.name === 'escape') {
|
|
156
220
|
doCancel();
|
|
157
221
|
}
|
|
158
|
-
}
|
|
222
|
+
};
|
|
223
|
+
input.on('keypress', keypressListener);
|
|
159
224
|
}
|
|
160
225
|
// Handle non-interactive terminal
|
|
161
226
|
if (!input.isTTY) {
|
|
@@ -56,9 +56,10 @@ When `paste` is enabled, users can paste from clipboard using:
|
|
|
56
56
|
- macOS/Linux: `Cmd+V` or `Ctrl+V`
|
|
57
57
|
- Windows: `Ctrl+V`
|
|
58
58
|
|
|
59
|
-
Multi-line paste is automatically supported.
|
|
60
|
-
- `
|
|
61
|
-
-
|
|
59
|
+
Multi-line paste is automatically supported. Keyboard behavior:
|
|
60
|
+
- `Enter` submits input
|
|
61
|
+
- `Shift+Enter` inserts a newline (including Ghostty sequence fallback variants)
|
|
62
|
+
- `Alt+Enter` inserts a newline fallback
|
|
62
63
|
|
|
63
64
|
### confirm
|
|
64
65
|
Yes/no confirmation prompt.
|