@levu/snap 0.3.4 → 0.3.6
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.6] - 2026-02-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Prevented accidental submit during terminal-native multi-line paste by detecting bracketed paste mode (`ESC[200~ ... ESC[201~`) and suppressing Enter submit while paste is in progress.
|
|
12
|
+
- Improved multiline prompt handling in Ghostty/macOS when pasted newlines are delivered via terminal stream instead of explicit paste shortcuts.
|
|
13
|
+
- Added regression coverage for bracketed multi-line paste to ensure pasted content is preserved until the user explicitly submits.
|
|
14
|
+
|
|
15
|
+
## [0.3.5] - 2026-02-26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Added an additional Ghostty/macOS fallback for `Shift+Enter` when emitted as plain `1`, `3`, `~` keypress chars (no modifier metadata).
|
|
19
|
+
- Added submit-time sanitation of residual Ghostty shift-enter tokens (`13~`, related escape variants) so pasted/echoed artifacts become real newlines in multiline prompts.
|
|
20
|
+
- Added regression coverage for plain-char `13~` keypress and residual token sanitation.
|
|
21
|
+
|
|
8
22
|
## [0.3.4] - 2026-02-26
|
|
9
23
|
|
|
10
24
|
### Fixed
|
|
@@ -28,12 +28,18 @@ export const createMultilineTextPrompt = () => {
|
|
|
28
28
|
let cancelled = false;
|
|
29
29
|
let rawModeEnabled = false;
|
|
30
30
|
let keypressListener;
|
|
31
|
+
let dataListener;
|
|
31
32
|
let ignoreNextLineEvent = false;
|
|
32
33
|
let expectingGhosttySequenceEcho = false;
|
|
34
|
+
let bracketPasteActive = false;
|
|
35
|
+
let bracketPasteProbe = '';
|
|
33
36
|
const cleanup = () => {
|
|
34
37
|
if (keypressListener) {
|
|
35
38
|
input.off('keypress', keypressListener);
|
|
36
39
|
}
|
|
40
|
+
if (dataListener) {
|
|
41
|
+
input.off?.('data', dataListener);
|
|
42
|
+
}
|
|
37
43
|
if (rawModeEnabled && input.setRawMode) {
|
|
38
44
|
input.setRawMode(false);
|
|
39
45
|
rawModeEnabled = false;
|
|
@@ -98,6 +104,12 @@ export const createMultilineTextPrompt = () => {
|
|
|
98
104
|
}
|
|
99
105
|
return null;
|
|
100
106
|
};
|
|
107
|
+
const normalizeGhosttyInlineTokens = (raw) => {
|
|
108
|
+
return raw.replace(/(?:\u001b\[13;2u|\[13;2u|\u001b\[27;2;13~|\[27;2;13~|13~|~13)/g, '\n');
|
|
109
|
+
};
|
|
110
|
+
const buildSubmitValue = () => {
|
|
111
|
+
return normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
|
|
112
|
+
};
|
|
101
113
|
const insertNewline = () => {
|
|
102
114
|
lines.push(getLiveLine());
|
|
103
115
|
currentLine = '';
|
|
@@ -107,6 +119,28 @@ export const createMultilineTextPrompt = () => {
|
|
|
107
119
|
const showPrompt = () => {
|
|
108
120
|
output.write(`\n${pc.dim('> ')}${currentLine}`);
|
|
109
121
|
};
|
|
122
|
+
const BRACKET_PASTE_START = '\u001b[200~';
|
|
123
|
+
const BRACKET_PASTE_END = '\u001b[201~';
|
|
124
|
+
const BRACKET_PASTE_PROBE_MAX = 96;
|
|
125
|
+
const updateBracketPasteState = (chunk) => {
|
|
126
|
+
if (!chunk)
|
|
127
|
+
return;
|
|
128
|
+
bracketPasteProbe = `${bracketPasteProbe}${chunk}`.slice(-BRACKET_PASTE_PROBE_MAX);
|
|
129
|
+
if (!bracketPasteActive) {
|
|
130
|
+
const startIndex = bracketPasteProbe.indexOf(BRACKET_PASTE_START);
|
|
131
|
+
if (startIndex >= 0) {
|
|
132
|
+
bracketPasteActive = true;
|
|
133
|
+
bracketPasteProbe = bracketPasteProbe.slice(startIndex + BRACKET_PASTE_START.length);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (bracketPasteActive) {
|
|
137
|
+
const endIndex = bracketPasteProbe.indexOf(BRACKET_PASTE_END);
|
|
138
|
+
if (endIndex >= 0) {
|
|
139
|
+
bracketPasteActive = false;
|
|
140
|
+
bracketPasteProbe = bracketPasteProbe.slice(endIndex + BRACKET_PASTE_END.length);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
110
144
|
showPrompt();
|
|
111
145
|
// Handle paste from clipboard
|
|
112
146
|
const handlePaste = async () => {
|
|
@@ -171,7 +205,7 @@ export const createMultilineTextPrompt = () => {
|
|
|
171
205
|
const now = Date.now();
|
|
172
206
|
// Check for double Enter to submit
|
|
173
207
|
if (line === '' && now - lastEnterTime < DOUBLE_ENTER_TIMEOUT) {
|
|
174
|
-
submit(
|
|
208
|
+
submit(buildSubmitValue());
|
|
175
209
|
return;
|
|
176
210
|
}
|
|
177
211
|
lastEnterTime = now;
|
|
@@ -207,9 +241,43 @@ export const createMultilineTextPrompt = () => {
|
|
|
207
241
|
input.setRawMode(true);
|
|
208
242
|
rawModeEnabled = true;
|
|
209
243
|
input.resume();
|
|
244
|
+
dataListener = (chunk) => {
|
|
245
|
+
if (cancelled)
|
|
246
|
+
return;
|
|
247
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
|
|
248
|
+
updateBracketPasteState(text);
|
|
249
|
+
};
|
|
250
|
+
input.on('data', dataListener);
|
|
210
251
|
keypressListener = async (str, key) => {
|
|
211
252
|
if (cancelled)
|
|
212
253
|
return;
|
|
254
|
+
if (key?.ctrl && key.name === 'c') {
|
|
255
|
+
doCancel();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (key?.name === 'escape') {
|
|
259
|
+
doCancel();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// In bracketed paste mode, treat all incoming keypresses as paste content.
|
|
263
|
+
// This avoids accidental submit on Enter while multi-line paste is flowing.
|
|
264
|
+
if (bracketPasteActive) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Some terminals emit Shift+Enter as literal chars "13~" with no key metadata.
|
|
268
|
+
// Readline may already have appended those chars into rl.line by the time we run.
|
|
269
|
+
if (!key?.ctrl && !key?.meta && key?.name !== 'enter' && key?.name !== 'return') {
|
|
270
|
+
const strippedLive = stripGhosttyShiftEnterSuffix(String(rl.line ?? ''));
|
|
271
|
+
if (strippedLive !== null) {
|
|
272
|
+
lines.push(strippedLive);
|
|
273
|
+
currentLine = '';
|
|
274
|
+
rl.line = '';
|
|
275
|
+
output.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
276
|
+
output.write(`${pc.dim('> ')}${strippedLive}`);
|
|
277
|
+
output.write(`\n${pc.dim('> ')}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
213
281
|
// Detect Ctrl+V or Cmd+V for paste
|
|
214
282
|
if ((key.ctrl && key.name === 'v') || (key.meta && key.name === 'v')) {
|
|
215
283
|
const pasted = await handlePaste();
|
|
@@ -241,10 +309,7 @@ export const createMultilineTextPrompt = () => {
|
|
|
241
309
|
insertNewline();
|
|
242
310
|
return;
|
|
243
311
|
}
|
|
244
|
-
submit(
|
|
245
|
-
}
|
|
246
|
-
else if (key.name === 'escape') {
|
|
247
|
-
doCancel();
|
|
312
|
+
submit(buildSubmitValue());
|
|
248
313
|
}
|
|
249
314
|
};
|
|
250
315
|
input.on('keypress', keypressListener);
|