mepcli 0.2.7 → 0.4.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 CHANGED
@@ -5,14 +5,15 @@
5
5
  ## Features
6
6
 
7
7
  - **Zero Dependency:** Keeps your project clean and fast.
8
- - **Comprehensive Prompts:** Includes `text`, `password`, `select`, `checkbox`, `confirm`, `number`, `toggle`, `list`, `slider`, `date`, `file`, and `multiSelect`.
8
+ - **Comprehensive Prompts:** Includes `text`, `password`, `select`, `checkbox`, `confirm`, `number`, `toggle`, `list`, `slider`, `date`, `file`, `multiSelect`, and `rating`.
9
+ - **Mouse Support:** Built-in support for mouse interaction (SGR 1006 protocol). Scroll to navigate lists or change values; click to select.
9
10
  - **Responsive Input:** Supports cursor movement (Left/Right) and character insertion/deletion in text-based prompts.
10
11
  - **Validation:** Built-in support for input validation (sync and async) with custom error messages.
11
12
  - **Elegant Look:** Uses ANSI colors for a clean, modern CLI experience.
12
13
 
13
14
  ## Installation
14
15
 
15
- ```bash
16
+ ```shell
16
17
  npm install mepcli
17
18
  # or
18
19
  yarn add mepcli
@@ -66,7 +67,15 @@ async function main() {
66
67
  ]
67
68
  });
68
69
 
69
- console.log({ name, age, newsletter, lang, tools });
70
+ // Rating (Star rating)
71
+ const stars = await MepCLI.rating({
72
+ message: "Rate your experience:",
73
+ min: 1,
74
+ max: 5,
75
+ initial: 5
76
+ });
77
+
78
+ console.log({ name, age, newsletter, lang, tools, stars });
70
79
  }
71
80
 
72
81
  main();
@@ -86,10 +95,23 @@ main();
86
95
  * `checkbox(options)` - Classic checkbox selection.
87
96
  * `list(options)` - Enter a list of tags/strings.
88
97
  * `slider(options)` - Select a number within a range using a visual slider.
98
+ * `rating(options)` - Star rating input.
89
99
  * `date(options)` - Date and time picker.
90
100
  * `file(options)` - File system navigator and selector.
91
101
  * `spin(message, promise)` - Display a spinner while waiting for a promise.
92
102
 
103
+ ## Mouse Support
104
+
105
+ MepCLI automatically detects modern terminals and enables **Mouse Tracking** (using SGR 1006 protocol).
106
+
107
+ * **Scrolling:**
108
+ * `select`, `multiSelect`, `checkbox`: Scroll to navigate the list.
109
+ * `number`, `slider`, `rating`, `date`: Scroll to increment/decrement values or fields.
110
+ * `toggle`, `confirm`: Scroll to toggle the state.
111
+ * **Configuration:**
112
+ * Mouse support is enabled by default if the terminal supports it.
113
+ * You can explicitly disable it per prompt by setting `mouse: false` in the options.
114
+
93
115
  ## License
94
116
 
95
117
  This project is under the **MIT License**.
package/dist/ansi.d.ts CHANGED
@@ -21,4 +21,7 @@ export declare const ANSI: {
21
21
  SHOW_CURSOR: string;
22
22
  UP: string;
23
23
  DOWN: string;
24
+ SET_ANY_EVENT_MOUSE: string;
25
+ SET_SGR_EXT_MODE_MOUSE: string;
26
+ DISABLE_MOUSE: string;
24
27
  };
package/dist/ansi.js CHANGED
@@ -26,4 +26,8 @@ exports.ANSI = {
26
26
  SHOW_CURSOR: '\x1b[?25h',
27
27
  UP: '\x1b[A',
28
28
  DOWN: '\x1b[B',
29
+ // Mouse Tracking (SGR Protocol)
30
+ SET_ANY_EVENT_MOUSE: '\x1b[?1003h',
31
+ SET_SGR_EXT_MODE_MOUSE: '\x1b[?1006h',
32
+ DISABLE_MOUSE: '\x1b[?1003l\x1b[?1006l',
29
33
  };
package/dist/base.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { MouseEvent } from './types';
1
2
  import { detectCapabilities } from './utils';
2
3
  /**
3
4
  * Abstract base class for all prompts.
@@ -29,6 +30,11 @@ export declare abstract class Prompt<T, O> {
29
30
  * @param key The raw buffer.
30
31
  */
