mepcli 0.3.0 → 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.
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
  }
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
@@ -15,6 +15,7 @@ const slider_1 = require("./prompts/slider");
15
15
  const date_1 = require("./prompts/date");
16
16
  const file_1 = require("./prompts/file");
17
17
  const multi_select_1 = require("./prompts/multi-select");
18
+ const rating_1 = require("./prompts/rating");
18
19
  /**
19
20
  * Public Facade for MepCLI
20
21
  */
@@ -80,6 +81,9 @@ class MepCLI {
80
81
  static multiSelect(options) {
81
82
  return new multi_select_1.MultiSelectPrompt(options).run();
82
83
  }
84
+ static rating(options) {
85
+ return new rating_1.RatingPrompt(options).run();
86
+ }
83
87
  }
84
88
  exports.MepCLI = MepCLI;
85
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
  }
@@ -111,5 +111,19 @@ class CheckboxPrompt extends base_1.Prompt {
111
111
  this.render(false);
112
112
  }
113
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
+ }
114
128
  }
115
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
  }
@@ -37,6 +37,18 @@ class DatePrompt extends base_1.Prompt {
37
37
  }
38
38
  handleInput(char) {
39
39
  if (char === '\r' || char === '\n') {
40
+ // Min constraint check
41
+ if (this.options.min && this.value < this.options.min) {
42
+ this.errorMsg = 'Date cannot be before minimum allowed.';
43
+ this.render(false);
44
+ return;
45
+ }
46
+ // Max constraint check
47
+ if (this.options.max && this.value > this.options.max) {
48
+ this.errorMsg = 'Date cannot be after maximum allowed.';
49
+ this.render(false);
50
+ return;
51
+ }
40
52
  this.submit(this.value);
41
53
  return;
42
54
  }
@@ -89,7 +101,16 @@ class DatePrompt extends base_1.Prompt {
89
101
  else if (this.selectedField === 4)
90
102
  d.setMinutes(Math.max(0, Math.min(59, finalVal)));
91
103
  this.value = d;
92
- this.errorMsg = '';
104
+ // Check immediately after updating the value to display an error message (but still allow further input)
105
+ if (this.options.min && this.value < this.options.min) {
106
+ this.errorMsg = 'Warning: Date is before minimum.';
107
+ }
108
+ else if (this.options.max && this.value > this.options.max) {
109
+ this.errorMsg = 'Warning: Date is after maximum.';
110
+ }
111
+ else {
112
+ this.errorMsg = '';
113
+ }
93
114
  this.render(false);
94
115
  return;
95
116
  }
@@ -99,39 +120,49 @@ class DatePrompt extends base_1.Prompt {
99
120
  if (isUp || isDown) {
100
121
  this.inputBuffer = ''; // Reset buffer on arrow move
101
122
  const dir = isUp ? 1 : -1;
102
- const d = new Date(this.value);
103
- switch (this.selectedField) {
104
- case 0:
105
- d.setFullYear(d.getFullYear() + dir);
106
- break;
107
- case 1:
108
- d.setMonth(d.getMonth() + dir);
109
- break;
110
- case 2:
111
- d.setDate(d.getDate() + dir);
112
- break;
113
- case 3:
114
- d.setHours(d.getHours() + dir);
115
- break;
116
- case 4:
117
- d.setMinutes(d.getMinutes() + dir);
118
- break;
119
- }
120
- let valid = true;
121
- if (this.options.min && d < this.options.min) {
122
- this.errorMsg = 'Date cannot be before minimum allowed.';
123
- valid = false;
124
- }
125
- if (this.options.max && d > this.options.max) {
126
- this.errorMsg = 'Date cannot be after maximum allowed.';
127
- valid = false;
123
+ this.adjustDate(dir);
124
+ }
125
+ }
126
+ handleMouse(event) {
127
+ if (event.action === 'scroll') {
128
+ if (event.scroll === 'up') {
129
+ this.adjustDate(1);
128
130
  }
129
- if (valid) {
130
- this.value = d;
131
- this.errorMsg = '';
131
+ else if (event.scroll === 'down') {
132
+ this.adjustDate(-1);
132
133
  }
133
- this.render(false);
134
134
  }
135
135
  }
136
+ adjustDate(dir) {
137
+ const d = new Date(this.value);
138
+ switch (this.selectedField) {
139
+ case 0:
140
+ d.setFullYear(d.getFullYear() + dir);
141
+ break;
142
+ case 1:
143
+ d.setMonth(d.getMonth() + dir);
144
+ break;
145
+ case 2:
146
+ d.setDate(d.getDate() + dir);
147
+ break;
148
+ case 3:
149
+ d.setHours(d.getHours() + dir);
150
+ break;
151
+ case 4:
152
+ d.setMinutes(d.getMinutes() + dir);
153
+ break;
154
+ }
155
+ this.value = d;
156
+ if (this.options.min && this.value < this.options.min) {
157
+ this.errorMsg = 'Date cannot be before minimum allowed.';
158
+ }
159
+ else if (this.options.max && this.value > this.options.max) {
160
+ this.errorMsg = 'Date cannot be after maximum allowed.';
161
+ }
162
+ else {
163
+ this.errorMsg = '';
164
+ }
165
+ this.render(false);
166
+ }
136
167
  }
137
168
  exports.DatePrompt = DatePrompt;
@@ -1,5 +1,5 @@
1
1
  import { Prompt } from '../base';
2
- import { MultiSelectOptions } from '../types';
2
+ import { MultiSelectOptions, MouseEvent } from '../types';
3
3
  export declare class MultiSelectPrompt<V> extends Prompt<any[], MultiSelectOptions<V>> {
4
4
  private selectedIndex;
5
5
  private checkedState;
@@ -11,4 +11,5 @@ export declare class MultiSelectPrompt<V> extends Prompt<any[], MultiSelectOptio
11
11
  private getFilteredChoices;
12
12
  protected render(firstRender: boolean): void;
13
13
  protected handleInput(char: string): void;
14
+ protected handleMouse(event: MouseEvent): void;
14
15
  }
@@ -120,5 +120,20 @@ class MultiSelectPrompt extends base_1.Prompt {
120
120
  this.render(false);
121
121
  }
122
122
  }
123
+ handleMouse(event) {
124
+ const choices = this.getFilteredChoices();
125
+ if (choices.length === 0)
126
+ return;
127
+ if (event.action === 'scroll') {
128
+ if (event.scroll === 'up') {
129
+ this.selectedIndex = (this.selectedIndex - 1 + choices.length) % choices.length;
130
+ this.render(false);
131
+ }
132
+ else if (event.scroll === 'down') {
133
+ this.selectedIndex = (this.selectedIndex + 1) % choices.length;
134
+ this.render(false);
135
+ }
136
+ }
137
+ }
123
138
  }
