mepcli 0.5.0 → 0.6.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 +182 -6
- package/dist/ansi.d.ts +1 -0
- package/dist/ansi.js +1 -0
- package/dist/base.d.ts +1 -1
- package/dist/base.js +1 -10
- package/dist/core.d.ts +26 -1
- package/dist/core.js +72 -0
- package/dist/highlight.d.ts +1 -0
- package/dist/highlight.js +40 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/input.js +26 -14
- package/dist/prompts/autocomplete.d.ts +1 -1
- package/dist/prompts/autocomplete.js +2 -7
- package/dist/prompts/calendar.d.ts +20 -0
- package/dist/prompts/calendar.js +329 -0
- package/dist/prompts/checkbox.d.ts +1 -1
- package/dist/prompts/checkbox.js +38 -8
- package/dist/prompts/code.d.ts +17 -0
- package/dist/prompts/code.js +210 -0
- package/dist/prompts/color.d.ts +14 -0
- package/dist/prompts/color.js +147 -0
- package/dist/prompts/confirm.d.ts +1 -1
- package/dist/prompts/confirm.js +1 -1
- package/dist/prompts/cron.d.ts +13 -0
- package/dist/prompts/cron.js +176 -0
- package/dist/prompts/date.d.ts +1 -1
- package/dist/prompts/date.js +15 -5
- package/dist/prompts/editor.d.ts +14 -0
- package/dist/prompts/editor.js +207 -0
- package/dist/prompts/file.d.ts +7 -0
- package/dist/prompts/file.js +56 -60
- package/dist/prompts/form.d.ts +17 -0
- package/dist/prompts/form.js +225 -0
- package/dist/prompts/grid.d.ts +14 -0
- package/dist/prompts/grid.js +178 -0
- package/dist/prompts/keypress.d.ts +7 -0
- package/dist/prompts/keypress.js +57 -0
- package/dist/prompts/list.d.ts +1 -1
- package/dist/prompts/list.js +42 -22
- package/dist/prompts/multi-select.d.ts +1 -1
- package/dist/prompts/multi-select.js +39 -4
- package/dist/prompts/number.d.ts +1 -1
- package/dist/prompts/number.js +2 -2
- package/dist/prompts/range.d.ts +9 -0
- package/dist/prompts/range.js +140 -0
- package/dist/prompts/rating.d.ts +1 -1
- package/dist/prompts/rating.js +1 -1
- package/dist/prompts/select.d.ts +1 -1
- package/dist/prompts/select.js +1 -1
- package/dist/prompts/slider.d.ts +1 -1
- package/dist/prompts/slider.js +1 -1
- package/dist/prompts/snippet.d.ts +18 -0
- package/dist/prompts/snippet.js +203 -0
- package/dist/prompts/sort.d.ts +1 -1
- package/dist/prompts/sort.js +1 -4
- package/dist/prompts/spam.d.ts +17 -0
- package/dist/prompts/spam.js +62 -0
- package/dist/prompts/table.d.ts +1 -1
- package/dist/prompts/table.js +1 -1
- package/dist/prompts/text.d.ts +1 -0
- package/dist/prompts/text.js +14 -32
- package/dist/prompts/toggle.d.ts +1 -1
- package/dist/prompts/toggle.js +1 -1
- package/dist/prompts/transfer.d.ts +18 -0
- package/dist/prompts/transfer.js +203 -0
- package/dist/prompts/tree-select.d.ts +32 -0
- package/dist/prompts/tree-select.js +277 -0
- package/dist/prompts/tree.d.ts +20 -0
- package/dist/prompts/tree.js +231 -0
- package/dist/prompts/wait.d.ts +18 -0
- package/dist/prompts/wait.js +62 -0
- package/dist/types.d.ts +105 -0
- package/dist/utils.js +6 -2
- package/example.ts +213 -27
- package/package.json +14 -4
package/dist/prompts/file.js
CHANGED
|
@@ -40,7 +40,9 @@ const theme_1 = require("../theme");
|
|
|
40
40
|
const symbols_1 = require("../symbols");
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Implementation of FilePrompt with autocomplete.
|
|
45
|
+
*/
|
|
44
46
|
class FilePrompt extends base_1.Prompt {
|
|
45
47
|
constructor(options) {
|
|
46
48
|
super(options);
|
|
@@ -49,22 +51,29 @@ class FilePrompt extends base_1.Prompt {
|
|
|
49
51
|
this.suggestions = [];
|
|
50
52
|
this.selectedSuggestion = -1;
|
|
51
53
|
this.errorMsg = '';
|
|
54
|
+
this.lastLinesUp = 0;
|
|
52
55
|
this.input = options.basePath || '';
|
|
53
56
|
this.cursor = this.input.length;
|
|
57
|
+
this.updateSuggestions();
|
|
54
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Updates the suggestions list based on the current input path.
|
|
61
|
+
*/
|
|
55
62
|
updateSuggestions() {
|
|
56
63
|
try {
|
|
57
|
-
|
|
58
|
-
const
|
|
64
|
+
// Determine the directory to scan and the partial file name
|
|
65
|
+
const isDirQuery = this.input.endsWith('/') || this.input.endsWith('\\');
|
|
66
|
+
const dir = isDirQuery ? this.input : (path.dirname(this.input) || '.');
|
|
67
|
+
const partial = isDirQuery ? '' : path.basename(this.input);
|
|
59
68
|
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
|
|
60
69
|
const files = fs.readdirSync(dir);
|
|
61
70
|
this.suggestions = files
|
|
62
|
-
.filter(f => f.startsWith(partial))
|
|
71
|
+
.filter(f => f.toLowerCase().startsWith(partial.toLowerCase()))
|
|
63
72
|
.filter(f => {
|
|
64
73
|
const fullPath = path.join(dir, f);
|
|
65
|
-
// Handle errors if file doesn't exist or permission denied
|
|
66
74
|
try {
|
|
67
|
-
const
|
|
75
|
+
const stats = fs.statSync(fullPath);
|
|
76
|
+
const isDir = stats.isDirectory();
|
|
68
77
|
if (this.options.onlyDirectories && !isDir)
|
|
69
78
|
return false;
|
|
70
79
|
if (this.options.extensions && !isDir) {
|
|
@@ -72,17 +81,18 @@ class FilePrompt extends base_1.Prompt {
|
|
|
72
81
|
}
|
|
73
82
|
return true;
|
|
74
83
|
}
|
|
75
|
-
catch (
|
|
84
|
+
catch (_e) {
|
|
76
85
|
return false;
|
|
77
86
|
}
|
|
78
87
|
})
|
|
79
88
|
.map(f => {
|
|
80
89
|
const fullPath = path.join(dir, f);
|
|
81
90
|
try {
|
|
91
|
+
// Append separator if the file is a directory
|
|
82
92
|
if (fs.statSync(fullPath).isDirectory())
|
|
83
|
-
return f +
|
|
93
|
+
return f + path.sep;
|
|
84
94
|
}
|
|
85
|
-
catch (
|
|
95
|
+
catch (_e) { /* ignore */ }
|
|
86
96
|
return f;
|
|
87
97
|
});
|
|
88
98
|
}
|
|
@@ -90,18 +100,21 @@ class FilePrompt extends base_1.Prompt {
|
|
|
90
100
|
this.suggestions = [];
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
|
-
catch (
|
|
103
|
+
catch (_e) {
|
|
94
104
|
this.suggestions = [];
|
|
95
105
|
}
|
|
96
106
|
this.selectedSuggestion = -1;
|
|
97
107
|
}
|
|
98
108
|
render(firstRender) {
|
|
99
|
-
//
|
|
109
|
+
// Restore cursor position to the bottom before renderFrame clears the area
|
|
110
|
+
if (!firstRender && this.lastLinesUp > 0) {
|
|
111
|
+
this.print(`\x1b[${this.lastLinesUp}B`);
|
|
112
|
+
this.lastLinesUp = 0;
|
|
113
|
+
}
|
|
100
114
|
const icon = this.errorMsg ? `${theme_1.theme.error}${symbols_1.symbols.cross}` : `${theme_1.theme.success}?`;
|
|
101
115
|
let output = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${this.input}`;
|
|
102
|
-
// Suggestions
|
|
103
116
|
if (this.suggestions.length > 0) {
|
|
104
|
-
output += '\n';
|
|
117
|
+
output += '\n';
|
|
105
118
|
const maxShow = 5;
|
|
106
119
|
const displayed = this.suggestions.slice(0, maxShow);
|
|
107
120
|
displayed.forEach((s, i) => {
|
|
@@ -115,86 +128,69 @@ class FilePrompt extends base_1.Prompt {
|
|
|
115
128
|
}
|
|
116
129
|
});
|
|
117
130
|
if (this.suggestions.length > maxShow) {
|
|
118
|
-
output += `\n ...and ${this.suggestions.length - maxShow} more`;
|
|
131
|
+
output += `\n ${theme_1.theme.muted}...and ${this.suggestions.length - maxShow} more${ansi_1.ANSI.RESET}`;
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
this.renderFrame(output);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// We need to move up to the input line if we printed suggestions.
|
|
125
|
-
// The input line is always the first line (index 0).
|
|
126
|
-
// So we move up by (totalLines - 1).
|
|
127
|
-
const totalLines = this.lastRenderHeight; // renderFrame sets this
|
|
135
|
+
// Move cursor back up to the input line
|
|
136
|
+
const totalLines = this.lastRenderHeight;
|
|
128
137
|
if (totalLines > 1) {
|
|
129
|
-
this.
|
|
138
|
+
this.lastLinesUp = totalLines - 1;
|
|
139
|
+
this.print(`\x1b[${this.lastLinesUp}A`);
|
|
130
140
|
}
|
|
131
|
-
//
|
|
141
|
+
// Calculate horizontal cursor position on the input line
|
|
132
142
|
const prefix = `${icon} ${theme_1.theme.title}${this.options.message} `;
|
|
133
143
|
const prefixLen = this.stripAnsi(prefix).length;
|
|
134
144
|
const targetCol = prefixLen + this.input.length;
|
|
135
145
|
this.print(ansi_1.ANSI.CURSOR_LEFT);
|
|
136
146
|
if (targetCol > 0)
|
|
137
147
|
this.print(`\x1b[${targetCol}C`);
|
|
148
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
138
149
|
}
|
|
139
150
|
handleInput(char) {
|
|
140
|
-
if (char === '\t') {
|
|
141
|
-
if (this.suggestions.length
|
|
142
|
-
|
|
143
|
-
|
|
151
|
+
if (char === '\t') {
|
|
152
|
+
if (this.suggestions.length > 0) {
|
|
153
|
+
// Use the selected suggestion or the first one available
|
|
154
|
+
const idx = this.selectedSuggestion === -1 ? 0 : this.selectedSuggestion;
|
|
155
|
+
const suggestion = this.suggestions[idx];
|
|
156
|
+
const isDir = this.input.endsWith('/') || this.input.endsWith('\\');
|
|
157
|
+
const dir = isDir ? this.input : path.dirname(this.input);
|
|
158
|
+
// Construct the new path accurately
|
|
159
|
+
const baseDir = (dir === '.' && !this.input.startsWith('.')) ? '' : dir;
|
|
160
|
+
this.input = path.join(baseDir, suggestion);
|
|
144
161
|
this.cursor = this.input.length;
|
|
145
|
-
|
|
146
|
-
this.render(false);
|
|
147
|
-
}
|
|
148
|
-
else if (this.suggestions.length > 1) {
|
|
149
|
-
// Cycle or show? For now cycle if selected
|
|
150
|
-
if (this.selectedSuggestion !== -1) {
|
|
151
|
-
const dir = path.dirname(this.input);
|
|
152
|
-
this.input = path.join(dir === '.' ? '' : dir, this.suggestions[this.selectedSuggestion]);
|
|
153
|
-
this.cursor = this.input.length;
|
|
154
|
-
this.suggestions = [];
|
|
155
|
-
this.render(false);
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
// Just show suggestions (already done in render loop usually, but update logic ensures it)
|
|
159
|
-
this.updateSuggestions();
|
|
160
|
-
this.render(false);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
162
|
+
// Immediately refresh suggestions for the new path
|
|
164
163
|
this.updateSuggestions();
|
|
165
164
|
this.render(false);
|
|
166
165
|
}
|
|
167
166
|
return;
|
|
168
167
|
}
|
|
169
168
|
if (char === '\r' || char === '\n') {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
174
|
-
this.suggestions = [];
|
|
175
|
-
this.selectedSuggestion = -1;
|
|
176
|
-
this.render(false);
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
this.submit(this.input);
|
|
169
|
+
// Move cursor to the bottom to ensure clean exit UI
|
|
170
|
+
if (this.lastLinesUp > 0) {
|
|
171
|
+
this.print(`\x1b[${this.lastLinesUp}B`);
|
|
172
|
+
this.lastLinesUp = 0;
|
|
180
173
|
}
|
|
174
|
+
this.submit(this.input);
|
|
181
175
|
return;
|
|
182
176
|
}
|
|
183
|
-
if (this.isDown(char)) {
|
|
177
|
+
if (this.isDown(char)) {
|
|
184
178
|
if (this.suggestions.length > 0) {
|
|
185
|
-
|
|
179
|
+
const count = Math.min(this.suggestions.length, 5);
|
|
180
|
+
this.selectedSuggestion = (this.selectedSuggestion + 1) % count;
|
|
186
181
|
this.render(false);
|
|
187
182
|
}
|
|
188
183
|
return;
|
|
189
184
|
}
|
|
190
|
-
if (this.isUp(char)) {
|
|
185
|
+
if (this.isUp(char)) {
|
|
191
186
|
if (this.suggestions.length > 0) {
|
|
192
|
-
|
|
187
|
+
const count = Math.min(this.suggestions.length, 5);
|
|
188
|
+
this.selectedSuggestion = (this.selectedSuggestion - 1 + count) % count;
|
|
193
189
|
this.render(false);
|
|
194
190
|
}
|
|
195
191
|
return;
|
|
196
192
|
}
|
|
197
|
-
if (char === '\u0008' || char === '\x7f') {
|
|
193
|
+
if (char === '\u0008' || char === '\x7f') {
|
|
198
194
|
if (this.input.length > 0) {
|
|
199
195
|
this.input = this.input.slice(0, -1);
|
|
200
196
|
this.updateSuggestions();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { FormOptions, MouseEvent } from '../types';
|
|
3
|
+
export declare class FormPrompt extends Prompt<Record<string, string>, FormOptions> {
|
|
4
|
+
private values;
|
|
5
|
+
private activeIndex;
|
|
6
|
+
private fieldErrors;
|
|
7
|
+
private globalError;
|
|
8
|
+
private cursor;
|
|
9
|
+
private lastLinesUp;
|
|
10
|
+
constructor(options: FormOptions);
|
|
11
|
+
protected render(firstRender: boolean): void;
|
|
12
|
+
protected handleInput(char: string, key: Buffer): void;
|
|
13
|
+
protected handleMouse(event: MouseEvent): void;
|
|
14
|
+
private moveFocus;
|
|
15
|
+
private validateCurrentField;
|
|
16
|
+
private submitForm;
|
|
17
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FormPrompt = 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 FormPrompt extends base_1.Prompt {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.values = {};
|
|
13
|
+
this.activeIndex = 0;
|
|
14
|
+
this.fieldErrors = {};
|
|
15
|
+
this.globalError = '';
|
|
16
|
+
this.cursor = 0; // Cursor position for the ACTIVE field.
|
|
17
|
+
this.lastLinesUp = 0; // To track cursor position after render
|
|
18
|
+
this.options.fields.forEach(field => {
|
|
19
|
+
this.values[field.name] = field.initial || '';
|
|
20
|
+
});
|
|
21
|
+
this.cursor = this.values[this.options.fields[0].name].length;
|
|
22
|
+
}
|
|
23
|
+
render(firstRender) {
|
|
24
|
+
if (!firstRender && this.lastLinesUp > 0) {
|
|
25
|
+
this.print(`\x1b[${this.lastLinesUp}B`);
|
|
26
|
+
}
|
|
27
|
+
this.lastLinesUp = 0;
|
|
28
|
+
const outputLines = [];
|
|
29
|
+
// Title
|
|
30
|
+
outputLines.push(`${theme_1.theme.success}? ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}`);
|
|
31
|
+
if (this.globalError) {
|
|
32
|
+
outputLines.push(`${theme_1.theme.error}>> ${this.globalError}${ansi_1.ANSI.RESET}`);
|
|
33
|
+
}
|
|
34
|
+
let cursorLineIndex = -1;
|
|
35
|
+
let cursorColIndex = 0;
|
|
36
|
+
this.options.fields.forEach((field, index) => {
|
|
37
|
+
const isActive = index === this.activeIndex;
|
|
38
|
+
const value = this.values[field.name];
|
|
39
|
+
const error = this.fieldErrors[field.name];
|
|
40
|
+
// Icon
|
|
41
|
+
let icon = '';
|
|
42
|
+
if (isActive) {
|
|
43
|
+
icon = `${theme_1.theme.main}${symbols_1.symbols.pointer}`; // >
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
icon = ' '; // indent
|
|
47
|
+
}
|
|
48
|
+
// Label
|
|
49
|
+
const labelStyle = isActive ? `${theme_1.theme.main}${ansi_1.ANSI.BOLD}` : theme_1.theme.muted;
|
|
50
|
+
const label = `${labelStyle}${field.message}:${ansi_1.ANSI.RESET}`;
|
|
51
|
+
// Value
|
|
52
|
+
// Note: Use Secret/Password prompt for case sensitive input, Form prompt is for general text input
|
|
53
|
+
const displayValue = isActive ? value : `${theme_1.theme.muted}${value}${ansi_1.ANSI.RESET}`;
|
|
54
|
+
// Construct Line
|
|
55
|
+
const line = `${icon} ${label} ${displayValue}`;
|
|
56
|
+
outputLines.push(line);
|
|
57
|
+
if (isActive) {
|
|
58
|
+
// Determine cursor position
|
|
59
|
+
// We need visual length of "icon + label + value_up_to_cursor"
|
|
60
|
+
const prefix = `${icon} ${label} `;
|
|
61
|
+
const valuePrefix = value.substring(0, this.cursor);
|
|
62
|
+
cursorLineIndex = outputLines.length - 1;
|
|
63
|
+
cursorColIndex = (0, utils_1.stringWidth)(this.stripAnsi(prefix)) + (0, utils_1.stringWidth)(this.stripAnsi(valuePrefix));
|
|
64
|
+
}
|
|
65
|
+
// Error for this field
|
|
66
|
+
if (error) {
|
|
67
|
+
outputLines.push(` ${theme_1.theme.error}>> ${error}${ansi_1.ANSI.RESET}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Instructions
|
|
71
|
+
outputLines.push('');
|
|
72
|
+
outputLines.push(`${ansi_1.ANSI.RESET}${theme_1.theme.muted}(Use Up/Down/Tab to navigate, Enter to submit)${ansi_1.ANSI.RESET}`);
|
|
73
|
+
const output = outputLines.join('\n');
|
|
74
|
+
this.renderFrame(output);
|
|
75
|
+
// Position Cursor
|
|
76
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
77
|
+
if (cursorLineIndex !== -1) {
|
|
78
|
+
// Calculate lines up from bottom
|
|
79
|
+
const totalLines = outputLines.length;
|
|
80
|
+
const linesUp = (totalLines - 1) - cursorLineIndex;
|
|
81
|
+
if (linesUp > 0) {
|
|
82
|
+
this.print(`\x1b[${linesUp}A`);
|
|
83
|
+
this.lastLinesUp = linesUp;
|
|
84
|
+
}
|
|
85
|
+
this.print(ansi_1.ANSI.CURSOR_LEFT);
|
|
86
|
+
if (cursorColIndex > 0) {
|
|
87
|
+
this.print(`\x1b[${cursorColIndex}C`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
handleInput(char, key) {
|
|
92
|
+
// Navigation: Up / Shift+Tab
|
|
93
|
+
if (this.isUp(char) || (char === '\t' && key /* how to detect Shift+Tab? usually \u001b[Z */) || char === '\u001b[Z') {
|
|
94
|
+
this.validateCurrentField();
|
|
95
|
+
this.moveFocus(-1);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Navigation: Down / Tab
|
|
99
|
+
if (this.isDown(char) || char === '\t') {
|
|
100
|
+
this.validateCurrentField();
|
|
101
|
+
this.moveFocus(1);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Enter
|
|
105
|
+
if (char === '\r' || char === '\n') {
|
|
106
|
+
this.validateCurrentField().then(isValid => {
|
|
107
|
+
if (isValid) {
|
|
108
|
+
if (this.activeIndex < this.options.fields.length - 1) {
|
|
109
|
+
this.moveFocus(1);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Submit
|
|
113
|
+
this.submitForm();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Editing Active Field
|
|
120
|
+
const activeField = this.options.fields[this.activeIndex];
|
|
121
|
+
const val = this.values[activeField.name];
|
|
122
|
+
if (char === '\u0008' || char === '\x7f') { // Backspace
|
|
123
|
+
if (this.cursor > 0) {
|
|
124
|
+
const pre = val.slice(0, this.cursor - 1);
|
|
125
|
+
const post = val.slice(this.cursor);
|
|
126
|
+
this.values[activeField.name] = pre + post;
|
|
127
|
+
this.cursor--;
|
|
128
|
+
this.render(false);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Left/Right arrow to move cursor within field
|
|
133
|
+
if (this.isLeft(char)) {
|
|
134
|
+
if (this.cursor > 0)
|
|
135
|
+
this.cursor--;
|
|
136
|
+
this.render(false);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (this.isRight(char)) {
|
|
140
|
+
if (this.cursor < val.length)
|
|
141
|
+
this.cursor++;
|
|
142
|
+
this.render(false);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Typing
|
|
146
|
+
if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
|
|
147
|
+
const pre = val.slice(0, this.cursor);
|
|
148
|
+
const post = val.slice(this.cursor);
|
|
149
|
+
this.values[activeField.name] = pre + char + post;
|
|
150
|
+
this.cursor += char.length;
|
|
151
|
+
this.fieldErrors[activeField.name] = ''; // Clear error on type
|
|
152
|
+
this.render(false);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
handleMouse(event) {
|
|
156
|
+
if (event.action === 'scroll') {
|
|
157
|
+
if (event.scroll === 'up') {
|
|
158
|
+
this.validateCurrentField();
|
|
159
|
+
this.moveFocus(-1);
|
|
160
|
+
}
|
|
161
|
+
else if (event.scroll === 'down') {
|
|
162
|
+
this.validateCurrentField();
|
|
163
|
+
this.moveFocus(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
moveFocus(direction) {
|
|
168
|
+
const nextIndex = this.activeIndex + direction;
|
|
169
|
+
if (nextIndex >= 0 && nextIndex < this.options.fields.length) {
|
|
170
|
+
this.activeIndex = nextIndex;
|
|
171
|
+
// "cursor must jump to end of data"
|
|
172
|
+
this.cursor = this.values[this.options.fields[this.activeIndex].name].length;
|
|
173
|
+
this.render(false);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async validateCurrentField() {
|
|
177
|
+
const field = this.options.fields[this.activeIndex];
|
|
178
|
+
const val = this.values[field.name];
|
|
179
|
+
if (field.validate) {
|
|
180
|
+
try {
|
|
181
|
+
const res = await field.validate(val);
|
|
182
|
+
if (res !== true) {
|
|
183
|
+
this.fieldErrors[field.name] = typeof res === 'string' ? res : 'Invalid value';
|
|
184
|
+
this.render(false);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
this.fieldErrors[field.name] = err.message || 'Validation failed';
|
|
190
|
+
this.render(false);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Clear error if valid
|
|
195
|
+
delete this.fieldErrors[field.name];
|
|
196
|
+
this.render(false);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
async submitForm() {
|
|
200
|
+
this.globalError = '';
|
|
201
|
+
// Validate all fields
|
|
202
|
+
let allValid = true;
|
|
203
|
+
for (const field of this.options.fields) {
|
|
204
|
+
const val = this.values[field.name];
|
|
205
|
+
if (field.validate) {
|
|
206
|
+
const res = await field.validate(val);
|
|
207
|
+
if (res !== true) {
|
|
208
|
+
this.fieldErrors[field.name] = typeof res === 'string' ? res : 'Invalid value';
|
|
209
|
+
allValid = false;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
delete this.fieldErrors[field.name];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (allValid) {
|
|
217
|
+
this.submit(this.values);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
this.globalError = 'Please fix errors before submitting.';
|
|
221
|
+
this.render(false);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
exports.FormPrompt = FormPrompt;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { GridOptions, MouseEvent } from '../types';
|
|
3
|
+
export declare class GridPrompt extends Prompt<boolean[][], GridOptions> {
|
|
4
|
+
private cursorRow;
|
|
5
|
+
private cursorCol;
|
|
6
|
+
private selected;
|
|
7
|
+
private columnWidths;
|
|
8
|
+
private rowLabelWidth;
|
|
9
|
+
constructor(options: GridOptions);
|
|
10
|
+
private calculateLayout;
|
|
11
|
+
protected render(_firstRender: boolean): void;
|
|
12
|
+
protected handleInput(char: string, _key: Buffer): void;
|
|
13
|
+
protected handleMouse(event: MouseEvent): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GridPrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
class GridPrompt extends base_1.Prompt {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.cursorRow = 0;
|
|
12
|
+
this.cursorCol = 0;
|
|
13
|
+
this.columnWidths = [];
|
|
14
|
+
this.rowLabelWidth = 0;
|
|
15
|
+
// Initialize selection matrix
|
|
16
|
+
if (options.initial) {
|
|
17
|
+
this.selected = options.initial.map(row => [...row]);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
this.selected = options.rows.map(() => new Array(options.columns.length).fill(false));
|
|
21
|
+
}
|
|
22
|
+
this.calculateLayout();
|
|
23
|
+
}
|
|
24
|
+
calculateLayout() {
|
|
25
|
+
// Calculate max width for row labels
|
|
26
|
+
this.rowLabelWidth = 0;
|
|
27
|
+
for (const row of this.options.rows) {
|
|
28
|
+
this.rowLabelWidth = Math.max(this.rowLabelWidth, (0, utils_1.stringWidth)(row));
|
|
29
|
+
}
|
|
30
|
+
// Calculate width for each column
|
|
31
|
+
this.columnWidths = this.options.columns.map(col => {
|
|
32
|
+
// Header width vs Cell width (3 chars for "[ ]")
|
|
33
|
+
return Math.max((0, utils_1.stringWidth)(col), 3);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
render(_firstRender) {
|
|
37
|
+
const { rows, columns } = this.options;
|
|
38
|
+
let output = `${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}\n`;
|
|
39
|
+
// Render Header
|
|
40
|
+
// Padding for the row label column
|
|
41
|
+
output += ' '.repeat(this.rowLabelWidth + 2); // +2 for spacing/border
|
|
42
|
+
output += columns.map((col, i) => {
|
|
43
|
+
const width = this.columnWidths[i];
|
|
44
|
+
const padding = width - (0, utils_1.stringWidth)(col);
|
|
45
|
+
const leftPad = Math.floor(padding / 2);
|
|
46
|
+
const rightPad = padding - leftPad;
|
|
47
|
+
return `${theme_1.theme.muted}${' '.repeat(leftPad)}${col}${' '.repeat(rightPad)}${ansi_1.ANSI.RESET}`;
|
|
48
|
+
}).join(' ') + '\n';
|
|
49
|
+
// Render Rows
|
|
50
|
+
for (let r = 0; r < rows.length; r++) {
|
|
51
|
+
const rowLabel = rows[r];
|
|
52
|
+
const labelPadding = this.rowLabelWidth - (0, utils_1.stringWidth)(rowLabel);
|
|
53
|
+
// Row Label
|
|
54
|
+
output += `${theme_1.theme.muted}${rowLabel}${' '.repeat(labelPadding)}${ansi_1.ANSI.RESET} `;
|
|
55
|
+
// Cells
|
|
56
|
+
const cells = this.selected[r].map((isChecked, c) => {
|
|
57
|
+
const isFocused = (r === this.cursorRow && c === this.cursorCol);
|
|
58
|
+
const width = this.columnWidths[c];
|
|
59
|
+
const content = isChecked ? `[${theme_1.theme.success}x${ansi_1.ANSI.RESET}]` : '[ ]';
|
|
60
|
+
// content string width is 3 (ignoring ansi)
|
|
61
|
+
const padding = width - 3;
|
|
62
|
+
const leftPad = Math.floor(padding / 2);
|
|
63
|
+
const rightPad = padding - leftPad;
|
|
64
|
+
let cellStr = `${' '.repeat(leftPad)}${content}${' '.repeat(rightPad)}`;
|
|
65
|
+
if (isFocused) {
|
|
66
|
+
// Highlight the focused cell
|
|
67
|
+
// If we use ANSI.REVERSE on the whole cell including padding:
|
|
68
|
+
cellStr = `${ansi_1.ANSI.REVERSE}${cellStr}${ansi_1.ANSI.RESET}`;
|
|
69
|
+
}
|
|
70
|
+
return cellStr;
|
|
71
|
+
});
|
|
72
|
+
output += cells.join(' ') + '\n';
|
|
73
|
+
}
|
|
74
|
+
output += `${theme_1.theme.muted}Arrows to move, Space to toggle, Enter to submit${ansi_1.ANSI.RESET}`;
|
|
75
|
+
this.renderFrame(output);
|
|
76
|
+
}
|
|
77
|
+
handleInput(char, _key) {
|
|
78
|
+
const isUp = this.isUp(char);
|
|
79
|
+
const isDown = this.isDown(char);
|
|
80
|
+
const isLeft = this.isLeft(char);
|
|
81
|
+
const isRight = this.isRight(char);
|
|
82
|
+
if (isUp || isDown || isLeft || isRight) {
|
|
83
|
+
if (isUp)
|
|
84
|
+
this.cursorRow = Math.max(0, this.cursorRow - 1);
|
|
85
|
+
if (isDown)
|
|
86
|
+
this.cursorRow = Math.min(this.options.rows.length - 1, this.cursorRow + 1);
|
|
87
|
+
if (isLeft)
|
|
88
|
+
this.cursorCol = Math.max(0, this.cursorCol - 1);
|
|
89
|
+
if (isRight)
|
|
90
|
+
this.cursorCol = Math.min(this.options.columns.length - 1, this.cursorCol + 1);
|
|
91
|
+
this.render(false);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (char === ' ') {
|
|
95
|
+
this.selected[this.cursorRow][this.cursorCol] = !this.selected[this.cursorRow][this.cursorCol];
|
|
96
|
+
this.render(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (char === '\r' || char === '\n') {
|
|
100
|
+
this.submit(this.selected);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// PageUp (First Row)
|
|
104
|
+
if (char === '\x1b[5~') {
|
|
105
|
+
this.cursorRow = 0;
|
|
106
|
+
this.render(false);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// PageDown (Last Row)
|
|
110
|
+
if (char === '\x1b[6~') {
|
|
111
|
+
this.cursorRow = this.options.rows.length - 1;
|
|
112
|
+
this.render(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Home (First Column)
|
|
116
|
+
if (char === '\x1b[H' || char === '\x1b[1~') {
|
|
117
|
+
this.cursorCol = 0;
|
|
118
|
+
this.render(false);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// End (Last Column)
|
|
122
|
+
if (char === '\x1b[F' || char === '\x1b[4~') {
|
|
123
|
+
this.cursorCol = this.options.columns.length - 1;
|
|
124
|
+
this.render(false);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// r: Toggle Row
|
|
128
|
+
if (char === 'r') {
|
|
129
|
+
const row = this.selected[this.cursorRow];
|
|
130
|
+
const allSelected = row.every(c => c);
|
|
131
|
+
this.selected[this.cursorRow] = row.map(() => !allSelected);
|
|
132
|
+
this.render(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// c: Toggle Column
|
|
136
|
+
if (char === 'c') {
|
|
137
|
+
const colIdx = this.cursorCol;
|
|
138
|
+
const colValues = this.selected.map(row => row[colIdx]);
|
|
139
|
+
const allSelected = colValues.every(c => c);
|
|
140
|
+
this.selected.forEach(row => row[colIdx] = !allSelected);
|
|
141
|
+
this.render(false);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// a: Select All
|
|
145
|
+
if (char === 'a') {
|
|
146
|
+
this.selected = this.selected.map(row => row.map(() => true));
|
|
147
|
+
this.render(false);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// x: Deselect All
|
|
151
|
+
if (char === 'x') {
|
|
152
|
+
this.selected = this.selected.map(row => row.map(() => false));
|
|
153
|
+
this.render(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// i: Invert
|
|
157
|
+
if (char === 'i') {
|
|
158
|
+
this.selected = this.selected.map(row => row.map(v => !v));
|
|
159
|
+
this.render(false);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
handleMouse(event) {
|
|
164
|
+
if (event.action === 'scroll') {
|
|
165
|
+
const direction = event.scroll === 'up' ? -1 : 1;
|
|
166
|
+
if (event.shift) {
|
|
167
|
+
// Horizontal (Column)
|
|
168
|
+
this.cursorCol = Math.max(0, Math.min(this.options.columns.length - 1, this.cursorCol + direction));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Vertical (Row)
|
|
172
|
+
this.cursorRow = Math.max(0, Math.min(this.options.rows.length - 1, this.cursorRow + direction));
|
|
173
|
+
}
|
|
174
|
+
this.render(false);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.GridPrompt = GridPrompt;
|
|
@@ -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
|
+
}
|