31
32
  protected abstract handleInput(char: string, key: Buffer): void;
33
+ /**
34
+ * Optional method to handle mouse events.
35
+ * Subclasses can override this to implement mouse interaction.
36
+ */
37
+ protected handleMouse(event: MouseEvent): void;
32
38
  protected print(text: string): void;
33
39
  /**
34
40
  * Starts the prompt interaction.
@@ -47,6 +53,11 @@ export declare abstract class Prompt<T, O> {
47
53
  * Render Method with Diffing (Virtual DOM for CLI).
48
54
  * Calculates new lines, compares with old lines, and updates only changed parts.
49
55
  */
56
+ /**
57
+ * Renders the current frame by clearing the previous output and writing the new content.
58
+ * This approach ("Move Up -> Erase Down -> Print") is more robust against artifacts
59
+ * than line-by-line diffing, especially when the number of lines changes (e.g., filtering).
60
+ */
50
61
  protected renderFrame(content: string): void;
51
62
  protected stripAnsi(str: string): string;
52
63
  protected truncate(str: string, width: number): string;
package/dist/base.js CHANGED
@@ -20,6 +20,11 @@ class Prompt {
20
20
  this._inputParser = new input_1.InputParser();
21
21
  this.capabilities = (0, utils_1.detectCapabilities)();
22
22
  }
23
+ /**
24
+ * Optional method to handle mouse events.
25
+ * Subclasses can override this to implement mouse interaction.
26
+ */
27
+ handleMouse(event) { }
23
28
  print(text) {
24
29
  this.stdout.write(text);
25
30
  }
@@ -36,6 +41,12 @@ class Prompt {
36
41
  }
37
42
  this.stdin.resume();
38
43
  this.stdin.setEncoding('utf8');
44
+ // Enable Mouse Tracking if supported and requested
45
+ // Default to true if capabilities support it, unless explicitly disabled in options
46
+ const shouldEnableMouse = this.options.mouse !== false && this.capabilities.hasMouse;
47
+ if (shouldEnableMouse) {
48
+ this.print(ansi_1.ANSI.SET_ANY_EVENT_MOUSE + ansi_1.ANSI.SET_SGR_EXT_MODE_MOUSE);
49
+ }
39
50
  // Initial render: Default to hidden cursor (good for menus)
40
51
  // Subclasses like TextPrompt will explicitly show it if needed.
