mepcli 0.2.1 → 0.2.6
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 +61 -17
- package/dist/ansi.d.ts +2 -0
- package/dist/ansi.js +2 -0
- package/dist/base.d.ts +57 -0
- package/dist/base.js +341 -0
- package/dist/core.d.ts +6 -1
- package/dist/core.js +35 -775
- package/dist/input.d.ts +13 -0
- package/dist/input.js +89 -0
- package/dist/prompts/checkbox.d.ts +12 -0
- package/dist/prompts/checkbox.js +114 -0
- package/dist/prompts/confirm.d.ts +7 -0
- package/dist/prompts/confirm.js +42 -0
- package/dist/prompts/date.d.ts +10 -0
- package/dist/prompts/date.js +136 -0
- package/dist/prompts/file.d.ts +13 -0
- package/dist/prompts/file.js +217 -0
- package/dist/prompts/list.d.ts +9 -0
- package/dist/prompts/list.js +88 -0
- package/dist/prompts/multi-select.d.ts +14 -0
- package/dist/prompts/multi-select.js +123 -0
- package/dist/prompts/number.d.ts +10 -0
- package/dist/prompts/number.js +133 -0
- package/dist/prompts/select.d.ts +14 -0
- package/dist/prompts/select.js +148 -0
- package/dist/prompts/slider.d.ts +7 -0
- package/dist/prompts/slider.js +49 -0
- package/dist/prompts/text.d.ts +13 -0
- package/dist/prompts/text.js +321 -0
- package/dist/prompts/toggle.d.ts +7 -0
- package/dist/prompts/toggle.js +41 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +11 -0
- package/dist/types.d.ts +24 -0
- package/dist/utils.d.ts +23 -0
- package/dist/utils.js +135 -0
- package/example.ts +57 -5
- package/package.json +4 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ListPrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
// --- Implementation: List Prompt ---
|
|
8
|
+
class ListPrompt extends base_1.Prompt {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.currentInput = '';
|
|
12
|
+
this.errorMsg = '';
|
|
13
|
+
this.value = options.initial || [];
|
|
14
|
+
}
|
|
15
|
+
render(firstRender) {
|
|
16
|
+
// Prepare content
|
|
17
|
+
const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
|
|
18
|
+
let mainLine = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `;
|
|
19
|
+
// Render Tags
|
|
20
|
+
if (this.value.length > 0) {
|
|
21
|
+
this.value.forEach((tag) => {
|
|
22
|
+
mainLine += `${theme_1.theme.main}[${tag}]${ansi_1.ANSI.RESET} `;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Render Current Input
|
|
26
|
+
mainLine += `${this.currentInput}`;
|
|
27
|
+
let output = mainLine;
|
|
28
|
+
if (this.errorMsg) {
|
|
29
|
+
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
30
|
+
}
|
|
31
|
+
// Use Double Buffering
|
|
32
|
+
this.renderFrame(output);
|
|
33
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
34
|
+
// If we printed an error, the cursor is at the end of the error line.
|
|
35
|
+
// We need to move it back to the end of the input line.
|
|
36
|
+
if (this.errorMsg) {
|
|
37
|
+
// Move up one line (since error is always on the next line in this simple implementation)
|
|
38
|
+
this.print(ansi_1.ANSI.UP);
|
|
39
|
+
// Move to the correct column.
|
|
40
|
+
// We need to calculate visual length of mainLine to place cursor correctly.
|
|
41
|
+
// stripAnsi is available in base class now.
|
|
42
|
+
const visualLength = this.stripAnsi(mainLine).length;
|
|
43
|
+
this.print(ansi_1.ANSI.CURSOR_LEFT); // Go to start
|
|
44
|
+
if (visualLength > 0) {
|
|
45
|
+
this.print(`\x1b[${visualLength}C`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
handleInput(char) {
|
|
50
|
+
if (char === '\r' || char === '\n') {
|
|
51
|
+
if (this.currentInput.trim()) {
|
|
52
|
+
this.value.push(this.currentInput.trim());
|
|
53
|
+
this.currentInput = '';
|
|
54
|
+
this.errorMsg = '';
|
|
55
|
+
this.render(false);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Done if input is empty
|
|
59
|
+
if (this.options.validate) {
|
|
60
|
+
const result = this.options.validate(this.value);
|
|
61
|
+
if (result !== true) {
|
|
62
|
+
this.errorMsg = typeof result === 'string' ? result : 'Invalid input';
|
|
63
|
+
this.render(false);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.submit(this.value);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (char === '\u0008' || char === '\x7f') { // Backspace
|
|
72
|
+
if (this.currentInput.length > 0) {
|
|
73
|
+
this.currentInput = this.currentInput.slice(0, -1);
|
|
74
|
+
this.render(false);
|
|
75
|
+
}
|
|
76
|
+
else if (this.value.length > 0) {
|
|
77
|
+
this.value.pop();
|
|
78
|
+
this.render(false);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
|
|
83
|
+
this.currentInput += char;
|
|
84
|
+
this.render(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.ListPrompt = ListPrompt;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { MultiSelectOptions } from '../types';
|
|
3
|
+
export declare class MultiSelectPrompt extends Prompt<any[], MultiSelectOptions> {
|
|
4
|
+
private selectedIndex;
|
|
5
|
+
private checkedState;
|
|
6
|
+
private searchBuffer;
|
|
7
|
+
private scrollTop;
|
|
8
|
+
private readonly pageSize;
|
|
9
|
+
private errorMsg;
|
|
10
|
+
constructor(options: MultiSelectOptions);
|
|
11
|
+
private getFilteredChoices;
|
|
12
|
+
protected render(firstRender: boolean): void;
|
|
13
|
+
protected handleInput(char: string): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MultiSelectPrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
// --- Implementation: MultiSelect Prompt ---
|
|
8
|
+
class MultiSelectPrompt extends base_1.Prompt {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.selectedIndex = 0;
|
|
12
|
+
this.searchBuffer = '';
|
|
13
|
+
this.scrollTop = 0;
|
|
14
|
+
this.pageSize = 7;
|
|
15
|
+
this.errorMsg = '';
|
|
16
|
+
this.checkedState = options.choices.map(c => !!c.selected);
|
|
17
|
+
this.selectedIndex = 0;
|
|
18
|
+
}
|
|
19
|
+
getFilteredChoices() {
|
|
20
|
+
if (!this.searchBuffer)
|
|
21
|
+
return this.options.choices.map((c, i) => ({ ...c, originalIndex: i }));
|
|
22
|
+
return this.options.choices
|
|
23
|
+
.map((c, i) => ({ ...c, originalIndex: i }))
|
|
24
|
+
.filter(c => c.title.toLowerCase().includes(this.searchBuffer.toLowerCase()));
|
|
25
|
+
}
|
|
26
|
+
render(firstRender) {
|
|
27
|
+
let output = '';
|
|
28
|
+
const choices = this.getFilteredChoices();
|
|
29
|
+
// Adjust Scroll
|
|
30
|
+
if (this.selectedIndex < this.scrollTop) {
|
|
31
|
+
this.scrollTop = this.selectedIndex;
|
|
32
|
+
}
|
|
33
|
+
else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
|
|
34
|
+
this.scrollTop = this.selectedIndex - this.pageSize + 1;
|
|
35
|
+
}
|
|
36
|
+
if (this.scrollTop > choices.length - 1) {
|
|
37
|
+
this.scrollTop = Math.max(0, choices.length - this.pageSize);
|
|
38
|
+
}
|
|
39
|
+
const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
|
|
40
|
+
const searchStr = this.searchBuffer ? ` ${theme_1.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
|
|
41
|
+
output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${searchStr}\n`;
|
|
42
|
+
if (choices.length === 0) {
|
|
43
|
+
output += ` ${theme_1.theme.muted}No results found${ansi_1.ANSI.RESET}`; // No newline at end
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const visible = choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
|
|
47
|
+
visible.forEach((choice, index) => {
|
|
48
|
+
if (index > 0)
|
|
49
|
+
output += '\n';
|
|
50
|
+
const actualIndex = this.scrollTop + index;
|
|
51
|
+
const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
|
|
52
|
+
const isChecked = this.checkedState[choice.originalIndex];
|
|
53
|
+
const checkbox = isChecked
|
|
54
|
+
? `${theme_1.theme.success}◉${ansi_1.ANSI.RESET}`
|
|
55
|
+
: `${theme_1.theme.muted}◯${ansi_1.ANSI.RESET}`;
|
|
56
|
+
output += `${cursor} ${checkbox} ${choice.title}`;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (this.errorMsg) {
|
|
60
|
+
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
61
|
+
}
|
|
62
|
+
this.renderFrame(output);
|
|
63
|
+
}
|
|
64
|
+
handleInput(char) {
|
|
65
|
+
const choices = this.getFilteredChoices();
|
|
66
|
+
if (char === '\r' || char === '\n') {
|
|
67
|
+
const selectedCount = this.checkedState.filter(Boolean).length;
|
|
68
|
+
const { min = 0, max } = this.options;
|
|
69
|
+
if (selectedCount < min) {
|
|
70
|
+
this.errorMsg = `Select at least ${min}.`;
|
|
71
|
+
this.render(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (max && selectedCount > max) {
|
|
75
|
+
this.errorMsg = `Max ${max} allowed.`;
|
|
76
|
+
this.render(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const results = this.options.choices
|
|
80
|
+
.filter((_, i) => this.checkedState[i])
|
|
81
|
+
.map(c => c.value);
|
|
82
|
+
this.submit(results);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (char === ' ') {
|
|
86
|
+
if (choices.length > 0) {
|
|
87
|
+
const choice = choices[this.selectedIndex];
|
|
88
|
+
const originalIndex = choice.originalIndex;
|
|
89
|
+
this.checkedState[originalIndex] = !this.checkedState[originalIndex];
|
|
90
|
+
this.render(false);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.isUp(char)) { // Up
|
|
95
|
+
if (choices.length > 0) {
|
|
96
|
+
this.selectedIndex = (this.selectedIndex - 1 + choices.length) % choices.length;
|
|
97
|
+
this.render(false);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (this.isDown(char)) { // Down
|
|
102
|
+
if (choices.length > 0) {
|
|
103
|
+
this.selectedIndex = (this.selectedIndex + 1) % choices.length;
|
|
104
|
+
this.render(false);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (char === '\u0008' || char === '\x7f') { // Backspace
|
|
109
|
+
if (this.searchBuffer.length > 0) {
|
|
110
|
+
this.searchBuffer = this.searchBuffer.slice(0, -1);
|
|
111
|
+
this.selectedIndex = 0;
|
|
112
|
+
this.render(false);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
|
|
117
|
+
this.searchBuffer += char;
|
|
118
|
+
this.selectedIndex = 0;
|
|
119
|
+
this.render(false);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.MultiSelectPrompt = MultiSelectPrompt;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { NumberOptions } from '../types';
|
|
3
|
+
export declare class NumberPrompt extends Prompt<number, NumberOptions> {
|
|
4
|
+
private stringValue;
|
|
5
|
+
private cursor;
|
|
6
|
+
private errorMsg;
|
|
7
|
+
constructor(options: NumberOptions);
|
|
8
|
+
protected render(firstRender: boolean): void;
|
|
9
|
+
protected handleInput(char: string): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NumberPrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
// --- Implementation: Number Prompt ---
|
|
8
|
+
class NumberPrompt extends base_1.Prompt {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.stringValue = '';
|
|
12
|
+
this.cursor = 0;
|
|
13
|
+
this.errorMsg = '';
|
|
14
|
+
// We work with string for editing, but value property stores the parsed number ultimately
|
|
15
|
+
// Initialize stringValue from initial
|
|
16
|
+
this.stringValue = options.initial !== undefined ? options.initial.toString() : '';
|
|
17
|
+
this.cursor = this.stringValue.length;
|
|
18
|
+
}
|
|
19
|
+
render(firstRender) {
|
|
20
|
+
// Prepare content
|
|
21
|
+
const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
|
|
22
|
+
// Prefix
|
|
23
|
+
let output = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `;
|
|
24
|
+
// Value
|
|
25
|
+
output += `${theme_1.theme.main}${this.stringValue}${ansi_1.ANSI.RESET}`;
|
|
26
|
+
if (this.errorMsg) {
|
|
27
|
+
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
28
|
+
}
|
|
29
|
+
this.renderFrame(output);
|
|
30
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
31
|
+
// Restore cursor position
|
|
32
|
+
// If errorMsg, we are on the line below the input.
|
|
33
|
+
if (this.errorMsg) {
|
|
34
|
+
this.print(ansi_1.ANSI.UP);
|
|
35
|
+
}
|
|
36
|
+
// Calculate visual offset
|
|
37
|
+
const prefix = `${icon} ${theme_1.theme.title}${this.options.message} `;
|
|
38
|
+
const prefixLen = this.stripAnsi(prefix).length;
|
|
39
|
+
const targetCol = prefixLen + this.cursor;
|
|
40
|
+
this.print(ansi_1.ANSI.CURSOR_LEFT);
|
|
41
|
+
if (targetCol > 0) {
|
|
42
|
+
this.print(`\x1b[${targetCol}C`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
handleInput(char) {
|
|
46
|
+
// Enter
|
|
47
|
+
if (char === '\r' || char === '\n') {
|
|
48
|
+
const num = parseFloat(this.stringValue);
|
|
49
|
+
if (this.stringValue.trim() === '' || isNaN(num)) {
|
|
50
|
+
// Check if empty is allowed?
|
|
51
|
+
// If not required? Assuming required for number prompt usually
|
|
52
|
+
this.errorMsg = 'Please enter a valid number.';
|
|
53
|
+
this.render(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (this.options.min !== undefined && num < this.options.min) {
|
|
57
|
+
this.errorMsg = `Minimum value is ${this.options.min}`;
|
|
58
|
+
this.render(false);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (this.options.max !== undefined && num > this.options.max) {
|
|
62
|
+
this.errorMsg = `Maximum value is ${this.options.max}`;
|
|
63
|
+
this.render(false);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.submit(num);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Up Arrow (Increment)
|
|
70
|
+
if (this.isUp(char)) {
|
|
71
|
+
let num = parseFloat(this.stringValue) || 0;
|
|
72
|
+
num += (this.options.step ?? 1);
|
|
73
|
+
if (this.options.max !== undefined && num > this.options.max)
|
|
74
|
+
num = this.options.max;
|
|
75
|
+
// Round to avoid float errors
|
|
76
|
+
num = Math.round(num * 10000) / 10000;
|
|
77
|
+
this.stringValue = num.toString();
|
|
78
|
+
this.cursor = this.stringValue.length;
|
|
79
|
+
this.errorMsg = '';
|
|
80
|
+
this.render(false);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Down Arrow (Decrement)
|
|
84
|
+
if (this.isDown(char)) {
|
|
85
|
+
let num = parseFloat(this.stringValue) || 0;
|
|
86
|
+
num -= (this.options.step ?? 1);
|
|
87
|
+
if (this.options.min !== undefined && num < this.options.min)
|
|
88
|
+
num = this.options.min;
|
|
89
|
+
// Round to avoid float errors
|
|
90
|
+
num = Math.round(num * 10000) / 10000;
|
|
91
|
+
this.stringValue = num.toString();
|
|
92
|
+
this.cursor = this.stringValue.length;
|
|
93
|
+
this.errorMsg = '';
|
|
94
|
+
this.render(false);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Backspace
|
|
98
|
+
if (char === '\u0008' || char === '\x7f') {
|
|
99
|
+
if (this.cursor > 0) {
|
|
100
|
+
this.stringValue = this.stringValue.slice(0, this.cursor - 1) + this.stringValue.slice(this.cursor);
|
|
101
|
+
this.cursor--;
|
|
102
|
+
this.errorMsg = '';
|
|
103
|
+
this.render(false);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Arrow Left
|
|
108
|
+
if (this.isLeft(char)) {
|
|
109
|
+
if (this.cursor > 0) {
|
|
110
|
+
this.cursor--;
|
|
111
|
+
this.render(false);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Arrow Right
|
|
116
|
+
if (this.isRight(char)) {
|
|
117
|
+
if (this.cursor < this.stringValue.length) {
|
|
118
|
+
this.cursor++;
|
|
119
|
+
this.render(false);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Numeric Input (and . and -)
|
|
124
|
+
if (/^[0-9.\-]+$/.test(char)) {
|
|
125
|
+
// Allow if it looks like a number part
|
|
126
|
+
this.stringValue = this.stringValue.slice(0, this.cursor) + char + this.stringValue.slice(this.cursor);
|
|
127
|
+
this.cursor += char.length;
|
|
128
|
+
this.errorMsg = '';
|
|
129
|
+
this.render(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.NumberPrompt = NumberPrompt;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { SelectOptions } from '../types';
|
|
3
|
+
export declare class SelectPrompt extends Prompt<any, SelectOptions> {
|
|
4
|
+
private selectedIndex;
|
|
5
|
+
private searchBuffer;
|
|
6
|
+
private scrollTop;
|
|
7
|
+
private readonly pageSize;
|
|
8
|
+
constructor(options: SelectOptions);
|
|
9
|
+
private isSeparator;
|
|
10
|
+
private findNextSelectableIndex;
|
|
11
|
+
private getFilteredChoices;
|
|
12
|
+
protected render(firstRender: boolean): void;
|
|
13
|
+
protected handleInput(char: string): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SelectPrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
// --- Implementation: Select Prompt ---
|
|
8
|
+
class SelectPrompt extends base_1.Prompt {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.selectedIndex = 0;
|
|
12
|
+
this.searchBuffer = '';
|
|
13
|
+
this.scrollTop = 0;
|
|
14
|
+
this.pageSize = 7;
|
|
15
|
+
// Find first non-separator index
|
|
16
|
+
this.selectedIndex = this.findNextSelectableIndex(-1, 1);
|
|
17
|
+
}
|
|
18
|
+
isSeparator(item) {
|
|
19
|
+
return item && item.separator === true;
|
|
20
|
+
}
|
|
21
|
+
findNextSelectableIndex(currentIndex, direction) {
|
|
22
|
+
let nextIndex = currentIndex + direction;
|
|
23
|
+
const choices = this.getFilteredChoices();
|
|
24
|
+
// Loop around logic
|
|
25
|
+
if (nextIndex < 0)
|
|
26
|
+
nextIndex = choices.length - 1;
|
|
27
|
+
if (nextIndex >= choices.length)
|
|
28
|
+
nextIndex = 0;
|
|
29
|
+
if (choices.length === 0)
|
|
30
|
+
return 0;
|
|
31
|
+
// Safety check to prevent infinite loop if all are separators (shouldn't happen in practice)
|
|
32
|
+
let count = 0;
|
|
33
|
+
while (this.isSeparator(choices[nextIndex]) && count < choices.length) {
|
|
34
|
+
nextIndex += direction;
|
|
35
|
+
if (nextIndex < 0)
|
|
36
|
+
nextIndex = choices.length - 1;
|
|
37
|
+
if (nextIndex >= choices.length)
|
|
38
|
+
nextIndex = 0;
|
|
39
|
+
count++;
|
|
40
|
+
}
|
|
41
|
+
return nextIndex;
|
|
42
|
+
}
|
|
43
|
+
getFilteredChoices() {
|
|
44
|
+
if (!this.searchBuffer)
|
|
45
|
+
return this.options.choices;
|
|
46
|
+
return this.options.choices.filter(c => {
|
|
47
|
+
if (this.isSeparator(c))
|
|
48
|
+
return false; // Hide separators when searching
|
|
49
|
+
return c.title.toLowerCase().includes(this.searchBuffer.toLowerCase());
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
render(firstRender) {
|
|
53
|
+
let output = '';
|
|
54
|
+
const choices = this.getFilteredChoices();
|
|
55
|
+
// Adjust Scroll Top
|
|
56
|
+
if (this.selectedIndex < this.scrollTop) {
|
|
57
|
+
this.scrollTop = this.selectedIndex;
|
|
58
|
+
}
|
|
59
|
+
else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
|
|
60
|
+
this.scrollTop = this.selectedIndex - this.pageSize + 1;
|
|
61
|
+
}
|
|
62
|
+
// Handle Filtering Edge Case: if list shrinks, scrollTop might be too high
|
|
63
|
+
if (this.scrollTop > choices.length - 1) {
|
|
64
|
+
this.scrollTop = Math.max(0, choices.length - this.pageSize);
|
|
65
|
+
}
|
|
66
|
+
// Header
|
|
67
|
+
const searchStr = this.searchBuffer ? ` ${theme_1.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
|
|
68
|
+
// Note: We avoid ERASE_LINE here because renderFrame handles full redraw
|
|
69
|
+
output += `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${searchStr}\n`;
|
|
70
|
+
if (choices.length === 0) {
|
|
71
|
+
output += ` ${theme_1.theme.muted}No results found${ansi_1.ANSI.RESET}`;
|
|
72
|
+
// We can omit newline at the very end if we want, but usually it's better to be consistent.
|
|
73
|
+
// renderFrame adds newline via truncate logic? No, it joins.
|
|
74
|
+
// So if I want a line break, I must add it.
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const visibleChoices = choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
|
|
78
|
+
visibleChoices.forEach((choice, index) => {
|
|
79
|
+
const actualIndex = this.scrollTop + index;
|
|
80
|
+
if (index > 0)
|
|
81
|
+
output += '\n'; // Separator between items
|
|
82
|
+
if (this.isSeparator(choice)) {
|
|
83
|
+
output += ` ${ansi_1.ANSI.DIM}${choice.text || '────────'}${ansi_1.ANSI.RESET}`;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (actualIndex === this.selectedIndex) {
|
|
87
|
+
output += `${theme_1.theme.main}❯ ${choice.title}${ansi_1.ANSI.RESET}`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
output += ` ${choice.title}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// No manual printing. Pass to renderFrame.
|
|
96
|
+
this.renderFrame(output);
|
|
97
|
+
}
|
|
98
|
+
handleInput(char) {
|
|
99
|
+
const choices = this.getFilteredChoices();
|
|
100
|
+
if (char === '\r' || char === '\n') {
|
|
101
|
+
if (choices.length === 0) {
|
|
102
|
+
this.searchBuffer = '';
|
|
103
|
+
this.selectedIndex = this.findNextSelectableIndex(-1, 1);
|
|
104
|
+
this.render(false);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (this.isSeparator(choices[this.selectedIndex]))
|
|
108
|
+
return;
|
|
109
|
+
this.cleanup();
|
|
110
|
+
// Cursor is shown by cleanup
|
|
111
|
+
if (this._resolve)
|
|
112
|
+
this._resolve(choices[this.selectedIndex].value);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this.isUp(char)) { // Up
|
|
116
|
+
if (choices.length > 0) {
|
|
117
|
+
this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, -1);
|
|
118
|
+
this.render(false);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (this.isDown(char)) { // Down
|
|
123
|
+
if (choices.length > 0) {
|
|
124
|
+
this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, 1);
|
|
125
|
+
this.render(false);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Backspace
|
|
130
|
+
if (char === '\u0008' || char === '\x7f') {
|
|
131
|
+
if (this.searchBuffer.length > 0) {
|
|
132
|
+
this.searchBuffer = this.searchBuffer.slice(0, -1);
|
|
133
|
+
this.selectedIndex = 0; // Reset selection
|
|
134
|
+
this.selectedIndex = this.findNextSelectableIndex(-1, 1);
|
|
135
|
+
this.render(false);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Typing
|
|
140
|
+
if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
|
|
141
|
+
this.searchBuffer += char;
|
|
142
|
+
this.selectedIndex = 0; // Reset selection
|
|
143
|
+
this.selectedIndex = this.findNextSelectableIndex(-1, 1);
|
|
144
|
+
this.render(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.SelectPrompt = SelectPrompt;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { SliderOptions } from '../types';
|
|
3
|
+
export declare class SliderPrompt extends Prompt<number, SliderOptions> {
|
|
4
|
+
constructor(options: SliderOptions);
|
|
5
|
+
protected render(firstRender: boolean): void;
|
|
6
|
+
protected handleInput(char: string): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SliderPrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
// --- Implementation: Slider Prompt ---
|
|
8
|
+
class SliderPrompt extends base_1.Prompt {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.value = options.initial ?? options.min;
|
|
12
|
+
}
|
|
13
|
+
render(firstRender) {
|
|
14
|
+
const width = 20;
|
|
15
|
+
const range = this.options.max - this.options.min;
|
|
16
|
+
const ratio = (this.value - this.options.min) / range;
|
|
17
|
+
const pos = Math.round(ratio * width);
|
|
18
|
+
let bar = '';
|
|
19
|
+
for (let i = 0; i <= width; i++) {
|
|
20
|
+
if (i === pos)
|
|
21
|
+
bar += `${theme_1.theme.main}O${ansi_1.ANSI.RESET}`;
|
|
22
|
+
else
|
|
23
|
+
bar += '─';
|
|
24
|
+
}
|
|
25
|
+
const unit = this.options.unit || '';
|
|
26
|
+
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} [${bar}] ${this.value}${unit}`;
|
|
27
|
+
this.renderFrame(output);
|
|
28
|
+
}
|
|
29
|
+
handleInput(char) {
|
|
30
|
+
if (char === '\r' || char === '\n') {
|
|
31
|
+
this.submit(this.value);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const step = this.options.step || 1;
|
|
35
|
+
if (this.isLeft(char)) { // Left
|
|
36
|
+
this.value = Math.max(this.options.min, this.value - step);
|
|
37
|
+
// Round to avoid float errors
|
|
38
|
+
this.value = Math.round(this.value * 10000) / 10000;
|
|
39
|
+
this.render(false);
|
|
40
|
+
}
|
|
41
|
+
if (this.isRight(char)) { // Right
|
|
42
|
+
this.value = Math.min(this.options.max, this.value + step);
|
|
43
|
+
// Round to avoid float errors
|
|
44
|
+
this.value = Math.round(this.value * 10000) / 10000;
|
|
45
|
+
this.render(false);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.SliderPrompt = SliderPrompt;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { TextOptions } from '../types';
|
|
3
|
+
export declare class TextPrompt extends Prompt<string, TextOptions> {
|
|
4
|
+
private errorMsg;
|
|
5
|
+
private cursor;
|
|
6
|
+
private hasTyped;
|
|
7
|
+
private segments;
|
|
8
|
+
constructor(options: TextOptions);
|
|
9
|
+
protected render(firstRender: boolean): void;
|
|
10
|
+
private getSegmentWidth;
|
|
11
|
+
protected handleInput(char: string): void;
|
|
12
|
+
private validateAndSubmit;
|
|
13
|
+
}
|