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
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# Mep
|
|
1
|
+
# Mep
|
|
2
2
|
|
|
3
3
|
**Mep** is a minimalist and zero-dependency library for creating interactive command-line prompts in Node.js. It focuses on simplicity, modern design, and robust input handling, including support for cursor movement and input validation.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Zero Dependency:** Keeps your project clean and fast.
|
|
8
|
-
- **
|
|
9
|
-
- **Responsive Input:** Supports cursor movement (Left/Right) and character insertion/deletion in
|
|
10
|
-
- **Validation:** Built-in support for input validation with custom error messages.
|
|
8
|
+
- **Comprehensive Prompts:** Includes `text`, `password`, `select`, `checkbox`, `confirm`, `number`, `toggle`, `list`, `slider`, `date`, `file`, and `multiSelect`.
|
|
9
|
+
- **Responsive Input:** Supports cursor movement (Left/Right) and character insertion/deletion in text-based prompts.
|
|
10
|
+
- **Validation:** Built-in support for input validation (sync and async) with custom error messages.
|
|
11
11
|
- **Elegant Look:** Uses ANSI colors for a clean, modern CLI experience.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
@@ -21,31 +21,75 @@ yarn add mepcli
|
|
|
21
21
|
## Usage Example
|
|
22
22
|
|
|
23
23
|
Mep provides a static class facade, `MepCLI`, for all interactions.
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
25
26
|
import { MepCLI } from 'mepcli';
|
|
26
27
|
|
|
27
|
-
async function
|
|
28
|
-
// Text input with validation
|
|
29
|
-
const
|
|
30
|
-
message: "Enter
|
|
31
|
-
|
|
28
|
+
async function main() {
|
|
29
|
+
// Text input with validation
|
|
30
|
+
const name = await MepCLI.text({
|
|
31
|
+
message: "Enter your name:",
|
|
32
|
+
placeholder: "John Doe",
|
|
33
|
+
validate: (v) => v.length > 0 || "Name cannot be empty"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Number input
|
|
37
|
+
const age = await MepCLI.number({
|
|
38
|
+
message: "How old are you?",
|
|
39
|
+
min: 1,
|
|
40
|
+
max: 120
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Toggle (Switch)
|
|
44
|
+
const newsletter = await MepCLI.toggle({
|
|
45
|
+
message: "Subscribe to newsletter?",
|
|
46
|
+
initial: true
|
|
32
47
|
});
|
|
33
48
|
|
|
34
49
|
// Select menu
|
|
35
|
-
const
|
|
36
|
-
message: "
|
|
50
|
+
const lang = await MepCLI.select({
|
|
51
|
+
message: "Preferred Language:",
|
|
52
|
+
choices: [
|
|
53
|
+
{ title: "JavaScript", value: "js" },
|
|
54
|
+
{ title: "TypeScript", value: "ts" },
|
|
55
|
+
{ title: "Python", value: "py" }
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Checkbox (Multiple choice)
|
|
60
|
+
const tools = await MepCLI.checkbox({
|
|
61
|
+
message: "Select tools:",
|
|
37
62
|
choices: [
|
|
38
|
-
{ title: "
|
|
39
|
-
{ title: "
|
|
63
|
+
{ title: "ESLint", value: "eslint" },
|
|
64
|
+
{ title: "Prettier", value: "prettier", selected: true },
|
|
65
|
+
{ title: "Jest", value: "jest" }
|
|
40
66
|
]
|
|
41
67
|
});
|
|
42
68
|
|
|
43
|
-
console.log(
|
|
69
|
+
console.log({ name, age, newsletter, lang, tools });
|
|
44
70
|
}
|
|
45
71
|
|
|
46
|
-
|
|
72
|
+
main();
|
|
47
73
|
```
|
|
48
74
|
|
|
75
|
+
## API Reference
|
|
76
|
+
|
|
77
|
+
### MepCLI
|
|
78
|
+
|
|
79
|
+
* `text(options)` - Single line or multiline text input.
|
|
80
|
+
* `password(options)` - Masked text input.
|
|
81
|
+
* `number(options)` - Numeric input with increment/decrement support.
|
|
82
|
+
* `confirm(options)` - Yes/No question.
|
|
83
|
+
* `toggle(options)` - On/Off switch.
|
|
84
|
+
* `select(options)` - Single item selection from a list.
|
|
85
|
+
* `multiSelect(options)` - Multiple item selection with filtering.
|
|
86
|
+
* `checkbox(options)` - Classic checkbox selection.
|
|
87
|
+
* `list(options)` - Enter a list of tags/strings.
|
|
88
|
+
* `slider(options)` - Select a number within a range using a visual slider.
|
|
89
|
+
* `date(options)` - Date and time picker.
|
|
90
|
+
* `file(options)` - File system navigator and selector.
|
|
91
|
+
* `spin(message, promise)` - Display a spinner while waiting for a promise.
|
|
92
|
+
|
|
49
93
|
## License
|
|
50
94
|
|
|
51
|
-
This project is under the **MIT License**.
|
|
95
|
+
This project is under the **MIT License**.
|
package/dist/ansi.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const ANSI: {
|
|
|
7
7
|
BOLD: string;
|
|
8
8
|
DIM: string;
|
|
9
9
|
ITALIC: string;
|
|
10
|
+
UNDERLINE: string;
|
|
10
11
|
FG_GREEN: string;
|
|
11
12
|
FG_CYAN: string;
|
|
12
13
|
FG_YELLOW: string;
|
|
@@ -14,6 +15,7 @@ export declare const ANSI: {
|
|
|
14
15
|
FG_GRAY: string;
|
|
15
16
|
FG_WHITE: string;
|
|
16
17
|
ERASE_LINE: string;
|
|
18
|
+
ERASE_DOWN: string;
|
|
17
19
|
CURSOR_LEFT: string;
|
|
18
20
|
HIDE_CURSOR: string;
|
|
19
21
|
SHOW_CURSOR: string;
|
package/dist/ansi.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.ANSI = {
|
|
|
10
10
|
BOLD: '\x1b[1m',
|
|
11
11
|
DIM: '\x1b[2m',
|
|
12
12
|
ITALIC: '\x1b[3m',
|
|
13
|
+
UNDERLINE: '\x1b[4m',
|
|
13
14
|
// Colors
|
|
14
15
|
FG_GREEN: '\x1b[32m',
|
|
15
16
|
FG_CYAN: '\x1b[36m',
|
|
@@ -19,6 +20,7 @@ exports.ANSI = {
|
|
|
19
20
|
FG_WHITE: '\x1b[37m',
|
|
20
21
|
// Cursor & Erasing
|
|
21
22
|
ERASE_LINE: '\x1b[2K', // Clear current line
|
|
23
|
+
ERASE_DOWN: '\x1b[J', // Clear from cursor to end of screen
|
|
22
24
|
CURSOR_LEFT: '\x1b[1000D', // Move cursor to start of line
|
|
23
25
|
HIDE_CURSOR: '\x1b[?25l',
|
|
24
26
|
SHOW_CURSOR: '\x1b[?25h',
|
package/dist/base.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { detectCapabilities } from './utils';
|
|
2
|
+
/**
|
|
3
|
+
* Abstract base class for all prompts.
|
|
4
|
+
* Handles common logic like stdin management, raw mode, and cleanup
|
|
5
|
+
* to enforce DRY (Don't Repeat Yourself) principles.
|
|
6
|
+
*/
|
|
7
|
+
export declare abstract class Prompt<T, O> {
|
|
8
|
+
protected options: O;
|
|
9
|
+
protected value: any;
|
|
10
|
+
protected stdin: NodeJS.ReadStream;
|
|
11
|
+
protected stdout: NodeJS.WriteStream;
|
|
12
|
+
private _resolve?;
|
|
13
|
+
private _reject?;
|
|
14
|
+
private _inputParser;
|
|
15
|
+
private _onKeyHandler?;
|
|
16
|
+
private _onDataHandler?;
|
|
17
|
+
protected lastRenderHeight: number;
|
|
18
|
+
protected lastRenderLines: string[];
|
|
19
|
+
protected capabilities: ReturnType<typeof detectCapabilities>;
|
|
20
|
+
constructor(options: O);
|
|
21
|
+
/**
|
|
22
|
+
* Renders the UI. Must be implemented by subclasses.
|
|
23
|
+
* @param firstRender Indicates if this is the initial render.
|
|
24
|
+
*/
|
|
25
|
+
protected abstract render(firstRender: boolean): void;
|
|
26
|
+
/**
|
|
27
|
+
* Handles specific key inputs. Must be implemented by subclasses.
|
|
28
|
+
* @param char The string representation of the key.
|
|
29
|
+
* @param key The raw buffer.
|
|
30
|
+
*/
|
|
31
|
+
protected abstract handleInput(char: string, key: Buffer): void;
|
|
32
|
+
protected print(text: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Starts the prompt interaction.
|
|
35
|
+
* Sets up raw mode and listeners, returning a Promise.
|
|
36
|
+
*/
|
|
37
|
+
run(): Promise<T>;
|
|
38
|
+
/**
|
|
39
|
+
* Cleans up listeners and restores stdin state.
|
|
40
|
+
*/
|
|
41
|
+
protected cleanup(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Submits the final value and resolves the promise.
|
|
44
|
+
*/
|
|
45
|
+
protected submit(result: T): void;
|
|
46
|
+
/**
|
|
47
|
+
* Render Method with Diffing (Virtual DOM for CLI).
|
|
48
|
+
* Calculates new lines, compares with old lines, and updates only changed parts.
|
|
49
|
+
*/
|
|
50
|
+
protected renderFrame(content: string): void;
|
|
51
|
+
protected stripAnsi(str: string): string;
|
|
52
|
+
protected truncate(str: string, width: number): string;
|
|
53
|
+
protected isUp(char: string): boolean;
|
|
54
|
+
protected isDown(char: string): boolean;
|
|
55
|
+
protected isRight(char: string): boolean;
|
|
56
|
+
protected isLeft(char: string): boolean;
|
|
57
|
+
}
|
package/dist/base.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Prompt = void 0;
|
|
4
|
+
const ansi_1 = require("./ansi");
|
|
5
|
+
const input_1 = require("./input");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
/**
|
|
8
|
+
* Abstract base class for all prompts.
|
|
9
|
+
* Handles common logic like stdin management, raw mode, and cleanup
|
|
10
|
+
* to enforce DRY (Don't Repeat Yourself) principles.
|
|
11
|
+
*/
|
|
12
|
+
class Prompt {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
// Smart Cursor State
|
|
15
|
+
this.lastRenderHeight = 0;
|
|
16
|
+
this.lastRenderLines = [];
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.stdin = process.stdin;
|
|
19
|
+
this.stdout = process.stdout;
|
|
20
|
+
this._inputParser = new input_1.InputParser();
|
|
21
|
+
this.capabilities = (0, utils_1.detectCapabilities)();
|
|
22
|
+
}
|
|
23
|
+
print(text) {
|
|
24
|
+
this.stdout.write(text);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Starts the prompt interaction.
|
|
28
|
+
* Sets up raw mode and listeners, returning a Promise.
|
|
29
|
+
*/
|
|
30
|
+
run() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
this._resolve = resolve;
|
|
33
|
+
this._reject = reject;
|
|
34
|
+
if (typeof this.stdin.setRawMode === 'function') {
|
|
35
|
+
this.stdin.setRawMode(true);
|
|
36
|
+
}
|
|
37
|
+
this.stdin.resume();
|
|
38
|
+
this.stdin.setEncoding('utf8');
|
|
39
|
+
// Initial render: Default to hidden cursor (good for menus)
|
|
40
|
+
// Subclasses like TextPrompt will explicitly show it if needed.
|
|
41
|
+
if (this.capabilities.isCI) {
|
|
42
|
+
// In CI, maybe don't hide cursor or do nothing?
|
|
43
|
+
// But for now follow standard flow.
|
|
44
|
+
}
|
|
45
|
+
this.print(ansi_1.ANSI.HIDE_CURSOR);
|
|
46
|
+
this.render(true);
|
|
47
|
+
// Setup Input Parser Listeners
|
|
48
|
+
this._onKeyHandler = (char, buffer) => {
|
|
49
|
+
// Global Exit Handler (Ctrl+C)
|
|
50
|
+
if (char === '\u0003') {
|
|
51
|
+
this.cleanup();
|
|
52
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR + '\n');
|
|
53
|
+
if (this._reject)
|
|
54
|
+
this._reject(new Error('User force closed'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.handleInput(char, buffer);
|
|
58
|
+
};
|
|
59
|
+
this._inputParser.on('keypress', this._onKeyHandler);
|
|
60
|
+
this._onDataHandler = (buffer) => {
|
|
61
|
+
this._inputParser.feed(buffer);
|
|
62
|
+
};
|
|
63
|
+
this.stdin.on('data', this._onDataHandler);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Cleans up listeners and restores stdin state.
|
|
68
|
+
*/
|
|
69
|
+
cleanup() {
|
|
70
|
+
if (this._onDataHandler) {
|
|
71
|
+
this.stdin.removeListener('data', this._onDataHandler);
|
|
72
|
+
}
|
|
73
|
+
if (this._onKeyHandler) {
|
|
74
|
+
this._inputParser.removeListener('keypress', this._onKeyHandler);
|
|
75
|
+
}
|
|
76
|
+
if (typeof this.stdin.setRawMode === 'function') {
|
|
77
|
+
this.stdin.setRawMode(false);
|
|
78
|
+
}
|
|
79
|
+
this.stdin.pause();
|
|
80
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Submits the final value and resolves the promise.
|
|
84
|
+
*/
|
|
85
|
+
submit(result) {
|
|
86
|
+
this.cleanup();
|
|
87
|
+
this.print('\n');
|
|
88
|
+
if (this._resolve)
|
|
89
|
+
this._resolve(result);
|
|
90
|
+
}
|
|
91
|
+
// --- Rendering Utilities ---
|
|
92
|
+
/**
|
|
93
|
+
* Render Method with Diffing (Virtual DOM for CLI).
|
|
94
|
+
* Calculates new lines, compares with old lines, and updates only changed parts.
|
|
95
|
+
*/
|
|
96
|
+
renderFrame(content) {
|
|
97
|
+
// Ensure lines are truncated to terminal width
|
|
98
|
+
const width = this.stdout.columns || 80;
|
|
99
|
+
const rawLines = content.split('\n');
|
|
100
|
+
// Truncate each line and prepare the new buffer
|
|
101
|
+
const newLines = rawLines.map(line => this.truncate(line, width));
|
|
102
|
+
// Cursor logic:
|
|
103
|
+
// We assume the cursor is currently at the END of the last rendered frame.
|
|
104
|
+
// But to diff, it's easier to always reset to the top of the frame first.
|
|
105
|
+
// 1. Move Cursor to Top of the Frame
|
|
106
|
+
if (this.lastRenderHeight > 0) {
|
|
107
|
+
this.print(`\x1b[${this.lastRenderHeight}A`); // Move up N lines
|
|
108
|
+
// Actually, if last height was 1 (just one line), we move up 1 line?
|
|
109
|
+
// "A\n" -> 2 lines. Cursor at bottom.
|
|
110
|
+
// If we move up, we are at top.
|
|
111
|
+
// Wait, if lastRenderHeight includes the "current line" which is usually empty if we printed with newlines?
|
|
112
|
+
// Let's stick to: we printed N lines. Cursor is at line N+1 (start).
|
|
113
|
+
// To go to line 1, we move up N lines.
|
|
114
|
+
this.print(`\x1b[${this.lastRenderHeight}A`);
|
|
115
|
+
}
|
|
116
|
+
this.print(ansi_1.ANSI.CURSOR_LEFT);
|
|
117
|
+
// 2. Diff and Render
|
|
118
|
+
// We iterate through newLines.
|
|
119
|
+
// For each line, check if it matches lastRenderLines[i].
|
|
120
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
121
|
+
const newLine = newLines[i];
|
|
122
|
+
const oldLine = this.lastRenderLines[i];
|
|
123
|
+
if (newLine !== oldLine) {
|
|
124
|
+
// Move to this line if not already there?
|
|
125
|
+
// We are writing sequentially, so after writing line i-1 (or skipping it),
|
|
126
|
+
// the cursor might not be at the start of line i if we skipped.
|
|
127
|
+
// Strategy:
|
|
128
|
+
// If we skipped lines, we need to jump down.
|
|
129
|
+
// But simpler: just move cursor to line i relative to top.
|
|
130
|
+
// \x1b[<N>B moves down N lines.
|
|
131
|
+
// But we are processing sequentially.
|
|
132
|
+
// If we are at line 0 (Top).
|
|
133
|
+
// Process line 0.
|
|
134
|
+
// If changed, write it + \n (or clear line + write).
|
|
135
|
+
// If unchanged, move cursor down 1 line.
|
|
136
|
+
// Wait, if we use \n at end of write, cursor moves down.
|
|
137
|
+
// If we skip writing, we must manually move down.
|
|
138
|
+
this.print(ansi_1.ANSI.ERASE_LINE); // Clear current line
|
|
139
|
+
this.print(newLine);
|
|
140
|
+
}
|
|
141
|
+
// Prepare for next line
|
|
142
|
+
if (i < newLines.length - 1) {
|
|
143
|
+
// If we wrote something, we are at end of line (maybe wrapped?).
|
|
144
|
+
// Since we truncate, we are not wrapped.
|
|
145
|
+
// But we didn't print \n yet if we just printed newLine.
|
|
146
|
+
// To move to next line start:
|
|
147
|
+
this.print('\n');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// 3. Clear remaining lines if new output is shorter
|
|
151
|
+
if (newLines.length < this.lastRenderLines.length) {
|
|
152
|
+
// We are at the last line of new output.
|
|
153
|
+
// BUG FIX: If the last line was unchanged, we skipped printing.
|
|
154
|
+
// The cursor is currently at the START of that line (or end of previous).
|
|
155
|
+
// We need to ensure we move to the NEXT line (or end of current) before clearing down.
|
|
156
|
+
// If we just finished loop `i = newLines.length - 1`, we are theoretically at the end of the content.
|
|
157
|
+
// However, since we might have skipped the last line, we need to be careful.
|
|
158
|
+
// Let's force move to the end of the visual content we just defined.
|
|
159
|
+
// Actually, simplest way: Just move cursor to start of line N (where N = newLines.length).
|
|
160
|
+
// Currently we are at line newLines.length - 1.
|
|
161
|
+
// We need to move down 1 line?
|
|
162
|
+
// If newLines has 1 line. Loop runs 0.
|
|
163
|
+
// If skipped, we are at start of line 0.
|
|
164
|
+
// We need to be at line 1 to clear from there down.
|
|
165
|
+
// But we didn't print \n.
|
|
166
|
+
// So: move cursor to (newLines.length) relative to top.
|
|
167
|
+
// We started at Top.
|
|
168
|
+
// We iterated newLines.
|
|
169
|
+
// We injected \n between lines.
|
|
170
|
+
// The cursor is implicitly tracking where we are.
|
|
171
|
+
// IF we skipped, we are physically at start of line `i`.
|
|
172
|
+
// We need to move over it.
|
|
173
|
+
// Fix: After the loop, explicitly move to the line AFTER the last line.
|
|
174
|
+
// Since we know where we started (Top), we can just jump to line `newLines.length`.
|
|
175
|
+
// But we are in relative movement land.
|
|
176
|
+
// Let's calculate where we *should* be: End of content.
|
|
177
|
+
// If we just rendered N lines, we want to be at line N+1 (conceptually) to clear below?
|
|
178
|
+
// Or just at the start of line N+1?
|
|
179
|
+
// If we have 2 lines.
|
|
180
|
+
// Line 0. \n. Line 1.
|
|
181
|
+
// Cursor is at end of Line 1.
|
|
182
|
+
// If we skipped Line 1, cursor is at start of Line 1.
|
|
183
|
+
// We want to clear everything BELOW Line 1.
|
|
184
|
+
// So we should be at start of Line 2.
|
|
185
|
+
// Logic:
|
|
186
|
+
// 1. We are currently at the cursor position after processing `newLines`.
|
|
187
|
+
// If last line was skipped, we are at start of last line.
|
|
188
|
+
// If last line was written, we are at end of last line.
|
|
189
|
+
// 2. We want to erase from the line *following* the last valid line.
|
|
190
|
+
// We can just calculate the difference and move down if needed.
|
|
191
|
+
// But simpler: Move cursor to the conceptual "end" of the new frame.
|
|
192
|
+
// If we processed `newLines.length` lines.
|
|
193
|
+
// We want to be at row `newLines.length` (0-indexed) to start clearing?
|
|
194
|
+
// No, rows are 0 to N-1.
|
|
195
|
+
// We want to clear starting from row N.
|
|
196
|
+
// Since we can't easily query cursor pos, let's use the fact we reset to Top.
|
|
197
|
+
// We can move to row N relative to current?
|
|
198
|
+
// Wait, `ERASE_DOWN` clears from cursor to end of screen.
|
|
199
|
+
// If we are at start of Line 1 (and it's valid), `ERASE_DOWN` deletes Line 1!
|
|
200
|
+
// So we MUST be past Line 1.
|
|
201
|
+
// If we skipped the last line, we must strictly move past it.
|
|
202
|
+
// How? `\x1b[1B` (Down).
|
|
203
|
+
// But we don't track if we skipped the last line explicitly outside the loop.
|
|
204
|
+
// Let's just track `currentLineIndex`.
|
|
205
|
+
// Alternate robust approach:
|
|
206
|
+
// After loop, we forcefully move cursor to `newLines.length` lines down from Top.
|
|
207
|
+
// We are currently at some unknown state (Start or End of last line).
|
|
208
|
+
// BUT we can just move UP to Top again and then move DOWN N lines.
|
|
209
|
+
// That feels safe.
|
|
210
|
+
// Reset to top of frame (which we are already inside/near).
|
|
211
|
+
// But we don't know exactly where we are relative to top anymore.
|
|
212
|
+
// Let's rely on the loop index.
|
|
213
|
+
// If loop finished, `i` was `newLines.length`.
|
|
214
|
+
// If `newLines.length > 0`.
|
|
215
|
+
// If we skipped the last line (index `len-1`), we are at start of it.
|
|
216
|
+
// If we wrote it, we are at end of it.
|
|
217
|
+
// If we skipped, we need `\x1b[1B`.
|
|
218
|
+
// If we wrote, we are at end. `ERASE_DOWN` from end of line clears rest of line + below.
|
|
219
|
+
// BUT we want to clear BELOW.
|
|
220
|
+
// `ERASE_DOWN` (J=0) clears from cursor to end of screen.
|
|
221
|
+
// If at end of line, it clears rest of that line (nothing) and lines below. Correct.
|
|
222
|
+
// So the issue is ONLY when we skipped the last line.
|
|
223
|
+
const lastLineIdx = newLines.length - 1;
|
|
224
|
+
if (lastLineIdx >= 0 && newLines[lastLineIdx] === this.lastRenderLines[lastLineIdx]) {
|
|
225
|
+
// We skipped the last line. Move down 1 line to ensure we don't delete it.
|
|
226
|
+
// Also move to start (CR) to be safe?
|
|
227
|
+
this.print('\n');
|
|
228
|
+
// Wait, \n moves down AND to start usually.
|
|
229
|
+
// But strictly \n is Line Feed (Down). \r is Carriage Return (Left).
|
|
230
|
+
// Console usually treats \n as \r\n in cooked mode, but in raw mode?
|
|
231
|
+
// We are in raw mode.
|
|
232
|
+
// We likely need \r\n or explicit movement.
|
|
233
|
+
// Let's just use \x1b[1B (Down) and \r (Left).
|
|
234
|
+
// Actually, if we use `\n` in loop, we rely on it working.
|
|
235
|
+
// Let's assume `\x1b[1B` is safer for "just move down".
|
|
236
|
+
// But wait, if we are at start of line, `1B` puts us at start of next line.
|
|
237
|
+
// `ERASE_DOWN` there is perfect.
|
|
238
|
+
this.print('\x1b[1B');
|
|
239
|
+
this.print('\r'); // Move to start
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// We wrote the last line. We are at the end of it.
|
|
243
|
+
// `ERASE_DOWN` will clear lines below.
|
|
244
|
+
// BUT if we want to clear the REST of the screen cleanly starting from next line...
|
|
245
|
+
// It's mostly fine.
|
|
246
|
+
// However, there is a subtle case:
|
|
247
|
+
// If we wrote the line, we are at the end of it.
|
|
248
|
+
// If we call ERASE_DOWN, it keeps current line intact (from cursor onwards, which is empty).
|
|
249
|
+
// And clears below.
|
|
250
|
+
// This is correct.
|
|
251
|
+
// EXCEPT: If the old screen had MORE lines, we want to clear them.
|
|
252
|
+
// If we are at end of Line N-1.
|
|
253
|
+
// Line N exists in old screen.
|
|
254
|
+
// ERASE_DOWN clears Line N etc.
|
|
255
|
+
// Correct.
|
|
256
|
+
}
|
|
257
|
+
this.print(ansi_1.ANSI.ERASE_DOWN);
|
|
258
|
+
}
|
|
259
|
+
// Update state
|
|
260
|
+
this.lastRenderLines = newLines;
|
|
261
|
+
this.lastRenderHeight = newLines.length;
|
|
262
|
+
}
|
|
263
|
+
stripAnsi(str) {
|
|
264
|
+
return (0, utils_1.stripAnsi)(str);
|
|
265
|
+
}
|
|
266
|
+
truncate(str, width) {
|
|
267
|
+
const visualWidth = (0, utils_1.stringWidth)(str);
|
|
268
|
+
if (visualWidth <= width) {
|
|
269
|
+
return str;
|
|
270
|
+
}
|
|
271
|
+
// Heuristic truncation using stringWidth
|
|
272
|
+
// We iterate and sum width until we hit limit - 3
|
|
273
|
+
let currentWidth = 0;
|
|
274
|
+
let cutIndex = 0;
|
|
275
|
+
// We need to iterate by Code Point or Grapheme to be safe?
|
|
276
|
+
// Let's use simple char iteration for speed, but respect ANSI.
|
|
277
|
+
// Actually, reusing the logic from stringWidth might be best but
|
|
278
|
+
// we need the index.
|
|
279
|
+
let inAnsi = false;
|
|
280
|
+
for (let i = 0; i < str.length; i++) {
|
|
281
|
+
const code = str.charCodeAt(i);
|
|
282
|
+
if (str[i] === '\x1b') {
|
|
283
|
+
inAnsi = true;
|
|
284
|
+
}
|
|
285
|
+
if (inAnsi) {
|
|
286
|
+
if ((str[i] >= '@' && str[i] <= '~') || (str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) {
|
|
287
|
+
inAnsi = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Width check
|
|
292
|
+
// Handle surrogates roughly (we don't need perfect width here during loop, just enough to stop)
|
|
293
|
+
// But wait, we imported `stringWidth`.
|
|
294
|
+
// We can't easily use `stringWidth` incrementally without re-parsing.
|
|
295
|
+
// Let's just trust the loop for cut index.
|
|
296
|
+
// Re-implement basic width logic here for the cut index finding
|
|
297
|
+
let charWidth = 1;
|
|
298
|
+
let cp = code;
|
|
299
|
+
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
|
|
300
|
+
const next = str.charCodeAt(i + 1);
|
|
301
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
302
|
+
cp = (code - 0xD800) * 0x400 + (next - 0xDC00) + 0x10000;
|
|
303
|
+
// i is incremented in main loop but we need to skip next char
|
|
304
|
+
// We'll handle i increment in the loop
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Check range (simplified or call helper)
|
|
308
|
+
// We don't have isWideCodePoint exported.
|
|
309
|
+
// But generally, we can just say:
|
|
310
|
+
if (cp >= 0x1100) { // Quick check for potentially wide
|
|
311
|
+
// It's acceptable to be slightly aggressive on wide chars for truncation
|
|
312
|
+
charWidth = 2;
|
|
313
|
+
}
|
|
314
|
+
if (currentWidth + charWidth > width - 3) {
|
|
315
|
+
cutIndex = i;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
currentWidth += charWidth;
|
|
319
|
+
if (cp > 0xFFFF) {
|
|
320
|
+
i++; // Skip low surrogate
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
cutIndex = i + 1;
|
|
324
|
+
}
|
|
325
|
+
return str.substring(0, cutIndex) + '...' + ansi_1.ANSI.RESET;
|
|
326
|
+
}
|
|
327
|
+
// Helper to check for arrow keys including application mode
|
|
328
|
+
isUp(char) {
|
|
329
|
+
return char === '\u001b[A' || char === '\u001bOA';
|
|
330
|
+
}
|
|
331
|
+
isDown(char) {
|
|
332
|
+
return char === '\u001b[B' || char === '\u001bOB';
|
|
333
|
+
}
|
|
334
|
+
isRight(char) {
|
|
335
|
+
return char === '\u001b[C' || char === '\u001bOC';
|
|
336
|
+
}
|
|
337
|
+
isLeft(char) {
|
|
338
|
+
return char === '\u001b[D' || char === '\u001bOD';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
exports.Prompt = Prompt;
|
package/dist/core.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions } from './types';
|
|
1
|
+
import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Public Facade for MepCLI
|
|
4
4
|
*/
|
|
@@ -15,4 +15,9 @@ export declare class MepCLI {
|
|
|
15
15
|
static password(options: TextOptions): Promise<string>;
|
|
16
16
|
static number(options: NumberOptions): Promise<number>;
|
|
17
17
|
static toggle(options: ToggleOptions): Promise<boolean>;
|
|
18
|
+
static list(options: ListOptions): Promise<string[]>;
|
|
19
|
+
static slider(options: SliderOptions): Promise<number>;
|
|
20
|
+
static date(options: DateOptions): Promise<Date>;
|
|
21
|
+
static file(options: FileOptions): Promise<string>;
|
|
22
|
+
static multiSelect(options: MultiSelectOptions): Promise<any[]>;
|
|
18
23
|
}
|