@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 twice or Alt+Enter to submit\n`));
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') + currentLine);
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
- input.on('keypress', async (str, key) => {
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 (key.alt && key.name === 'enter') {
152
- // Alt+Enter to submit
153
- submit(lines.join('\n') + currentLine);
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. Submit with:
60
- - `Alt+Enter` or `Cmd+Enter`
61
- - Double `Enter` (press Enter twice quickly)
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levu/snap",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "snap": "./dist/cli-entry.js"