mepcli 0.4.0 → 0.5.5
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 +46 -4
- package/dist/core.d.ts +10 -3
- package/dist/core.js +28 -24
- package/dist/prompts/autocomplete.d.ts +22 -0
- package/dist/prompts/autocomplete.js +175 -0
- package/dist/prompts/checkbox.js +0 -4
- package/dist/prompts/editor.d.ts +14 -0
- package/dist/prompts/editor.js +207 -0
- package/dist/prompts/file.js +0 -6
- package/dist/prompts/keypress.d.ts +7 -0
- package/dist/prompts/keypress.js +57 -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 +5 -24
- package/dist/prompts/tree.d.ts +20 -0
- package/dist/prompts/tree.js +223 -0
- package/dist/spinner.d.ts +33 -0
- package/dist/spinner.js +89 -0
- package/dist/types.d.ts +39 -0
- package/dist/utils.js +5 -1
- package/example.ts +113 -22
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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`, `multiSelect`, and `
|
|
8
|
+
- **Comprehensive Prompts:** Includes `text`, `password`, `select`, `checkbox`, `confirm`, `number`, `toggle`, `list`, `slider`, `date`, `file`, `multiSelect`, `autocomplete`, `sort`, `table`, `rating`, `editor`, `tree`, and `keypress`.
|
|
9
9
|
- **Mouse Support:** Built-in support for mouse interaction (SGR 1006 protocol). Scroll to navigate lists or change values; click to select.
|
|
10
10
|
- **Responsive Input:** Supports cursor movement (Left/Right) and character insertion/deletion in text-based prompts.
|
|
11
11
|
- **Validation:** Built-in support for input validation (sync and async) with custom error messages.
|
|
@@ -75,7 +75,42 @@ async function main() {
|
|
|
75
75
|
initial: 5
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
|
|
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
|
+
// Editor (External text editor)
|
|
108
|
+
const bio = await MepCLI.editor({
|
|
109
|
+
message: "Write your biography:",
|
|
110
|
+
extension: ".md"
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
console.log({ name, age, newsletter, lang, tools, stars, city, priorities, user, bio });
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
main();
|
|
@@ -98,15 +133,22 @@ main();
|
|
|
98
133
|
* `rating(options)` - Star rating input.
|
|
99
134
|
* `date(options)` - Date and time picker.
|
|
100
135
|
* `file(options)` - File system navigator and selector.
|
|
101
|
-
* `
|
|
136
|
+
* `autocomplete(options)` - Searchable selection with async suggestions.
|
|
137
|
+
* `sort(options)` - Reorder a list of items.
|
|
138
|
+
* `table(options)` - Display data in columns and select rows.
|
|
139
|
+
* `tree(options)` - Navigate and select from a hierarchical tree structure.
|
|
140
|
+
* `keypress(options)` - Wait for a specific key press or any key.
|
|
141
|
+
* `editor(options)` - Launch an external editor (Vim, Nano, Notepad, etc.) to capture multi-line content.
|
|
142
|
+
* `spinner(message)` - Returns a `Spinner` instance for manual control (`start`, `stop`, `update`, `success`, `error`).
|
|
102
143
|
|
|
103
144
|
## Mouse Support
|
|
104
145
|
|
|
105
146
|
MepCLI automatically detects modern terminals and enables **Mouse Tracking** (using SGR 1006 protocol).
|
|
106
147
|
|
|
107
148
|
* **Scrolling:**
|
|
108
|
-
* `select`, `multiSelect`, `checkbox`: Scroll to navigate the list.
|
|
149
|
+
* `select`, `multiSelect`, `checkbox`, `autocomplete`, `table`, `tree`: Scroll to navigate the list.
|
|
109
150
|
* `number`, `slider`, `rating`, `date`: Scroll to increment/decrement values or fields.
|
|
151
|
+
* `sort`: Scroll to navigate or reorder items (when grabbed).
|
|
110
152
|
* `toggle`, `confirm`: Scroll to toggle the state.
|
|
111
153
|
* **Configuration:**
|
|
112
154
|
* Mouse support is enabled by default if the terminal supports it.
|
package/dist/core.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions, RatingOptions } from './types';
|
|
1
|
+
import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions, RatingOptions, AutocompleteOptions, SortOptions, TableOptions, EditorOptions, TreeOptions, KeypressOptions } 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[]>;
|
|
@@ -21,4 +22,10 @@ export declare class MepCLI {
|
|
|
21
22
|
static file(options: FileOptions): Promise<string>;
|
|
22
23
|
static multiSelect<const V>(options: MultiSelectOptions<V>): Promise<V[]>;
|
|
23
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>;
|
|
28
|
+
static editor(options: EditorOptions): Promise<string>;
|
|
29
|
+
static tree<const V>(options: TreeOptions<V>): Promise<V>;
|
|
30
|
+
static keypress(options: KeypressOptions): Promise<string>;
|
|
24
31
|
}
|
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");
|
|
@@ -16,34 +15,21 @@ const date_1 = require("./prompts/date");
|
|
|
16
15
|
const file_1 = require("./prompts/file");
|
|
17
16
|
const multi_select_1 = require("./prompts/multi-select");
|
|
18
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");
|
|
21
|
+
const editor_1 = require("./prompts/editor");
|
|
22
|
+
const tree_1 = require("./prompts/tree");
|
|
23
|
+
const keypress_1 = require("./prompts/keypress");
|
|
19
24
|
/**
|
|
20
25
|
* Public Facade for MepCLI
|
|
21
26
|
*/
|
|
22
27
|
class MepCLI {
|
|
23
28
|
/**
|
|
24
|
-
*
|
|
29
|
+
* Creates a new Spinner instance.
|
|
25
30
|
*/
|
|
26
|
-
static
|
|
27
|
-
|
|
28
|
-
let i = 0;
|
|
29
|
-
process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
|
|
30
|
-
const interval = setInterval(() => {
|
|
31
|
-
process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.main}${frames[i]}${ansi_1.ANSI.RESET} ${message}`);
|
|
32
|
-
i = (i + 1) % frames.length;
|
|
33
|
-
}, 80);
|
|
34
|
-
try {
|
|
35
|
-
const result = await taskPromise;
|
|
36
|
-
clearInterval(interval);
|
|
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`);
|
|
38
|
-
process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
clearInterval(interval);
|
|
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`);
|
|
44
|
-
process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
31
|
+
static spinner(message) {
|
|
32
|
+
return new spinner_1.Spinner(message);
|
|
47
33
|
}
|
|
48
34
|
static text(options) {
|
|
49
35
|
return new text_1.TextPrompt(options).run();
|
|
@@ -84,6 +70,24 @@ class MepCLI {
|
|
|
84
70
|
static rating(options) {
|
|
85
71
|
return new rating_1.RatingPrompt(options).run();
|
|
86
72
|
}
|
|
73
|
+
static autocomplete(options) {
|
|
74
|
+
return new autocomplete_1.AutocompletePrompt(options).run();
|
|
75
|
+
}
|
|
76
|
+
static sort(options) {
|
|
77
|
+
return new sort_1.SortPrompt(options).run();
|
|
78
|
+
}
|
|
79
|
+
static table(options) {
|
|
80
|
+
return new table_1.TablePrompt(options).run();
|
|
81
|
+
}
|
|
82
|
+
static editor(options) {
|
|
83
|
+
return new editor_1.EditorPrompt(options).run();
|
|
84
|
+
}
|
|
85
|
+
static tree(options) {
|
|
86
|
+
return new tree_1.TreePrompt(options).run();
|
|
87
|
+
}
|
|
88
|
+
static keypress(options) {
|
|
89
|
+
return new keypress_1.KeypressPrompt(options).run();
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
92
|
exports.MepCLI = MepCLI;
|
|
89
93
|
MepCLI.theme = theme_1.theme;
|
|
@@ -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;
|
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}`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { EditorOptions } from '../types';
|
|
3
|
+
export declare class EditorPrompt extends Prompt<string, EditorOptions> {
|
|
4
|
+
private errorMsg;
|
|
5
|
+
private status;
|
|
6
|
+
private tempFilePath;
|
|
7
|
+
constructor(options: EditorOptions);
|
|
8
|
+
protected cleanup(): void;
|
|
9
|
+
protected render(firstRender: boolean): void;
|
|
10
|
+
protected handleInput(char: string): void;
|
|
11
|
+
private resolveEditor;
|
|
12
|
+
private spawnEditor;
|
|
13
|
+
private restoreMep;
|
|
14
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EditorPrompt = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const base_1 = require("../base");
|
|
42
|
+
const theme_1 = require("../theme");
|
|
43
|
+
const symbols_1 = require("../symbols");
|
|
44
|
+
const ansi_1 = require("../ansi");
|
|
45
|
+
class EditorPrompt extends base_1.Prompt {
|
|
46
|
+
constructor(options) {
|
|
47
|
+
super(options);
|
|
48
|
+
this.errorMsg = '';
|
|
49
|
+
this.status = 'pending';
|
|
50
|
+
this.tempFilePath = null;
|
|
51
|
+
// Default waitUserInput to true if not specified
|
|
52
|
+
if (this.options.waitUserInput === undefined) {
|
|
53
|
+
this.options.waitUserInput = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
cleanup() {
|
|
57
|
+
if (this.tempFilePath) {
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(this.tempFilePath)) {
|
|
60
|
+
fs.unlinkSync(this.tempFilePath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// Ignore cleanup errors
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
super.cleanup();
|
|
68
|
+
}
|
|
69
|
+
render(firstRender) {
|
|
70
|
+
if (this.status === 'editing') {
|
|
71
|
+
return; // Don't render while editor is open (stdio inherited)
|
|
72
|
+
}
|
|
73
|
+
const icon = this.status === 'done' ? theme_1.theme.success + symbols_1.symbols.tick : theme_1.theme.main + '?';
|
|
74
|
+
const message = `${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}`;
|
|
75
|
+
const hint = this.options.waitUserInput
|
|
76
|
+
? ` ${theme_1.theme.muted}[Press <Enter> to launch editor]${ansi_1.ANSI.RESET}`
|
|
77
|
+
: ` ${theme_1.theme.muted}[Launching editor...]${ansi_1.ANSI.RESET}`;
|
|
78
|
+
let output = `${icon} ${ansi_1.ANSI.BOLD}${message}${ansi_1.ANSI.RESET}${hint}`;
|
|
79
|
+
if (this.errorMsg) {
|
|
80
|
+
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
81
|
+
}
|
|
82
|
+
this.renderFrame(output);
|
|
83
|
+
// Auto-launch handling
|
|
84
|
+
if (firstRender && this.options.waitUserInput === false) {
|
|
85
|
+
// We need to delay slightly to ensure the render frame is flushed
|
|
86
|
+
// and raw mode setup is complete from .run()
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
this.spawnEditor();
|
|
89
|
+
}, 50);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
handleInput(char) {
|
|
93
|
+
if (this.status !== 'pending')
|
|
94
|
+
return;
|
|
95
|
+
// Enter
|
|
96
|
+
if (char === '\r' || char === '\n') {
|
|
97
|
+
this.spawnEditor();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
resolveEditor() {
|
|
101
|
+
// 1. Env vars
|
|
102
|
+
const envEditor = process.env.VISUAL || process.env.EDITOR;
|
|
103
|
+
if (envEditor) {
|
|
104
|
+
const parts = envEditor.split(' ');
|
|
105
|
+
return { cmd: parts[0], args: parts.slice(1) };
|
|
106
|
+
}
|
|
107
|
+
// 2. OS specific
|
|
108
|
+
if (process.platform === 'win32') {
|
|
109
|
+
// Priority: notepad -> code -> wordpad
|
|
110
|
+
return { cmd: 'notepad', args: [] };
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Unix/Linux/Mac
|
|
114
|
+
// Priority: vim -> nano -> vi
|
|
115
|
+
// We'll stick to 'vim' as the default safe bet if we can't detect.
|
|
116
|
+
// A more robust solution would check paths, but for now we assume 'vim'.
|
|
117
|
+
return { cmd: 'vim', args: [] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
spawnEditor() {
|
|
121
|
+
this.status = 'editing';
|
|
122
|
+
// 1. Prepare Temp File
|
|
123
|
+
const ext = this.options.extension || '.txt';
|
|
124
|
+
// Ensure extension has dot
|
|
125
|
+
const safeExt = ext.startsWith('.') ? ext : '.' + ext;
|
|
126
|
+
const filename = `mep-editor-${Date.now()}-${Math.floor(Math.random() * 1000)}${safeExt}`;
|
|
127
|
+
this.tempFilePath = path.join(os.tmpdir(), filename);
|
|
128
|
+
const initialContent = this.options.initial || '';
|
|
129
|
+
try {
|
|
130
|
+
fs.writeFileSync(this.tempFilePath, initialContent, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
this.errorMsg = `Failed to create temp file: ${e.message}`;
|
|
134
|
+
this.status = 'pending';
|
|
135
|
+
this.render(false);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// 2. Resolve Editor
|
|
139
|
+
const { cmd, args } = this.resolveEditor();
|
|
140
|
+
const editorArgs = [...args, this.tempFilePath];
|
|
141
|
+
// 3. Pause Mep
|
|
142
|
+
// Temporarily disable mouse tracking if it was enabled
|
|
143
|
+
const shouldEnableMouse = this.options.mouse !== false && this.capabilities.hasMouse;
|
|
144
|
+
if (shouldEnableMouse) {
|
|
145
|
+
this.print(ansi_1.ANSI.DISABLE_MOUSE);
|
|
146
|
+
}
|
|
147
|
+
// Pause stdin and raw mode to allow child process to take over TTY
|
|
148
|
+
this.stdin.setRawMode(false);
|
|
149
|
+
this.stdin.pause();
|
|
150
|
+
// 4. Spawn
|
|
151
|
+
const child = (0, child_process_1.spawn)(cmd, editorArgs, {
|
|
152
|
+
stdio: 'inherit',
|
|
153
|
+
shell: true
|
|
154
|
+
});
|
|
155
|
+
child.on('error', (err) => {
|
|
156
|
+
this.restoreMep();
|
|
157
|
+
this.status = 'pending';
|
|
158
|
+
this.errorMsg = `Could not launch editor '${cmd}': ${err.message}`;
|
|
159
|
+
this.render(false);
|
|
160
|
+
});
|
|
161
|
+
child.on('exit', (code) => {
|
|
162
|
+
// 5. Read Result
|
|
163
|
+
let content = initialContent;
|
|
164
|
+
try {
|
|
165
|
+
if (this.tempFilePath && fs.existsSync(this.tempFilePath)) {
|
|
166
|
+
content = fs.readFileSync(this.tempFilePath, 'utf8');
|
|
167
|
+
fs.unlinkSync(this.tempFilePath); // Cleanup
|
|
168
|
+
this.tempFilePath = null; // Mark as cleaned
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
// Ignore read/delete errors
|
|
173
|
+
}
|
|
174
|
+
this.restoreMep();
|
|
175
|
+
if (code !== 0) {
|
|
176
|
+
this.status = 'pending';
|
|
177
|
+
this.errorMsg = `Editor exited with code ${code}`;
|
|
178
|
+
this.render(false);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Success
|
|
182
|
+
this.status = 'done';
|
|
183
|
+
// Trim trailing newline which editors often add
|
|
184
|
+
// We only trim the *last* newline added by the editor if it wasn't there?
|
|
185
|
+
// Usually editors ensure a final newline.
|
|
186
|
+
// If the user entered "abc", vim saves "abc\n". We probably want "abc".
|
|
187
|
+
if (content.endsWith('\n')) {
|
|
188
|
+
content = content.slice(0, -1);
|
|
189
|
+
}
|
|
190
|
+
if (content.endsWith('\r')) {
|
|
191
|
+
content = content.slice(0, -1);
|
|
192
|
+
}
|
|
193
|
+
this.submit(content);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
restoreMep() {
|
|
197
|
+
this.stdin.resume();
|
|
198
|
+
this.stdin.setRawMode(true);
|
|
199
|
+
// Re-enable mouse if it was enabled
|
|
200
|
+
const shouldEnableMouse = this.options.mouse !== false && this.capabilities.hasMouse;
|
|
201
|
+
if (shouldEnableMouse) {
|
|
202
|
+
this.print(ansi_1.ANSI.SET_ANY_EVENT_MOUSE + ansi_1.ANSI.SET_SGR_EXT_MODE_MOUSE);
|
|
203
|
+
}
|
|
204
|
+
this.print(ansi_1.ANSI.HIDE_CURSOR); // Ensure cursor is hidden again for Mep
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
exports.EditorPrompt = EditorPrompt;
|
package/dist/prompts/file.js
CHANGED
|
@@ -131,12 +131,6 @@ class FilePrompt extends base_1.Prompt {
|
|
|
131
131
|
// Move right
|
|
132
132
|
const prefix = `${icon} ${theme_1.theme.title}${this.options.message} `;
|
|
133
133
|
const prefixLen = this.stripAnsi(prefix).length;
|
|
134
|
-
// Cursor is usually at the end of input unless we add backspace support etc.
|
|
135
|
-
// The cursor property tracks it, but my handleInput simplified it.
|
|
136
|
-
// Let's rely on this.input.length for now since handleInput appends.
|
|
137
|
-
// Ah, handleInput logic below supports cursor pos theoretically but I only see appending?
|
|
138
|
-
// Actually handleInput doesn't support left/right in the original code, it supports down/up for suggestions.
|
|
139
|
-
// So cursor is always at end.
|
|
140
134
|
const targetCol = prefixLen + this.input.length;
|
|
141
135
|
this.print(ansi_1.ANSI.CURSOR_LEFT);
|
|
142
136
|
if (targetCol > 0)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { KeypressOptions } from '../types';
|
|
3
|
+
export declare class KeypressPrompt extends Prompt<string, KeypressOptions> {
|
|
4
|
+
constructor(options: KeypressOptions);
|
|
5
|
+
protected render(firstRender: boolean): void;
|
|
6
|
+
protected handleInput(char: string, key: Buffer): void;
|
|
7
|
+
}
|