124
139
  exports.MultiSelectPrompt = MultiSelectPrompt;
@@ -1,5 +1,5 @@
1
1
  import { Prompt } from '../base';
2
- import { NumberOptions } from '../types';
2
+ import { NumberOptions, MouseEvent } from '../types';
3
3
  export declare class NumberPrompt extends Prompt<number, NumberOptions> {
4
4
  private stringValue;
5
5
  private cursor;
@@ -7,4 +7,5 @@ export declare class NumberPrompt extends Prompt<number, NumberOptions> {
7
7
  constructor(options: NumberOptions);
8
8
  protected render(firstRender: boolean): void;
9
9
  protected handleInput(char: string): void;
10
+ protected handleMouse(event: MouseEvent): void;
10
11
  }
@@ -130,5 +130,27 @@ class NumberPrompt extends base_1.Prompt {
130
130
  this.render(false);
131
131
  }
132
132
  }
133
+ handleMouse(event) {
134
+ if (event.action === 'scroll') {
135
+ let num = parseFloat(this.stringValue) || 0;
136
+ const step = this.options.step ?? 1;
137
+ if (event.scroll === 'up') {
138
+ num += step;
139
+ if (this.options.max !== undefined && num > this.options.max)
140
+ num = this.options.max;
141
+ }
142
+ else if (event.scroll === 'down') {
143
+ num -= step;
144
+ if (this.options.min !== undefined && num < this.options.min)
145
+ num = this.options.min;
146
+ }
147
+ // Round to avoid float errors
148
+ num = Math.round(num * 10000) / 10000;
149
+ this.stringValue = num.toString();
150
+ this.cursor = this.stringValue.length;
151
+ this.errorMsg = '';
152
+ this.render(false);
153
+ }
154
+ }
133
155
  }
