@levu/snap 0.3.0 → 0.3.1

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,14 @@ 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.1] - 2026-02-26
9
+
10
+ ### Fixed
11
+ - Multiline text prompt now submits on `Enter` in raw input mode, restoring submit behavior in macOS terminals like JetBrains and Ghostty.
12
+ - Added `Shift+Enter` (and `Alt+Enter` fallback) support to insert newline in multiline prompt mode.
13
+ - Restored terminal state reliably by disabling raw mode and detaching keypress listeners on prompt cleanup.
14
+ - Added regression tests covering `Enter` submit and `Shift+Enter` newline behavior for multiline prompts.
15
+
8
16
  ## [0.2.0] - 2025-02-24
9
17
 
10
18
  ### 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,17 @@ 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;
28
31
  const cleanup = () => {
32
+ if (keypressListener) {
33
+ input.off('keypress', keypressListener);
34
+ }
35
+ if (rawModeEnabled && input.setRawMode) {
36
+ input.setRawMode(false);
37
+ rawModeEnabled = false;
38
+ }
29
39
  rl.close();
30
40
  };
31
41
  const submit = (val) => {
@@ -43,9 +53,15 @@ export const createMultilineTextPrompt = () => {
43
53
  if (allowPaste) {
44
54
  output.write(pc.dim(` Paste support: Ctrl+V to paste (macOS/Linux: Cmd+Shift+V)\n`));
45
55
  }
46
- output.write(pc.dim(` Press Enter twice or Alt+Enter to submit\n`));
56
+ output.write(pc.dim(` Press Enter to submit; Shift+Enter for newline (Alt+Enter fallback)\n`));
47
57
  const lines = value.split('\n');
48
58
  let currentLine = lines.length > 0 ? lines.pop() : '';
59
+ const getLiveLine = () => {
60
+ const rlLine = typeof rl.line === 'string' ? rl.line : '';
61
+ if (rlLine.length > 0)
62
+ return rlLine;
63
+ return currentLine;
64
+ };
49
65
  const showPrompt = () => {
50
66
  output.write(`\n${pc.dim('> ')}${currentLine}`);
51
67
  };
@@ -89,10 +105,14 @@ export const createMultilineTextPrompt = () => {
89
105
  rl.on('line', (line) => {
90
106
  if (cancelled)
91
107
  return;
108
+ if (ignoreNextLineEvent) {
109
+ ignoreNextLineEvent = false;
110
+ return;
111
+ }
92
112
  const now = Date.now();
93
113
  // Check for double Enter to submit
94
114
  if (line === '' && now - lastEnterTime < DOUBLE_ENTER_TIMEOUT) {
95
- submit(lines.join('\n') + currentLine);
115
+ submit(lines.concat(getLiveLine()).join('\n'));
96
116
  return;
97
117
  }
98
118
  lastEnterTime = now;
@@ -124,9 +144,11 @@ export const createMultilineTextPrompt = () => {
124
144
  }
125
145
  // Handle paste via keyboard shortcut
126
146
  if (allowPaste && input.setRawMode) {
147
+ emitKeypressEvents(input);
127
148
  input.setRawMode(true);
149
+ rawModeEnabled = true;
128
150
  input.resume();
129
- input.on('keypress', async (str, key) => {
151
+ keypressListener = async (str, key) => {
130
152
  if (cancelled)
131
153
  return;
132
154
  // Detect Ctrl+V or Cmd+V for paste
@@ -145,17 +167,27 @@ export const createMultilineTextPrompt = () => {
145
167
  // Single line paste
146
168
  currentLine += pasted;
147
169
  }
170
+ rl.line = currentLine;
148
171
  output.write(`${pc.dim('> ')}${currentLine}`);
149
172
  }
150
173
  }
151
- else if (key.alt && key.name === 'enter') {
152
- // Alt+Enter to submit
153
- submit(lines.join('\n') + currentLine);
174
+ else if (key.name === 'enter' || key.name === 'return') {
175
+ ignoreNextLineEvent = true;
176
+ if (key.shift || key.alt) {
177
+ // Shift+Enter / Alt+Enter inserts a new line.
178
+ lines.push(getLiveLine());
179
+ currentLine = '';
180
+ rl.line = '';
181
+ output.write(`\n${pc.dim('> ')}`);
182
+ return;
183
+ }
184
+ submit(lines.concat(getLiveLine()).join('\n'));
154
185
  }
155
186
  else if (key.name === 'escape') {
156
187
  doCancel();
157
188
  }
158
- });
189
+ };
190
+ input.on('keypress', keypressListener);
159
191
  }
160
192
  // Handle non-interactive terminal
161
193
  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 (when your terminal reports Shift modifiers)
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.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "snap": "./dist/cli-entry.js"