41
52
  if (this.capabilities.isCI) {
@@ -57,6 +68,10 @@ class Prompt {
57
68
  this.handleInput(char, buffer);
58
69
  };
59
70
  this._inputParser.on('keypress', this._onKeyHandler);
71
+ // Listen to mouse events
72
+ this._inputParser.on('mouse', (event) => {
73
+ this.handleMouse(event);
74
+ });
60
75
  this._onDataHandler = (buffer) => {
61
76
  this._inputParser.feed(buffer);
62
77
  };
@@ -73,6 +88,9 @@ class Prompt {
73
88
  if (this._onKeyHandler) {
74
89
  this._inputParser.removeListener('keypress', this._onKeyHandler);
75
90
  }
91
+ // Cleanup mouse listener - though InputParser is instance specific, so it's fine.
92
+ // Disable Mouse Tracking
93
+ this.print(ansi_1.ANSI.DISABLE_MOUSE);
76
94
  if (typeof this.stdin.setRawMode === 'function') {
77
95
  this.stdin.setRawMode(false);
78
96
  }
@@ -93,170 +111,37 @@ class Prompt {
93
111
  * Render Method with Diffing (Virtual DOM for CLI).
94
112
  * Calculates new lines, compares with old lines, and updates only changed parts.
95
113
  */
114
+ /**
115
+ * Renders the current frame by clearing the previous output and writing the new content.
116
+ * This approach ("Move Up -> Erase Down -> Print") is more robust against artifacts
117
+ * than line-by-line diffing, especially when the number of lines changes (e.g., filtering).
118
+ */
96
119
  renderFrame(content) {
97
- // Ensure lines are truncated to terminal width
98
120
  const width = this.stdout.columns || 80;
99
121
  const rawLines = content.split('\n');
100
- // Truncate each line and prepare the new buffer
122
+ // Truncate each line to fit terminal width to avoid wrapping issues
101
123
  const newLines = rawLines.map(line => this.truncate(line, width));
102
- // Cursor logic:
103
- // We assume the cursor is currently at the END of the last rendered frame.
104
- // But to diff, it's easier to always reset to the top of the frame first.
105
- // 1. Move Cursor to Top of the Frame
124
+ // 1. Move cursor to the start of the current line
125
+ this.print(ansi_1.ANSI.CURSOR_LEFT);
126
+ // 2. Move cursor up to the top of the previously rendered frame
106
127
  if (this.lastRenderHeight > 0) {
107
- this.print(`\x1b[${this.lastRenderHeight}A`); // Move up N lines
108
- // Actually, if last height was 1 (just one line), we move up 1 line?
109
- // "A\n" -> 2 lines. Cursor at bottom.
110
- // If we move up, we are at top.
111
- // Wait, if lastRenderHeight includes the "current line" which is usually empty if we printed with newlines?
112
- // Let's stick to: we printed N lines. Cursor is at line N+1 (start).
113
- // To go to line 1, we move up N lines.
114
- this.print(`\x1b[${this.lastRenderHeight}A`);
128
+ // If the previous render had multiple lines, move up to the first line
129
+ if (this.lastRenderHeight > 1) {
130
+ this.print(`\x1b[${this.lastRenderHeight - 1}A`);
131
+ }
115
132
  }
116
- this.print(ansi_1.ANSI.CURSOR_LEFT);
117
- // 2. Diff and Render
118
- // We iterate through newLines.
119
- // For each line, check if it matches lastRenderLines[i].
133
+ // 3. Clear everything from the cursor down
134
+ // This ensures all previous content (including "ghost" lines) is removed
135
+ this.print(ansi_1.ANSI.ERASE_DOWN);
136
+ // 4. Print the new frame content
120
137
  for (let i = 0; i < newLines.length; i++) {
121
- const newLine = newLines[i];
122
- const oldLine = this.lastRenderLines[i];
123
- if (newLine !== oldLine) {
124
- // Move to this line if not already there?
125
- // We are writing sequentially, so after writing line i-1 (or skipping it),
126
- // the cursor might not be at the start of line i if we skipped.
127
- // Strategy:
128
- // If we skipped lines, we need to jump down.
129
- // But simpler: just move cursor to line i relative to top.
130
- // \x1b[<N>B moves down N lines.
131
- // But we are processing sequentially.
132
- // If we are at line 0 (Top).
133
- // Process line 0.
134
- // If changed, write it + \n (or clear line + write).
135
- // If unchanged, move cursor down 1 line.
136
- // Wait, if we use \n at end of write, cursor moves down.
137
- // If we skip writing, we must manually move down.
138
- this.print(ansi_1.ANSI.ERASE_LINE); // Clear current line
139
- this.print(newLine);
140
- }
141
- // Prepare for next line
138
+ this.print(newLines[i]);
139
+ // Add newline character between lines, but not after the last line
142
140
  if (i < newLines.length - 1) {
143
- // If we wrote something, we are at end of line (maybe wrapped?).
144
- // Since we truncate, we are not wrapped.
145
- // But we didn't print \n yet if we just printed newLine.
146
- // To move to next line start:
147
141
  this.print('\n');
148
142
  }
149
143
  }
150
- // 3. Clear remaining lines if new output is shorter
151
- if (newLines.length < this.lastRenderLines.length) {
152
- // We are at the last line of new output.
153
- // BUG FIX: If the last line was unchanged, we skipped printing.
154
- // The cursor is currently at the START of that line (or end of previous).
155
- // We need to ensure we move to the NEXT line (or end of current) before clearing down.
156
- // If we just finished loop `i = newLines.length - 1`, we are theoretically at the end of the content.
157
- // However, since we might have skipped the last line, we need to be careful.
158
- // Let's force move to the end of the visual content we just defined.
159
- // Actually, simplest way: Just move cursor to start of line N (where N = newLines.length).
160
- // Currently we are at line newLines.length - 1.
161
- // We need to move down 1 line?
162
- // If newLines has 1 line. Loop runs 0.
163
- // If skipped, we are at start of line 0.
164
- // We need to be at line 1 to clear from there down.
165
- // But we didn't print \n.
166
- // So: move cursor to (newLines.length) relative to top.
167
- // We started at Top.
168
- // We iterated newLines.
169
- // We injected \n between lines.
170
- // The cursor is implicitly tracking where we are.
171
- // IF we skipped, we are physically at start of line `i`.
172
- // We need to move over it.
173
- // Fix: After the loop, explicitly move to the line AFTER the last line.
174
- // Since we know where we started (Top), we can just jump to line `newLines.length`.
175
- // But we are in relative movement land.
176
- // Let's calculate where we *should* be: End of content.
177
- // If we just rendered N lines, we want to be at line N+1 (conceptually) to clear below?
178
- // Or just at the start of line N+1?
179
- // If we have 2 lines.
180
- // Line 0. \n. Line 1.
181
- // Cursor is at end of Line 1.
182
- // If we skipped Line 1, cursor is at start of Line 1.
183
- // We want to clear everything BELOW Line 1.
184
- // So we should be at start of Line 2.
185
- // Logic:
186
- // 1. We are currently at the cursor position after processing `newLines`.
187
- // If last line was skipped, we are at start of last line.
188
- // If last line was written, we are at end of last line.
189
- // 2. We want to erase from the line *following* the last valid line.
190
- // We can just calculate the difference and move down if needed.
191
- // But simpler: Move cursor to the conceptual "end" of the new frame.
192
- // If we processed `newLines.length` lines.
193
- // We want to be at row `newLines.length` (0-indexed) to start clearing?
194
- // No, rows are 0 to N-1.
195
- // We want to clear starting from row N.
196
- // Since we can't easily query cursor pos, let's use the fact we reset to Top.
197
- // We can move to row N relative to current?
198
- // Wait, `ERASE_DOWN` clears from cursor to end of screen.
199
- // If we are at start of Line 1 (and it's valid), `ERASE_DOWN` deletes Line 1!
200
- // So we MUST be past Line 1.
201
- // If we skipped the last line, we must strictly move past it.
202
- // How? `\x1b[1B` (Down).
203
- // But we don't track if we skipped the last line explicitly outside the loop.
204
- // Let's just track `currentLineIndex`.
205
- // Alternate robust approach:
206
- // After loop, we forcefully move cursor to `newLines.length` lines down from Top.
207
- // We are currently at some unknown state (Start or End of last line).
208
- // BUT we can just move UP to Top again and then move DOWN N lines.
209
- // That feels safe.
210
- // Reset to top of frame (which we are already inside/near).
211
- // But we don't know exactly where we are relative to top anymore.
212
- // Let's rely on the loop index.
213
- // If loop finished, `i` was `newLines.length`.
214
- // If `newLines.length > 0`.
215
- // If we skipped the last line (index `len-1`), we are at start of it.
216
- // If we wrote it, we are at end of it.
217
- // If we skipped, we need `\x1b[1B`.
218
- // If we wrote, we are at end. `ERASE_DOWN` from end of line clears rest of line + below.
219
- // BUT we want to clear BELOW.
220
- // `ERASE_DOWN` (J=0) clears from cursor to end of screen.
221
- // If at end of line, it clears rest of that line (nothing) and lines below. Correct.
222
- // So the issue is ONLY when we skipped the last line.
223
- const lastLineIdx = newLines.length - 1;
224
- if (lastLineIdx >= 0 && newLines[lastLineIdx] === this.lastRenderLines[lastLineIdx]) {
225
- // We skipped the last line. Move down 1 line to ensure we don't delete it.
226
- // Also move to start (CR) to be safe?
227
- this.print('\n');
228
- // Wait, \n moves down AND to start usually.
229
- // But strictly \n is Line Feed (Down). \r is Carriage Return (Left).
230
- // Console usually treats \n as \r\n in cooked mode, but in raw mode?
231
- // We are in raw mode.
232
- // We likely need \r\n or explicit movement.
233
- // Let's just use \x1b[1B (Down) and \r (Left).
234
- // Actually, if we use `\n` in loop, we rely on it working.
235
- // Let's assume `\x1b[1B` is safer for "just move down".
236
- // But wait, if we are at start of line, `1B` puts us at start of next line.
237
- // `ERASE_DOWN` there is perfect.
238
- this.print('\x1b[1B');
239
- this.print('\r'); // Move to start
240
- }
241
- else {
242
- // We wrote the last line. We are at the end of it.
243
- // `ERASE_DOWN` will clear lines below.
244
- // BUT if we want to clear the REST of the screen cleanly starting from next line...
245
- // It's mostly fine.
246
- // However, there is a subtle case:
247
- // If we wrote the line, we are at the end of it.
248
- // If we call ERASE_DOWN, it keeps current line intact (from cursor onwards, which is empty).
249
- // And clears below.
250
- // This is correct.
251
- // EXCEPT: If the old screen had MORE lines, we want to clear them.
252
- // If we are at end of Line N-1.
253
- // Line N exists in old screen.
254
- // ERASE_DOWN clears Line N etc.
255
- // Correct.
256
- }
257
- this.print(ansi_1.ANSI.ERASE_DOWN);
258
- }
259
- // Update state
144
+ // 5. Update state for the next render cycle
260
145
  this.lastRenderLines = newLines;
261
146
  this.lastRenderHeight = newLines.length;
262
147
  }
@@ -272,10 +157,6 @@ class Prompt {
272
157
  // We iterate and sum width until we hit limit - 3
273
158
  let currentWidth = 0;
274
159
  let cutIndex = 0;
275
- // We need to iterate by Code Point or Grapheme to be safe?
276
- // Let's use simple char iteration for speed, but respect ANSI.
277
- // Actually, reusing the logic from stringWidth might be best but
278
- // we need the index.
279
160
  let inAnsi = false;
280
161
  for (let i = 0; i < str.length; i++) {
281
162
  const code = str.charCodeAt(i);
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions } from './types';
1
+ import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions, RatingOptions } from './types';
2
2
  /**
3
3
  * Public Facade for MepCLI
4
4
  */
@@ -20,4 +20,5 @@ export declare class MepCLI {
20
20
  static date(options: DateOptions): Promise<Date>;
21
21
  static file(options: FileOptions): Promise<string>;
22
22
  static multiSelect<const V>(options: MultiSelectOptions<V>): Promise<V[]>;
23
+ static rating(options: RatingOptions): Promise<number>;
23
24
  }
package/dist/core.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MepCLI = void 0;
4
4
  const ansi_1 = require("./ansi");
5
5
  const theme_1 = require("./theme");
6
+ const symbols_1 = require("./symbols");
6
7
  const text_1 = require("./prompts/text");
7
8
  const select_1 = require("./prompts/select");
8
9
  const checkbox_1 = require("./prompts/checkbox");
@@ -14,6 +15,7 @@ const slider_1 = require("./prompts/slider");
14
15
  const date_1 = require("./prompts/date");
15
16
  const file_1 = require("./prompts/file");
16
17
  const multi_select_1 = require("./prompts/multi-select");
18
+ const rating_1 = require("./prompts/rating");
17
19
  /**
18
20
  * Public Facade for MepCLI
19
21
  */
@@ -22,7 +24,7 @@ class MepCLI {
22
24
  * Shows a spinner while a promise is pending.
23
25
  */
24
26
  static async spin(message, taskPromise) {
25
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
27
+ const frames = symbols_1.symbols.spinner;
26
28
  let i = 0;
27
29
  process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
28
30
  const interval = setInterval(() => {
@@ -32,13 +34,13 @@ class MepCLI {
32
34
  try {
33
35
  const result = await taskPromise;
34
36
  clearInterval(interval);
35
- process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}✔${ansi_1.ANSI.RESET} ${message}\n`);
37
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}${symbols_1.symbols.tick}${ansi_1.ANSI.RESET} ${message}\n`);
36
38
  process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
37
39
  return result;
38
40
  }
39
41
  catch (error) {
40
42
  clearInterval(interval);
41
- process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.error}✖${ansi_1.ANSI.RESET} ${message}\n`);
43
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.error}${symbols_1.symbols.cross}${ansi_1.ANSI.RESET} ${message}\n`);
42
44
  process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
43
45
  throw error;
44
46
  }
@@ -79,6 +81,9 @@ class MepCLI {
79
81
  static multiSelect(options) {
80
82
  return new multi_select_1.MultiSelectPrompt(options).run();
81
83
  }
84
+ static rating(options) {
85
+ return new rating_1.RatingPrompt(options).run();
86
+ }
82
87
  }
83
88
  exports.MepCLI = MepCLI;
84
89
  MepCLI.theme = theme_1.theme;
package/dist/input.d.ts CHANGED
@@ -9,5 +9,6 @@ export declare class InputParser extends EventEmitter {
9
9
  */
10
10
  feed(data: Buffer): void;
11
11
  private processChar;
12
+ private parseSGRMouse;
12
13
  private emitKey;
13
14
  }
package/dist/input.js CHANGED
@@ -14,14 +14,6 @@ class InputParser extends events_1.EventEmitter {
14
14
  * Feed data into the parser.
15
15
  */
16
16
  feed(data) {
17
- // Convert buffer to string.
18
- // For partial multi-byte sequences at the chunk boundary,
19
- // buffer.toString() might produce replacement chars.
20
- // Ideally we should use StringDecoder, but since we are handling KeyPresses,
21
- // and usually a keypress is complete, simple toString often works.
22
- // However, the user mentioned fragmentation issues.
23
- // But InputParser usually receives data from stdin.
24
- // The core issue of fragmentation is splitting escape codes like \x1b [ A
25
17
  const input = data.toString('utf-8');
26
18
  for (let i = 0; i < input.length; i++) {
27
19
  const char = input[i];
@@ -67,6 +59,11 @@ class InputParser extends events_1.EventEmitter {
67
59
  }
68
60
  else if (this.state === 'CSI') {
69
61
  this.buffer += char;
62
+ if (char === '<') {
63
+ this.state = 'MOUSE_SGR';
64
+ this.buffer = '<';
65
+ return;
66
+ }
70
67
  // CSI sequences end with 0x40-0x7E
71
68
  if (char >= '@' && char <= '~') {
72
69
  this.emitKey(this.buffer);
@@ -75,6 +72,63 @@ class InputParser extends events_1.EventEmitter {
75
72
  }
76
73
  // Otherwise, we keep buffering (params like 1;2)
77
74
  }
75
+ else if (this.state === 'MOUSE_SGR') {
76
+ this.buffer += char;
77
+ // SGR sequences end with 'm' (release) or 'M' (press)
78
+ if (char === 'm' || char === 'M') {
79
+ this.parseSGRMouse(this.buffer);
80
+ this.buffer = '';
81
+ this.state = 'NORMAL';
82
+ }
83
+ }
84
+ }
85
+ parseSGRMouse(buffer) {
86
+ // console.log('Parsing SGR:', buffer);
87
+ // format: <b;x;yM or <b;x;ym
88
+ // buffer includes the leading < and trailing M/m
89
+ const content = buffer.slice(1, -1);
90
+ const type = buffer.slice(-1); // m or M
91
+ const parts = content.split(';').map(Number);
92
+ if (parts.length >= 3) {
93
+ const [b, x, y] = parts;
94
+ let action = 'press';
95
+ // Interpret button codes
96
+ // 0: Left, 1: Middle, 2: Right
97
+ // +32: Motion
98
+ // 64: Scroll Up
99
+ // 65: Scroll Down
100
+ if (b === 64) {
101
+ action = 'scroll';
102
+ this.emit('mouse', { name: 'mouse', x, y, button: 0, action, scroll: 'up' });
103
+ // Also emit keypress for scroll if needed? No, prompt should listen to mouse.
104
+ // But for "Easy Features", we emit standard names
105
+ this.emit('scrollup');
106
+ return;
107
+ }
108
+ if (b === 65) {
109
+ action = 'scroll';
110
+ this.emit('mouse', { name: 'mouse', x, y, button: 0, action, scroll: 'down' });
111
+ this.emit('scrolldown');
112
+ return;
113
+ }
114
+ if (type === 'm') {
115
+ action = 'release';
116
+ }
117
+ else {
118
+ action = 'press';
119
+ // Check if motion
120
+ if (b & 32) {
121
+ action = 'move';
122
+ }
123
+ }
124
+ this.emit('mouse', {
125
+ name: 'mouse',
126
+ x,
127
+ y,
128
+ button: b & 3, // Strip modifiers to get raw button 0-2
129
+ action
130
+ });
131
+ }
78
132
  }
79
133
  emitKey(key) {
80
134
  // Normalize Enter
@@ -1,5 +1,5 @@
1
1
  import { Prompt } from '../base';
2
- import { CheckboxOptions } from '../types';
2
+ import { CheckboxOptions, MouseEvent } from '../types';
3
3
  export declare class CheckboxPrompt<V> extends Prompt<any[], CheckboxOptions<V>> {
4
4
  private selectedIndex;
5
5
  private checkedState;
@@ -9,4 +9,5 @@ export declare class CheckboxPrompt<V> extends Prompt<any[], CheckboxOptions<V>>
9
9
  constructor(options: CheckboxOptions<V>);
10
10
  protected render(firstRender: boolean): void;
11
11
  protected handleInput(char: string): void;
12
+ protected handleMouse(event: MouseEvent): void;
12
13
  }
@@ -4,6 +4,7 @@ exports.CheckboxPrompt = void 0;
4
4
  const ansi_1 = require("../ansi");
5
5
  const base_1 = require("../base");
6
6
  const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
7
8
  // --- Implementation: Checkbox Prompt ---
8
9
  class CheckboxPrompt extends base_1.Prompt {
9
10
  constructor(options) {
@@ -29,7 +30,7 @@ class CheckboxPrompt extends base_1.Prompt {
29
30
  }
30
31
  let output = '';
31
32
  // Header
32
- const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
33
+ const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
33
34
  output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${theme_1.theme.muted}(Press <space> to select, <enter> to confirm)${ansi_1.ANSI.RESET}`;
34
35
  // List
35
36
  const choices = this.options.choices;
@@ -37,11 +38,11 @@ class CheckboxPrompt extends base_1.Prompt {
37
38
  visibleChoices.forEach((choice, index) => {
38
39
  const actualIndex = this.scrollTop + index;
39
40
  output += '\n'; // New line for each item
40
- const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
41
+ const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}${symbols_1.symbols.pointer}${ansi_1.ANSI.RESET}` : ' ';
41
42
  const isChecked = this.checkedState[actualIndex];
42
43
  const checkbox = isChecked
43
- ? `${theme_1.theme.success}◉${ansi_1.ANSI.RESET}`
44
- : `${theme_1.theme.muted}◯${ansi_1.ANSI.RESET}`;
44
+ ? `${theme_1.theme.success}${symbols_1.symbols.checked}${ansi_1.ANSI.RESET}`
45
+ : `${theme_1.theme.muted}${symbols_1.symbols.unchecked}${ansi_1.ANSI.RESET}`;
45
46
  const title = actualIndex === this.selectedIndex
46
47
  ? `${theme_1.theme.main}${choice.title}${ansi_1.ANSI.RESET}`
47
48
  : choice.title;
@@ -110,5 +111,19 @@ class CheckboxPrompt extends base_1.Prompt {
110
111
  this.render(false);
111
112
  }
112
113
  }
114
+ handleMouse(event) {
115
+ if (event.action === 'scroll') {
116
+ if (event.scroll === 'up') {
117
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.choices.length - 1;
118
+ this.errorMsg = '';
119
+ this.render(false);
120
+ }
121
+ else if (event.scroll === 'down') {
122
+ this.selectedIndex = this.selectedIndex < this.options.choices.length - 1 ? this.selectedIndex + 1 : 0;
123
+ this.errorMsg = '';
124
+ this.render(false);
125
+ }
126
+ }
127
+ }
113
128
  }
114
129
  exports.CheckboxPrompt = CheckboxPrompt;
@@ -1,7 +1,8 @@
1
1
  import { Prompt } from '../base';
2
- import { ConfirmOptions } from '../types';
2
+ import { ConfirmOptions, MouseEvent } from '../types';
3
3
  export declare class ConfirmPrompt extends Prompt<boolean, ConfirmOptions> {
4
4
  constructor(options: ConfirmOptions);
5
5
  protected render(firstRender: boolean): void;
6
6
  protected handleInput(char: string): void;
7
+ protected handleMouse(event: MouseEvent): void;
7
8
  }
@@ -38,5 +38,11 @@ class ConfirmPrompt extends base_1.Prompt {
38
38
  this.render(false);
39
39
  }
40
40
  }
41
+ handleMouse(event) {
42
+ if (event.action === 'scroll') {
43
+ this.value = !this.value;
44
+ this.render(false);
45
+ }
46
+ }
41
47
  }
42
48
  exports.ConfirmPrompt = ConfirmPrompt;
@@ -1,5 +1,5 @@
1
1
  import { Prompt } from '../base';
2
- import { DateOptions } from '../types';
2
+ import { DateOptions, MouseEvent } from '../types';
3
3
  export declare class DatePrompt extends Prompt<Date, DateOptions> {
4
4
  private selectedField;
5
5
  private errorMsg;
@@ -7,4 +7,6 @@ export declare class DatePrompt extends Prompt<Date, DateOptions> {
7
7
  constructor(options: DateOptions);
8
8
  protected render(firstRender: boolean): void;
9
9
  protected handleInput(char: string): void;
10
+ protected handleMouse(event: MouseEvent): void;
11
+ private adjustDate;
10
12
  }