mepcli 0.3.0 → 0.5.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 +59 -4
- package/dist/ansi.d.ts +3 -0
- package/dist/ansi.js +4 -0
- package/dist/base.d.ts +6 -0
- package/dist/base.js +18 -0
- package/dist/core.d.ts +8 -3
- package/dist/core.js +20 -24
- package/dist/input.d.ts +1 -0
- package/dist/input.js +62 -8
- package/dist/prompts/autocomplete.d.ts +22 -0
- package/dist/prompts/autocomplete.js +175 -0
- package/dist/prompts/checkbox.d.ts +2 -1
- package/dist/prompts/checkbox.js +14 -4
- package/dist/prompts/confirm.d.ts +2 -1
- package/dist/prompts/confirm.js +6 -0
- package/dist/prompts/date.d.ts +3 -1
- package/dist/prompts/date.js +62 -31
- package/dist/prompts/file.js +0 -6
- package/dist/prompts/multi-select.d.ts +2 -1
- package/dist/prompts/multi-select.js +15 -0
- package/dist/prompts/number.d.ts +2 -1
- package/dist/prompts/number.js +22 -0
- package/dist/prompts/rating.d.ts +8 -0
- package/dist/prompts/rating.js +78 -0
- package/dist/prompts/select.d.ts +2 -1
- package/dist/prompts/select.js +15 -0
- package/dist/prompts/slider.d.ts +2 -1
- package/dist/prompts/slider.js +14 -0
- package/dist/prompts/sort.d.ts +14 -0
- package/dist/prompts/sort.js +160 -0
- package/dist/prompts/table.d.ts +14 -0
- package/dist/prompts/table.js +106 -0
- package/dist/prompts/text.js +0 -50
- package/dist/prompts/toggle.d.ts +2 -1
- package/dist/prompts/toggle.js +6 -0
- package/dist/spinner.d.ts +33 -0
- package/dist/spinner.js +89 -0
- package/dist/symbols.d.ts +4 -0
- package/dist/symbols.js +6 -2
- package/dist/types.d.ts +32 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +4 -1
- package/example.ts +60 -11
- package/package.json +1 -1
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 `
|
|
8
|
+
- **Comprehensive Prompts:** Includes `text`, `password`, `select`, `checkbox`, `confirm`, `number`, `toggle`, `list`, `slider`, `date`, `file`, `multiSelect`, `autocomplete`, `sort`, `table`, 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
|
-
```
|
|
16
|
+
```shell
|
|
16
17
|
npm install mepcli
|
|
17
18
|
# or
|
|
18
19
|
yarn add mepcli
|
|
@@ -66,7 +67,44 @@ async function main() {
|
|
|
66
67
|
]
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
|
|
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
|
+
// Autocomplete (Search)
|
|
79
|
+
const city = await MepCLI.autocomplete({
|
|
80
|
+
message: "Search for a city:",
|
|
81
|
+
suggest: async (query) => {
|
|
82
|
+
const cities = [
|
|
83
|
+
{ title: "New York", value: "NY" },
|
|
84
|
+
{ title: "London", value: "LDN" },
|
|
85
|
+
{ title: "Paris", value: "PAR" }
|
|
86
|
+
];
|
|
87
|
+
return cities.filter(c => c.title.toLowerCase().includes(query.toLowerCase()));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Sort (Drag & Drop)
|
|
92
|
+
const priorities = await MepCLI.sort({
|
|
93
|
+
message: "Rank priorities:",
|
|
94
|
+
items: ["Speed", "Quality", "Cost"]
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Table (Data selection)
|
|
98
|
+
const user = await MepCLI.table({
|
|
99
|
+
message: "Select a user:",
|
|
100
|
+
columns: ["ID", "Name"],
|
|
101
|
+
data: [
|
|
102
|
+
{ value: 1, row: ["001", "Alice"] },
|
|
103
|
+
{ value: 2, row: ["002", "Bob"] }
|
|
104
|
+
]
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log({ name, age, newsletter, lang, tools, stars, city, priorities, user });
|
|
70
108
|
}
|
|
71
109
|
|
|
72
110
|
main();
|
|
@@ -86,9 +124,26 @@ main();
|
|
|
86
124
|
* `checkbox(options)` - Classic checkbox selection.
|
|
87
125
|
* `list(options)` - Enter a list of tags/strings.
|
|
88
126
|
* `slider(options)` - Select a number within a range using a visual slider.
|
|
127
|
+
* `rating(options)` - Star rating input.
|
|
89
128
|
* `date(options)` - Date and time picker.
|
|
90
129
|
* `file(options)` - File system navigator and selector.
|
|
91
|
-
* `
|
|
130
|
+
* `autocomplete(options)` - Searchable selection with async suggestions.
|
|
131
|
+
* `sort(options)` - Reorder a list of items.
|
|
132
|
+
* `table(options)` - Display data in columns and select rows.
|
|
133
|
+
* `spinner(message)` - Returns a `Spinner` instance for manual control (`start`, `stop`, `update`, `success`, `error`).
|
|
134
|
+
|
|
135
|
+
## Mouse Support
|
|
136
|
+
|
|
137
|
+
MepCLI automatically detects modern terminals and enables **Mouse Tracking** (using SGR 1006 protocol).
|
|
138
|
+
|
|
139
|
+
* **Scrolling:**
|
|
140
|
+
* `select`, `multiSelect`, `checkbox`, `autocomplete`, `table`: Scroll to navigate the list.
|
|
141
|
+
* `number`, `slider`, `rating`, `date`: Scroll to increment/decrement values or fields.
|
|
142
|
+
* `sort`: Scroll to navigate or reorder items (when grabbed).
|
|
143
|
+
* `toggle`, `confirm`: Scroll to toggle the state.
|
|
144
|
+
* **Configuration:**
|
|
145
|
+
* Mouse support is enabled by default if the terminal supports it.
|
|
146
|
+
* You can explicitly disable it per prompt by setting `mouse: false` in the options.
|
|
92
147
|
|
|
93
148
|
## License
|
|
94
149
|
|
package/dist/ansi.d.ts
CHANGED
package/dist/ansi.js
CHANGED
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,13 +1,14 @@
|
|
|
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, AutocompleteOptions, SortOptions, TableOptions } from './types';
|
|
2
|
+
import { Spinner } from './spinner';
|
|
2
3
|
/**
|
|
3
4
|
* Public Facade for MepCLI
|
|
4
5
|
*/
|
|
5
6
|
export declare class MepCLI {
|
|
6
7
|
static theme: ThemeConfig;
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Creates a new Spinner instance.
|
|
9
10
|
*/
|
|
10
|
-
static
|
|
11
|
+
static spinner(message: string): Spinner;
|
|
11
12
|
static text(options: TextOptions): Promise<string>;
|
|
12
13
|
static select<const V>(options: SelectOptions<V>): Promise<V>;
|
|
13
14
|
static checkbox<const V>(options: CheckboxOptions<V>): Promise<V[]>;
|
|
@@ -20,4 +21,8 @@ export declare class MepCLI {
|
|
|
20
21
|
static date(options: DateOptions): Promise<Date>;
|
|
21
22
|
static file(options: FileOptions): Promise<string>;
|
|
22
23
|
static multiSelect<const V>(options: MultiSelectOptions<V>): Promise<V[]>;
|
|
24
|
+
static rating(options: RatingOptions): Promise<number>;
|
|
25
|
+
static autocomplete<const V>(options: AutocompleteOptions<V>): Promise<V>;
|
|
26
|
+
static sort(options: SortOptions): Promise<string[]>;
|
|
27
|
+
static table<const V>(options: TableOptions<V>): Promise<V>;
|
|
23
28
|
}
|
package/dist/core.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MepCLI = void 0;
|
|
4
|
-
const ansi_1 = require("./ansi");
|
|
5
4
|
const theme_1 = require("./theme");
|
|
6
|
-
const
|
|
5
|
+
const spinner_1 = require("./spinner");
|
|
7
6
|
const text_1 = require("./prompts/text");
|
|
8
7
|
const select_1 = require("./prompts/select");
|
|
9
8
|
const checkbox_1 = require("./prompts/checkbox");
|
|
@@ -15,34 +14,19 @@ const slider_1 = require("./prompts/slider");
|
|
|
15
14
|
const date_1 = require("./prompts/date");
|
|
16
15
|
const file_1 = require("./prompts/file");
|
|
17
16
|
const multi_select_1 = require("./prompts/multi-select");
|
|
17
|
+
const rating_1 = require("./prompts/rating");
|
|
18
|
+
const autocomplete_1 = require("./prompts/autocomplete");
|
|
19
|
+
const sort_1 = require("./prompts/sort");
|
|
20
|
+
const table_1 = require("./prompts/table");
|
|
18
21
|
/**
|
|
19
22
|
* Public Facade for MepCLI
|
|
20
23
|
*/
|
|
21
24
|
class MepCLI {
|
|
22
25
|
/**
|
|
23
|
-
*
|
|
26
|
+
* Creates a new Spinner instance.
|
|
24
27
|
*/
|
|
25
|
-
static
|
|
26
|
-
|
|
27
|
-
let i = 0;
|
|
28
|
-
process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
|
|
29
|
-
const interval = setInterval(() => {
|
|
30
|
-
process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.main}${frames[i]}${ansi_1.ANSI.RESET} ${message}`);
|
|
31
|
-
i = (i + 1) % frames.length;
|
|
32
|
-
}, 80);
|
|
33
|
-
try {
|
|
34
|
-
const result = await taskPromise;
|
|
35
|
-
clearInterval(interval);
|
|
36
|
-
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`);
|
|
37
|
-
process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
clearInterval(interval);
|
|
42
|
-
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`);
|
|
43
|
-
process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
28
|
+
static spinner(message) {
|
|
29
|
+
return new spinner_1.Spinner(message);
|
|
46
30
|
}
|
|
47
31
|
static text(options) {
|
|
48
32
|
return new text_1.TextPrompt(options).run();
|
|
@@ -80,6 +64,18 @@ class MepCLI {
|
|
|
80
64
|
static multiSelect(options) {
|
|
81
65
|
return new multi_select_1.MultiSelectPrompt(options).run();
|
|
82
66
|
}
|
|
67
|
+
static rating(options) {
|
|
68
|
+
return new rating_1.RatingPrompt(options).run();
|
|
69
|
+
}
|
|
70
|
+
static autocomplete(options) {
|
|
71
|
+
return new autocomplete_1.AutocompletePrompt(options).run();
|
|
72
|
+
}
|
|
73
|
+
static sort(options) {
|
|
74
|
+
return new sort_1.SortPrompt(options).run();
|
|
75
|
+
}
|
|
76
|
+
static table(options) {
|
|
77
|
+
return new table_1.TablePrompt(options).run();
|
|
78
|
+
}
|
|
83
79
|
}
|
|
84
80
|
exports.MepCLI = MepCLI;
|
|
85
81
|
MepCLI.theme = theme_1.theme;
|
package/dist/input.d.ts
CHANGED
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
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { AutocompleteOptions, MouseEvent } from '../types';
|
|
3
|
+
export declare class AutocompletePrompt<V> extends Prompt<V, AutocompleteOptions<V>> {
|
|
4
|
+
private input;
|
|
5
|
+
private choices;
|
|
6
|
+
private selectedIndex;
|
|
7
|
+
private loading;
|
|
8
|
+
private debounceTimer?;
|
|
9
|
+
private spinnerFrame;
|
|
10
|
+
private spinnerTimer?;
|
|
11
|
+
private scrollTop;
|
|
12
|
+
private readonly pageSize;
|
|
13
|
+
private hasSearched;
|
|
14
|
+
constructor(options: AutocompleteOptions<V>);
|
|
15
|
+
private search;
|
|
16
|
+
private startSpinner;
|
|
17
|
+
private stopSpinner;
|
|
18
|
+
protected cleanup(): void;
|
|
19
|
+
protected render(firstRender: boolean): void;
|
|
20
|
+
protected handleInput(char: string): void;
|
|
21
|
+
protected handleMouse(event: MouseEvent): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AutocompletePrompt = 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
|
+
const utils_1 = require("../utils");
|
|
9
|
+
class AutocompletePrompt extends base_1.Prompt {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.input = '';
|
|
13
|
+
this.choices = [];
|
|
14
|
+
this.selectedIndex = 0;
|
|
15
|
+
this.loading = false;
|
|
16
|
+
this.spinnerFrame = 0;
|
|
17
|
+
this.scrollTop = 0;
|
|
18
|
+
this.pageSize = 7;
|
|
19
|
+
this.hasSearched = false;
|
|
20
|
+
this.input = options.initial || '';
|
|
21
|
+
this.search(this.input);
|
|
22
|
+
}
|
|
23
|
+
search(query) {
|
|
24
|
+
if (this.debounceTimer)
|
|
25
|
+
clearTimeout(this.debounceTimer);
|
|
26
|
+
this.loading = true;
|
|
27
|
+
this.startSpinner();
|
|
28
|
+
this.render(false);
|
|
29
|
+
this.debounceTimer = setTimeout(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const results = await this.options.suggest(query);
|
|
32
|
+
if (query !== this.input)
|
|
33
|
+
return;
|
|
34
|
+
this.choices = results.slice(0, this.options.limit || 10);
|
|
35
|
+
this.selectedIndex = 0;
|
|
36
|
+
this.scrollTop = 0;
|
|
37
|
+
this.hasSearched = true;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
this.choices = [];
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
this.loading = false;
|
|
44
|
+
this.stopSpinner();
|
|
45
|
+
this.render(false);
|
|
46
|
+
}
|
|
47
|
+
}, 300); // 300ms debounce
|
|
48
|
+
}
|
|
49
|
+
startSpinner() {
|
|
50
|
+
if (this.spinnerTimer)
|
|
51
|
+
return;
|
|
52
|
+
this.spinnerTimer = setInterval(() => {
|
|
53
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % symbols_1.symbols.spinner.length;
|
|
54
|
+
this.render(false);
|
|
55
|
+
}, 80);
|
|
56
|
+
}
|
|
57
|
+
stopSpinner() {
|
|
58
|
+
if (this.spinnerTimer) {
|
|
59
|
+
clearInterval(this.spinnerTimer);
|
|
60
|
+
this.spinnerTimer = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
cleanup() {
|
|
64
|
+
this.stopSpinner();
|
|
65
|
+
super.cleanup();
|
|
66
|
+
}
|
|
67
|
+
render(firstRender) {
|
|
68
|
+
// --- FIX START: Restore cursor position ---
|
|
69
|
+
// renderFrame() in Base assumes the cursor is at the bottom of the output.
|
|
70
|
+
// Since Autocomplete moves the cursor to the top (for input) after rendering,
|
|
71
|
+
// we must manually move it back down before the next render cycle.
|
|
72
|
+
if (this.lastRenderHeight > 1) {
|
|
73
|
+
this.print(`\x1b[${this.lastRenderHeight - 1}B`); // Move down
|
|
74
|
+
}
|
|
75
|
+
// --- FIX END ---
|
|
76
|
+
let output = '';
|
|
77
|
+
// Header
|
|
78
|
+
const icon = this.loading ? `${theme_1.theme.main}${symbols_1.symbols.spinner[this.spinnerFrame]}${ansi_1.ANSI.RESET}` : `${theme_1.theme.success}?${ansi_1.ANSI.RESET}`;
|
|
79
|
+
output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${this.input}`;
|
|
80
|
+
output += '\n';
|
|
81
|
+
if (this.loading) {
|
|
82
|
+
output += ` ${theme_1.theme.muted}Searching...${ansi_1.ANSI.RESET}`;
|
|
83
|
+
}
|
|
84
|
+
else if (this.choices.length === 0 && this.hasSearched) {
|
|
85
|
+
output += ` ${theme_1.theme.muted}${this.options.fallback || 'No results found.'}${ansi_1.ANSI.RESET}`;
|
|
86
|
+
}
|
|
87
|
+
else if (this.choices.length > 0) {
|
|
88
|
+
// Render list similar to SelectPrompt
|
|
89
|
+
if (this.selectedIndex < this.scrollTop) {
|
|
90
|
+
this.scrollTop = this.selectedIndex;
|
|
91
|
+
}
|
|
92
|
+
else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
|
|
93
|
+
this.scrollTop = this.selectedIndex - this.pageSize + 1;
|
|
94
|
+
}
|
|
95
|
+
const visibleChoices = this.choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
|
|
96
|
+
visibleChoices.forEach((choice, index) => {
|
|
97
|
+
const actualIndex = this.scrollTop + index;
|
|
98
|
+
if (index > 0)
|
|
99
|
+
output += '\n';
|
|
100
|
+
if (actualIndex === this.selectedIndex) {
|
|
101
|
+
output += `${theme_1.theme.main}${symbols_1.symbols.pointer} ${choice.title}${ansi_1.ANSI.RESET}`;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
output += ` ${choice.title}`;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
this.renderFrame(output);
|
|
109
|
+
// Position cursor at the end of the input string on the first line
|
|
110
|
+
const lines = output.split('\n');
|
|
111
|
+
const height = lines.length;
|
|
112
|
+
// Move up (height - 1)
|
|
113
|
+
if (height > 1) {
|
|
114
|
+
this.print(`\x1b[${height - 1}A`);
|
|
115
|
+
}
|
|
116
|
+
this.print(`\r`);
|
|
117
|
+
const firstLine = lines[0];
|
|
118
|
+
const visualLen = (0, utils_1.stringWidth)(this.stripAnsi(firstLine));
|
|
119
|
+
this.print(`\x1b[${visualLen}C`);
|
|
120
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
121
|
+
}
|
|
122
|
+
handleInput(char) {
|
|
123
|
+
// Enter
|
|
124
|
+
if (char === '\r' || char === '\n') {
|
|
125
|
+
if (this.choices.length > 0) {
|
|
126
|
+
this.submit(this.choices[this.selectedIndex].value);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Backspace
|
|
131
|
+
if (char === '\u0008' || char === '\x7f') {
|
|
132
|
+
if (this.input.length > 0) {
|
|
133
|
+
this.input = this.input.slice(0, -1);
|
|
134
|
+
this.search(this.input);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Up
|
|
139
|
+
if (this.isUp(char)) {
|
|
140
|
+
if (this.choices.length > 0) {
|
|
141
|
+
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.choices.length - 1;
|
|
142
|
+
this.render(false);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Down
|
|
147
|
+
if (this.isDown(char)) {
|
|
148
|
+
if (this.choices.length > 0) {
|
|
149
|
+
this.selectedIndex = this.selectedIndex < this.choices.length - 1 ? this.selectedIndex + 1 : 0;
|
|
150
|
+
this.render(false);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Typing
|
|
155
|
+
if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
|
|
156
|
+
this.input += char;
|
|
157
|
+
this.search(this.input);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
handleMouse(event) {
|
|
161
|
+
if (this.choices.length === 0)
|
|
162
|
+
return;
|
|
163
|
+
if (event.action === 'scroll') {
|
|
164
|
+
if (event.scroll === 'up') {
|
|
165
|
+
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.choices.length - 1;
|
|
166
|
+
this.render(false);
|
|
167
|
+
}
|
|
168
|
+
else if (event.scroll === 'down') {
|
|
169
|
+
this.selectedIndex = this.selectedIndex < this.choices.length - 1 ? this.selectedIndex + 1 : 0;
|
|
170
|
+
this.render(false);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.AutocompletePrompt = AutocompletePrompt;
|
|
@@ -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
|
}
|
package/dist/prompts/checkbox.js
CHANGED
|
@@ -51,10 +51,6 @@ class CheckboxPrompt extends base_1.Prompt {
|
|
|
51
51
|
// Indication of more items
|
|
52
52
|
if (choices.length > this.pageSize) {
|
|
53
53
|
const progress = ` ${this.scrollTop + 1}-${Math.min(this.scrollTop + this.pageSize, choices.length)} of ${choices.length}`;
|
|
54
|
-
// Maybe add this to the header or footer?
|
|
55
|
-
// Let's add it to footer or header. Adding to header is cleaner.
|
|
56
|
-
// But I already wrote header.
|
|
57
|
-
// Let's just append it at the bottom if I want, or ignore for now to keep UI minimal.
|
|
58
54
|
}
|
|
59
55
|
if (this.errorMsg) {
|
|
60
56
|
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
@@ -111,5 +107,19 @@ class CheckboxPrompt extends base_1.Prompt {
|
|
|
111
107
|
this.render(false);
|
|
112
108
|
}
|
|
113
109
|
}
|
|
110
|
+
handleMouse(event) {
|
|
111
|
+
if (event.action === 'scroll') {
|
|
112
|
+
if (event.scroll === 'up') {
|
|
113
|
+
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.choices.length - 1;
|
|
114
|
+
this.errorMsg = '';
|
|
115
|
+
this.render(false);
|
|
116
|
+
}
|
|
117
|
+
else if (event.scroll === 'down') {
|
|
118
|
+
this.selectedIndex = this.selectedIndex < this.options.choices.length - 1 ? this.selectedIndex + 1 : 0;
|
|
119
|
+
this.errorMsg = '';
|
|
120
|
+
this.render(false);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
114
124
|
}
|
|
115
125
|
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
|
}
|
package/dist/prompts/confirm.js
CHANGED
package/dist/prompts/date.d.ts
CHANGED
|
@@ -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
|
}
|