134
156
  exports.NumberPrompt = NumberPrompt;
@@ -0,0 +1,8 @@
1
+ import { Prompt } from '../base';
2
+ import { RatingOptions, MouseEvent } from '../types';
3
+ export declare class RatingPrompt extends Prompt<number, RatingOptions> {
4
+ constructor(options: RatingOptions);
5
+ protected render(firstRender: boolean): void;
6
+ protected handleInput(char: string): void;
7
+ protected handleMouse(event: MouseEvent): void;
8
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RatingPrompt = void 0;
4
+ const ansi_1 = require("../ansi");
5
+ const base_1 = require("../base");
6
+ const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
8
+ class RatingPrompt extends base_1.Prompt {
9
+ constructor(options) {
10
+ super(options);
11
+ // Default to min if initial is not provided
12
+ this.value = options.initial ?? (options.min || 1);
13
+ }
14
+ render(firstRender) {
15
+ const min = this.options.min || 1;
16
+ const max = this.options.max || 5;
17
+ // Render stars
18
+ let stars = '';
19
+ for (let i = min; i <= max; i++) {
20
+ if (i <= this.value) {
21
+ stars += `${theme_1.theme.success}${symbols_1.symbols.star}${ansi_1.ANSI.RESET} `;
22
+ }
23
+ else {
24
+ stars += `${theme_1.theme.muted}${symbols_1.symbols.starEmpty}${ansi_1.ANSI.RESET} `;
25
+ }
26
+ }
27
+ const output = `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${stars}`;
28
+ this.renderFrame(output);
29
+ }
30
+ handleInput(char) {
31
+ const min = this.options.min || 1;
32
+ const max = this.options.max || 5;
33
+ if (char === '\r' || char === '\n') {
34
+ this.submit(this.value);
35
+ return;
36
+ }
37
+ // Arrow keys
38
+ if (this.isLeft(char) || this.isDown(char)) { // Left/Down decreases
39
+ if (this.value > min) {
40
+ this.value--;
41
+ this.render(false);
42
+ }
43
+ }
44
+ else if (this.isRight(char) || this.isUp(char)) { // Right/Up increases
45
+ if (this.value < max) {
46
+ this.value++;
47
+ this.render(false);
48
+ }
49
+ }
50
+ // Number keys (1-9)
51
+ const num = parseInt(char);
52
+ if (!isNaN(num)) {
53
+ if (num >= min && num <= max) {
54
+ this.value = num;
55
+ this.render(false);
56
+ }
57
+ }
58
+ }
59
+ handleMouse(event) {
60
+ const min = this.options.min || 1;
61
+ const max = this.options.max || 5;
62
+ if (event.action === 'scroll') {
63
+ if (event.scroll === 'up') {
64
+ if (this.value < max) {
65
+ this.value++;
66
+ this.render(false);
67
+ }
68
+ }
69
+ else if (event.scroll === 'down') {
70
+ if (this.value > min) {
71
+ this.value--;
72
+ this.render(false);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ exports.RatingPrompt = RatingPrompt;
@@ -1,5 +1,5 @@
1
1
  import { Prompt } from '../base';
2
- import { SelectOptions } from '../types';
2
+ import { SelectOptions, MouseEvent } from '../types';
3
3
  export declare class SelectPrompt<V> extends Prompt<any, SelectOptions<V>> {
4
4
  private selectedIndex;
5
5
  private searchBuffer;
@@ -11,4 +11,5 @@ export declare class SelectPrompt<V> extends Prompt<any, SelectOptions<V>> {
11
11
  private getFilteredChoices;
12
12
  protected render(firstRender: boolean): void;
13
13
  protected handleInput(char: string): void;
14
+ protected handleMouse(event: MouseEvent): void;
14
15
  }
@@ -145,5 +145,20 @@ class SelectPrompt extends base_1.Prompt {
145
145
  this.render(false);
146
146
  }
147
147
  }
148
+ handleMouse(event) {
149
+ const choices = this.getFilteredChoices();
150
+ if (choices.length === 0)
151
+ return;
152
+ if (event.action === 'scroll') {
153
+ if (event.scroll === 'up') {
154
+ this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, -1);
155
+ this.render(false);
156
+ }
157
+ else if (event.scroll === 'down') {
158
+ this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, 1);
159
+ this.render(false);
160
+ }
161
+ }
162
+ }
148
163
  }
149
164
  exports.SelectPrompt = SelectPrompt;
@@ -1,7 +1,8 @@
1
1
  import { Prompt } from '../base';
2
- import { SliderOptions } from '../types';
2
+ import { SliderOptions, MouseEvent } from '../types';
3
3
  export declare class SliderPrompt extends Prompt<number, SliderOptions> {
4
4
  constructor(options: SliderOptions);
5
5
  protected render(firstRender: boolean): void;
6
6
  protected handleInput(char: string): void;
7
+ protected handleMouse(event: MouseEvent): void;
7
8
  }
@@ -46,5 +46,19 @@ class SliderPrompt extends base_1.Prompt {
46
46
  this.render(false);
47
47
  }
48
48
  }
49
+ handleMouse(event) {
50
+ if (event.action === 'scroll') {
51
+ const step = this.options.step || 1;
52
+ if (event.scroll === 'up') { // Scroll Up -> Increase
53
+ this.value = Math.min(this.options.max, this.value + step);
54
+ }
55
+ if (event.scroll === 'down') { // Scroll Down -> Decrease
56
+ this.value = Math.max(this.options.min, this.value - step);
57
+ }
58
+ // Round to avoid float errors
59
+ this.value = Math.round(this.value * 10000) / 10000;
60
+ this.render(false);
61
+ }
62
+ }
49
63
  }
50
64
  exports.SliderPrompt = SliderPrompt;
@@ -77,41 +77,10 @@ class TextPrompt extends base_1.Prompt {
77
77
  visualColIndex = 0;
78
78
  }
79
79
  else {
80
- // Calculate width of this segment?
81
- // No, for simple text editor logic we often assume 1 char = 1 pos unless we do full layout.
82
- // But here we want correct cursor placement over wide chars.
83
- // So we should sum width.
84
- // However, standard terminals handle wide chars by advancing cursor 2 spots.
85
- // So we just need to sum the string length of the segment?
86
- // Or 2 if it's wide?
87
- // Standard terminal behavior:
88
- // If I write an Emoji (2 cols), the cursor advances 2 cols.
89
- // So visualColIndex should track stringWidth(seg).
90
- // But if isPassword, it's '*'. Width 1.
91
80
  if (this.options.isPassword) {
92
81
  visualColIndex += 1;
93
82
  }
94
83
  else {
95
- // Use our helper? Or just length?
96
- // If we used stringWidth, it would be accurate.
97
- // But we don't have access to stringWidth here easily unless we import it again (we did in base).
98
- // Let's assume segment.length for now (byte length),
99
- // because `\x1b[<N>C` moves N COLUMNS? No, N characters?
100
- // ANSI `CUB` / `CUF` moves N *columns* usually?
101
- // "The Cursor Forward (CUF) sequence moves the cursor forward by n columns."
102
- // So if we have an emoji (2 cols), we need to move past it.
103
- // If we print an emoji, cursor is at +2.
104
- // Wait, if we use `renderFrame`, we rewrite everything.
105
- // Then we calculate where to put the cursor.
106
- // If line is "A <Emoji> B".
107
- // Output: "A <Emoji> B".
108
- // If cursor is after Emoji.
109
- // We need to be at position: width("A") + width("<Emoji>").
110
- // = 1 + 2 = 3.
111
- // So `visualColIndex` should use `stringWidth(seg)`.
112
- // But I didn't export `stringWidth` from `utils.ts` in the last step?
113
- // Checking `src/utils.ts`... I did export it.
114
- // But I need to import it here.
115
84
  visualColIndex += this.options.isPassword ? 1 : this.getSegmentWidth(seg);
116
85
  }
117
86
  }
@@ -1,7 +1,8 @@
1
1
  import { Prompt } from '../base';
2
- import { ToggleOptions } from '../types';
2
+ import { ToggleOptions, MouseEvent } from '../types';
3
3
  export declare class TogglePrompt extends Prompt<boolean, ToggleOptions> {
4
4
  constructor(options: ToggleOptions);
5
5
  protected render(firstRender: boolean): void;
6
6
  protected handleInput(char: string): void;
7
+ protected handleMouse(event: MouseEvent): void;
7
8
  }
@@ -37,5 +37,11 @@ class TogglePrompt extends base_1.Prompt {
37
37
  this.render(false);
38
38
  }
39
39
  }
40
+ handleMouse(event) {
41
+ if (event.action === 'scroll') {
42
+ this.value = !this.value;
43
+ this.render(false);
44
+ }
45
+ }
40
46
  }
41
47
  exports.TogglePrompt = TogglePrompt;
package/dist/symbols.d.ts CHANGED
@@ -13,5 +13,9 @@ export interface SymbolDefinition {
13
13
  unchecked: string;
14
14
  /** Animation frames for the spinner */
15
15
  spinner: string[];
16
+ /** Star symbol for rating */
17
+ star: string;
18
+ /** Empty star symbol for rating */
19
+ starEmpty: string;
16
20
  }
17
21
  export declare const symbols: SymbolDefinition;
package/dist/symbols.js CHANGED
@@ -9,7 +9,9 @@ const UnicodeSymbols = {
9
9
  line: '─',
10
10
  checked: '◉',
11
11
  unchecked: '◯',
12
- spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
12
+ spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
13
+ star: '★',
14
+ starEmpty: '☆'
13
15
  };
14
16
  const AsciiSymbols = {
15
17
  tick: '+',
@@ -18,7 +20,9 @@ const AsciiSymbols = {
18
20
  line: '-',
19
21
  checked: '[x]',
20
22
  unchecked: '[ ]',
21
- spinner: ['|', '/', '-', '\\']
23
+ spinner: ['|', '/', '-', '\\'],
24
+ star: '*',
25
+ starEmpty: ' '
22
26
  };
23
27
  const capabilities = (0, utils_1.detectCapabilities)();
24
28
  const useUnicode = capabilities.hasUnicode && process.env.MEP_NO_UNICODE !== '1';
package/dist/types.d.ts CHANGED
@@ -10,6 +10,15 @@ export interface ThemeConfig {
10
10
  }
11
11
  export interface BaseOptions {
12
12
  message: string;
13
+ mouse?: boolean;
14
+ }
15
+ export interface MouseEvent {
16
+ name: 'mouse';
17
+ x: number;
18
+ y: number;
19
+ button: number;
20
+ action: 'press' | 'release' | 'move' | 'scroll';
21
+ scroll?: 'up' | 'down';
13
22
  }
14
23
  export interface TextOptions extends BaseOptions {
15
24
  placeholder?: string;
@@ -65,6 +74,11 @@ export interface SliderOptions extends BaseOptions {
65
74
  step?: number;
66
75
  unit?: string;
67
76
  }
77
+ export interface RatingOptions extends BaseOptions {
78
+ min?: number;
79
+ max?: number;
80
+ initial?: number;
81
+ }
68
82
  export interface DateOptions extends BaseOptions {
69
83
  initial?: Date;
70
84
  min?: Date;
package/dist/utils.d.ts CHANGED
@@ -5,6 +5,7 @@ export declare function detectCapabilities(): {
5
5
  isCI: boolean;
6
6
  hasTrueColor: boolean;
7
7
  hasUnicode: boolean | "";
8
+ hasMouse: boolean;
8
9
  };
9
10
  /**
10
11
  * Strips ANSI escape codes from a string.
package/dist/utils.js CHANGED
@@ -52,7 +52,10 @@ function detectCapabilities() {
52
52
  isCI,
53
53
  hasTrueColor,
54
54
  // Enable Unicode only if it's TTY and environment supports it.
55
- hasUnicode: isTTY && isUnicodeSupported()
55
+ hasUnicode: isTTY && isUnicodeSupported(),
56
+ // Check if mouse should be enabled (TTY and not CI, or explicit override)
57
+ // SGR is widely supported in modern terminals
58
+ hasMouse: isTTY && !isCI
56
59
  };
57
60
  }
58
61
  /**
package/example.ts CHANGED
@@ -96,7 +96,16 @@ async function runComprehensiveDemo() {
96
96
  });
97
97
  console.log(`\n✅ Slider Result: Brightness: ${brightness}%`);
98
98
 
99
- // --- 9. Date / Time Picker (New) ---
99
+ // --- 9. Rating Prompt (New) ---
100
+ const userRating = await MepCLI.rating({
101
+ message: "How would you rate this CLI tool?",
102
+ min: 1,
103
+ max: 5,
104
+ initial: 5
105
+ });
106
+ console.log(`\n✅ Rating Result: You rated it: ${userRating}/5`);
107
+
108
+ // --- 10. Date / Time Picker (New) ---
100
109
  // We capture 'now' once to ensure initial >= min
101
110
  const now = new Date();
102
111
  const releaseDate = await MepCLI.date({
@@ -106,14 +115,14 @@ async function runComprehensiveDemo() {
106
115
  });
107
116
  console.log(`\n✅ Date Result: Release set for: ${releaseDate.toLocaleString()}`);
108
117
 
109
- // --- 10. File Path Selector (New) ---
118
+ // --- 11. File Path Selector (New) ---
110
119
  const configPath = await MepCLI.file({
111
120
  message: "Select configuration file (Tab to autocomplete):",
112
121
  basePath: process.cwd()
113
122
  });
114
123
  console.log(`\n✅ File Result: Path: ${configPath}`);
115
124
 
116
- // --- 11. Multi-Select Autocomplete (New) ---
125
+ // --- 12. Multi-Select Autocomplete (New) ---
117
126
  const linters = await MepCLI.multiSelect({
118
127
  message: "Select linters to install (Type to search, Space to select):",
119
128
  choices: [
@@ -128,14 +137,14 @@ async function runComprehensiveDemo() {
128
137
  });
129
138
  console.log(`\n✅ MultiSelect Result: Linters: [${linters.join(', ')}]`);
130
139
 
131
- // --- 12. Confirm Prompt (Simple Yes/No) ---
140
+ // --- 13. Confirm Prompt (Simple Yes/No) ---
132
141
  const proceed = await MepCLI.confirm({
133
142
  message: "Ready to deploy the project now?",
134
143
  initial: true
135
144
  });
136
145
  console.log(`\n✅ Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
137
146
 
138
- // --- 13. Spin Utility (Loading/Async Task Indicator) ---
147
+ // --- 14. Spin Utility (Loading/Async Task Indicator) ---
139
148
  await MepCLI.spin(
140
149
  "Finalizing configuration and deploying...",
141
150
  new Promise(resolve => setTimeout(resolve, 1500)) // Simulates a 1.5 second async task
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
5
  "repository": {
6
6
  "type": "git",