mepcli 0.5.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 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`, `autocomplete`, `sort`, `table`, and `rating`.
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.
@@ -104,7 +104,13 @@ async function main() {
104
104
  ]
105
105
  });
106
106
 
107
- console.log({ name, age, newsletter, lang, tools, stars, city, priorities, user });
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 });
108
114
  }
109
115
 
110
116
  main();
@@ -130,6 +136,9 @@ main();
130
136
  * `autocomplete(options)` - Searchable selection with async suggestions.
131
137
  * `sort(options)` - Reorder a list of items.
132
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.
133
142
  * `spinner(message)` - Returns a `Spinner` instance for manual control (`start`, `stop`, `update`, `success`, `error`).
134
143
 
135
144
  ## Mouse Support
@@ -137,7 +146,7 @@ main();
137
146
  MepCLI automatically detects modern terminals and enables **Mouse Tracking** (using SGR 1006 protocol).
138
147
 
139
148
  * **Scrolling:**
140
- * `select`, `multiSelect`, `checkbox`, `autocomplete`, `table`: Scroll to navigate the list.
149
+ * `select`, `multiSelect`, `checkbox`, `autocomplete`, `table`, `tree`: Scroll to navigate the list.
141
150
  * `number`, `slider`, `rating`, `date`: Scroll to increment/decrement values or fields.
142
151
  * `sort`: Scroll to navigate or reorder items (when grabbed).
143
152
  * `toggle`, `confirm`: Scroll to toggle the state.
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions, RatingOptions, AutocompleteOptions, SortOptions, TableOptions } 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
2
  import { Spinner } from './spinner';
3
3
  /**
4
4
  * Public Facade for MepCLI
@@ -25,4 +25,7 @@ export declare class MepCLI {
25
25
  static autocomplete<const V>(options: AutocompleteOptions<V>): Promise<V>;
26
26
  static sort(options: SortOptions): Promise<string[]>;
27
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>;
28
31
  }
package/dist/core.js CHANGED
@@ -18,6 +18,9 @@ const rating_1 = require("./prompts/rating");
18
18
  const autocomplete_1 = require("./prompts/autocomplete");
19
19
  const sort_1 = require("./prompts/sort");
20
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");
21
24
  /**
22
25
  * Public Facade for MepCLI
23
26
  */
@@ -76,6 +79,15 @@ class MepCLI {
76
79
  static table(options) {
77
80
  return new table_1.TablePrompt(options).run();
78
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
+ }
79
91
  }
80
92
  exports.MepCLI = MepCLI;
81
93
  MepCLI.theme = theme_1.theme;
@@ -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;
@@ -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
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeypressPrompt = void 0;
4
+ const ansi_1 = require("../ansi");
5
+ const base_1 = require("../base");
6
+ const theme_1 = require("../theme");
7
+ class KeypressPrompt extends base_1.Prompt {
8
+ constructor(options) {
9
+ super(options);
10
+ }
11
+ render(firstRender) {
12
+ let output = `${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}`;
13
+ if (this.options.keys) {
14
+ const hint = this.options.keys.map(k => {
15
+ if (k === '\r' || k === '\n' || k === 'enter')
16
+ return 'enter';
17
+ if (k === ' ' || k === 'space')
18
+ return 'space';
19
+ return k;
20
+ }).join('/');
21
+ // Only show hint if it's short enough to be helpful, or always?
22
+ // Let's always show it if provided, or maybe just dimmed.
23
+ output += ` ${theme_1.theme.muted}(${hint})${ansi_1.ANSI.RESET}`;
24
+ }
25
+ else {
26
+ output += ` ${theme_1.theme.muted}(Press any key)${ansi_1.ANSI.RESET}`;
27
+ }
28
+ this.renderFrame(output);
29
+ }
30
+ handleInput(char, key) {
31
+ let keyName = char;
32
+ if (char === '\r' || char === '\n')
33
+ keyName = 'enter';
34
+ else if (char === ' ')
35
+ keyName = 'space';
36
+ else if (char === '\u001b')
37
+ keyName = 'escape';
38
+ else if (char === '\t')
39
+ keyName = 'tab';
40
+ // Handle backspace
41
+ else if (char === '\x7f' || char === '\b')
42
+ keyName = 'backspace';
43
+ // Check against whitelist
44
+ if (this.options.keys) {
45
+ const allowed = this.options.keys.map(k => k.toLowerCase());
46
+ // Check normalized name or exact char
47
+ if (!allowed.includes(keyName) && !allowed.includes(char)) {
48
+ return;
49
+ }
50
+ }
51
+ if (this.options.showInvisible) {
52
+ this.print(` ${theme_1.theme.success}${keyName}${ansi_1.ANSI.RESET}`);
53
+ }
54
+ this.submit(keyName);
55
+ }
56
+ }
57
+ exports.KeypressPrompt = KeypressPrompt;
@@ -52,22 +52,22 @@ class TextPrompt extends base_1.Prompt {
52
52
  // This is tricky because segments might contain '\n'.
53
53
  // safeSplit treats '\n' as a segment.
54
54
  let cursorLineIndex = 0;
55
- let cursorSegmentIndexOnLine = 0;
56
- let currentSegmentIndex = 0;
55
+ const cursorSegmentIndexOnLine = 0;
56
+ const currentSegmentIndex = 0;
57
57
  for (let i = 0; i < lines.length; i++) {
58
58
  // How many segments in this line?
59
59
  // We can't just use lines[i].length because that's chars.
60
60
  // We need to split the line again or iterate segments.
61
61
  // Iterating segments is safer.
62
62
  // Let's assume we iterate global segments until we hit a newline segment
63
- let lineSegmentsCount = 0;
63
+ const lineSegmentsCount = 0;
64
64
  // Since rawValue.split('\n') consumes the newlines, we need to account for them.
65
65
  // Alternative: iterate this.segments
66
66
  // Find where the cursor falls.
67
67
  }
68
68
  // Let's iterate segments to find cursor position (row, col)
69
69
  cursorLineIndex = 0;
70
- let colIndex = 0; // Visual column or char index?
70
+ const colIndex = 0; // Visual column or char index?
71
71
  // If we want visual cursor position, we need visual width of segments.
72
72
  let visualColIndex = 0;
73
73
  for (let i = 0; i < this.cursor; i++) {
@@ -90,7 +90,7 @@ class TextPrompt extends base_1.Prompt {
90
90
  // Now prepare lines for display (scrolling/truncation)
91
91
  // We need to reconstruct lines from segments to apply styling/truncation logic per line.
92
92
  let currentLineSegments = [];
93
- let processedLines = []; // Array of segment arrays
93
+ const processedLines = []; // Array of segment arrays
94
94
  for (const seg of this.segments) {
95
95
  if (seg === '\n') {
96
96
  processedLines.push(currentLineSegments);
@@ -0,0 +1,20 @@
1
+ import { Prompt } from '../base';
2
+ import { TreeOptions, MouseEvent } from '../types';
3
+ export declare class TreePrompt<V> extends Prompt<V, TreeOptions<V>> {
4
+ private cursor;
5
+ private expandedNodes;
6
+ private flatList;
7
+ private scrollTop;
8
+ private readonly pageSize;
9
+ private readonly ICON_CLOSED;
10
+ private readonly ICON_OPEN;
11
+ private readonly ICON_LEAF;
12
+ constructor(options: TreeOptions<V>);
13
+ private expandPathTo;
14
+ private initializeExpanded;
15
+ private recalculateFlatList;
16
+ private traverse;
17
+ protected render(firstRender: boolean): void;
18
+ protected handleInput(char: string, key: Buffer): void;
19
+ protected handleMouse(event: MouseEvent): void;
20
+ }
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TreePrompt = 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
+ class TreePrompt extends base_1.Prompt {
9
+ constructor(options) {
10
+ super(options);
11
+ this.cursor = 0;
12
+ this.expandedNodes = new Set();
13
+ this.flatList = [];
14
+ this.scrollTop = 0;
15
+ this.pageSize = 15;
16
+ // Icons
17
+ this.ICON_CLOSED = symbols_1.symbols.pointer === '>' ? '+' : '▸';
18
+ this.ICON_OPEN = symbols_1.symbols.pointer === '>' ? '-' : '▾';
19
+ this.ICON_LEAF = symbols_1.symbols.pointer === '>' ? ' ' : ' '; // No specific icon for leaf, just indentation
20
+ this.initializeExpanded(this.options.data);
21
+ // Handle initial value
22
+ if (this.options.initial !== undefined) {
23
+ this.expandPathTo(this.options.initial);
24
+ }
25
+ this.recalculateFlatList();
26
+ if (this.options.initial !== undefined) {
27
+ const index = this.flatList.findIndex(item => item.node.value === this.options.initial);
28
+ if (index !== -1) {
29
+ this.cursor = index;
30
+ }
31
+ }
32
+ }
33
+ expandPathTo(value) {
34
+ const find = (nodes) => {
35
+ for (const node of nodes) {
36
+ if (node.value === value)
37
+ return true;
38
+ if (node.children) {
39
+ if (find(node.children)) {
40
+ this.expandedNodes.add(node);
41
+ return true;
42
+ }
43
+ }
44
+ }
45
+ return false;
46
+ };
47
+ find(this.options.data);
48
+ }
49
+ initializeExpanded(nodes) {
50
+ for (const node of nodes) {
51
+ if (node.expanded) {
52
+ this.expandedNodes.add(node);
53
+ }
54
+ if (node.children) {
55
+ this.initializeExpanded(node.children);
56
+ }
57
+ }
58
+ }
59
+ recalculateFlatList() {
60
+ this.flatList = [];
61
+ this.traverse(this.options.data, 0, null);
62
+ // Adjust cursor if it went out of bounds (e.g. collapsing a folder above cursor)
63
+ if (this.cursor >= this.flatList.length) {
64
+ this.cursor = Math.max(0, this.flatList.length - 1);
65
+ }
66
+ }
67
+ traverse(nodes, depth, parent) {
68
+ for (const node of nodes) {
69
+ this.flatList.push({
70
+ node,
71
+ depth,
72
+ parent
73
+ });
74
+ if (node.children && node.children.length > 0 && this.expandedNodes.has(node)) {
75
+ this.traverse(node.children, depth + 1, node);
76
+ }
77
+ }
78
+ }
79
+ render(firstRender) {
80
+ let output = `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}\n`;
81
+ if (this.flatList.length === 0) {
82
+ output += ` ${theme_1.theme.muted}No data${ansi_1.ANSI.RESET}`;
83
+ this.renderFrame(output);
84
+ return;
85
+ }
86
+ // Adjust Scroll
87
+ if (this.cursor < this.scrollTop) {
88
+ this.scrollTop = this.cursor;
89
+ }
90
+ else if (this.cursor >= this.scrollTop + this.pageSize) {
91
+ this.scrollTop = this.cursor - this.pageSize + 1;
92
+ }
93
+ const visible = this.flatList.slice(this.scrollTop, this.scrollTop + this.pageSize);
94
+ visible.forEach((item, index) => {
95
+ const actualIndex = this.scrollTop + index;
96
+ const isSelected = actualIndex === this.cursor;
97
+ // Indentation
98
+ const indentSize = this.options.indent || 2;
99
+ const indentation = ' '.repeat(item.depth * indentSize);
100
+ // Pointer
101
+ let linePrefix = '';
102
+ if (isSelected) {
103
+ linePrefix = `${theme_1.theme.main}${symbols_1.symbols.pointer} `;
104
+ }
105
+ else {
106
+ linePrefix = ' ';
107
+ }
108
+ // Folder Icon
109
+ let icon = ' '; // Default 2 spaces for alignment
110
+ const hasChildren = item.node.children && item.node.children.length > 0;
111
+ if (hasChildren) {
112
+ if (this.expandedNodes.has(item.node)) {
113
+ icon = `${this.ICON_OPEN} `;
114
+ }
115
+ else {
116
+ icon = `${this.ICON_CLOSED} `;
117
+ }
118
+ }
119
+ // Title
120
+ let title = item.node.title;
121
+ if (item.node.disabled) {
122
+ title = `${theme_1.theme.muted}${title} (disabled)${ansi_1.ANSI.RESET}`;
123
+ }
124
+ // Compose line
125
+ let line = `${indentation}${icon}${title}`;
126
+ if (isSelected) {
127
+ line = `${theme_1.theme.main}${line}${ansi_1.ANSI.RESET}`;
128
+ }
129
+ output += linePrefix + line;
130
+ if (index < visible.length - 1)
131
+ output += '\n';
132
+ });
133
+ this.renderFrame(output);
134
+ }
135
+ handleInput(char, key) {
136
+ if (this.flatList.length === 0)
137
+ return;
138
+ // Navigation
139
+ if (this.isUp(char)) {
140
+ this.cursor = (this.cursor - 1 + this.flatList.length) % this.flatList.length;
141
+ this.render(false);
142
+ return;
143
+ }
144
+ if (this.isDown(char)) {
145
+ this.cursor = (this.cursor + 1) % this.flatList.length;
146
+ this.render(false);
147
+ return;
148
+ }
149
+ const currentItem = this.flatList[this.cursor];
150
+ const node = currentItem.node;
151
+ const hasChildren = node.children && node.children.length > 0;
152
+ // Right: Expand or Go Down
153
+ if (this.isRight(char)) {
154
+ if (hasChildren) {
155
+ if (!this.expandedNodes.has(node)) {
156
+ this.expandedNodes.add(node);
157
+ this.recalculateFlatList();
158
+ }
159
+ else {
160
+ // Go to first child (next item in flat list)
161
+ if (this.cursor + 1 < this.flatList.length) {
162
+ this.cursor++;
163
+ }
164
+ }
165
+ this.render(false);
166
+ return;
167
+ }
168
+ }
169
+ // Left: Collapse or Go Up (Parent)
170
+ if (this.isLeft(char)) {
171
+ if (hasChildren && this.expandedNodes.has(node)) {
172
+ this.expandedNodes.delete(node);
173
+ this.recalculateFlatList();
174
+ this.render(false);
175
+ return;
176
+ }
177
+ else {
178
+ // Go to parent
179
+ if (currentItem.parent) {
180
+ const parentIndex = this.flatList.findIndex(x => x.node === currentItem.parent);
181
+ if (parentIndex !== -1) {
182
+ this.cursor = parentIndex;
183
+ this.render(false);
184
+ return;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ // Toggle (Space)
190
+ if (char === ' ') {
191
+ if (hasChildren) {
192
+ if (this.expandedNodes.has(node)) {
193
+ this.expandedNodes.delete(node);
194
+ }
195
+ else {
196
+ this.expandedNodes.add(node);
197
+ }
198
+ this.recalculateFlatList();
199
+ this.render(false);
200
+ }
201
+ return;
202
+ }
203
+ // Submit (Enter)
204
+ if (char === '\r' || char === '\n') {
205
+ if (!node.disabled) {
206
+ this.submit(node.value);
207
+ }
208
+ }
209
+ }
210
+ handleMouse(event) {
211
+ if (event.action === 'scroll') {
212
+ if (event.scroll === 'up') {
213
+ this.cursor = (this.cursor - 1 + this.flatList.length) % this.flatList.length;
214
+ this.render(false);
215
+ }
216
+ else if (event.scroll === 'down') {
217
+ this.cursor = (this.cursor + 1) % this.flatList.length;
218
+ this.render(false);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ exports.TreePrompt = TreePrompt;
package/dist/types.d.ts CHANGED
@@ -100,6 +100,11 @@ export interface AutocompleteOptions<V> extends BaseOptions {
100
100
  export interface SortOptions extends BaseOptions {
101
101
  items: string[];
102
102
  }
103
+ export interface EditorOptions extends BaseOptions {
104
+ initial?: string;
105
+ extension?: string;
106
+ waitUserInput?: boolean;
107
+ }
103
108
  export interface TableRow<V> {
104
109
  value: V;
105
110
  row: string[];
@@ -109,3 +114,19 @@ export interface TableOptions<V> extends BaseOptions {
109
114
  data: TableRow<V>[];
110
115
  rows?: number;
111
116
  }
117
+ export interface TreeNode<V> {
118
+ title: string;
119
+ value: V;
120
+ children?: TreeNode<V>[];
121
+ expanded?: boolean;
122
+ disabled?: boolean;
123
+ }
124
+ export interface TreeOptions<V> extends BaseOptions {
125
+ data: TreeNode<V>[];
126
+ initial?: V;
127
+ indent?: number;
128
+ }
129
+ export interface KeypressOptions extends BaseOptions {
130
+ keys?: string[];
131
+ showInvisible?: boolean;
132
+ }
package/dist/utils.js CHANGED
@@ -131,7 +131,11 @@ function stringWidth(str) {
131
131
  continue;
132
132
  }
133
133
  if (inAnsi) {
134
- if ((str[i] >= '@' && str[i] <= '~') || (str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) {
134
+ if (str[i] === '[') {
135
+ // Continue, this is the start of CSI
136
+ continue;
137
+ }
138
+ if ((str[i] >= '@' && str[i] <= '~')) {
135
139
  inAnsi = false;
136
140
  }
137
141
  continue;
package/example.ts CHANGED
@@ -21,14 +21,14 @@ async function runComprehensiveDemo() {
21
21
  return true;
22
22
  }
23
23
  });
24
- console.log(`\n Text Result: Project name set to '${projectName}'`);
24
+ console.log(`\n Text Result: Project name set to '${projectName}'`);
25
25
 
26
26
  // --- 2. Password Prompt (Hidden input) ---
27
27
  const apiKey = await MepCLI.password({
28
28
  message: "Enter the project's external API key:",
29
29
  placeholder: "Input will be hidden..."
30
30
  });
31
- console.log(`\n Password Result: API key entered (length: ${apiKey.length})`);
31
+ console.log(`\n Password Result: API key entered (length: ${apiKey.length})`);
32
32
 
33
33
  // --- 3. Select Prompt (Single choice, supports filtering/searching by typing) ---
34
34
  const theme = await MepCLI.select({
@@ -42,7 +42,7 @@ async function runComprehensiveDemo() {
42
42
  { title: "Monokai Pro", value: "monokai" },
43
43
  ]
44
44
  });
45
- console.log(`\n Select Result: Chosen theme is: ${theme}`);
45
+ console.log(`\n Select Result: Chosen theme is: ${theme}`);
46
46
 
47
47
  // --- 4. Checkbox Prompt (Multi-choice with Min/Max limits) ---
48
48
  const buildTools = await MepCLI.checkbox({
@@ -56,7 +56,7 @@ async function runComprehensiveDemo() {
56
56
  { title: "esbuild", value: "esbuild" }
57
57
  ]
58
58
  });
59
- console.log(`\n Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
59
+ console.log(`\n Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
60
60
 
61
61
  // --- 5. Number Prompt (Numeric input, supports Min/Max and Up/Down arrow for Step) ---
62
62
  const port = await MepCLI.number({
@@ -66,7 +66,7 @@ async function runComprehensiveDemo() {
66
66
  max: 65535,
67
67
  step: 100 // Increments/decrements by 100 with arrows
68
68
  });
69
- console.log(`\n Number Result: Server port: ${port}`);
69
+ console.log(`\n Number Result: Server port: ${port}`);
70
70
 
71
71
  // --- 6. Toggle Prompt (Boolean input, supports custom labels) ---
72
72
  const isSecure = await MepCLI.toggle({
@@ -75,7 +75,7 @@ async function runComprehensiveDemo() {
75
75
  activeText: "SECURE", // Custom 'on' label
76
76
  inactiveText: "INSECURE" // Custom 'off' label
77
77
  });
78
- console.log(`\n Toggle Result: HTTPS enabled: ${isSecure}`);
78
+ console.log(`\n Toggle Result: HTTPS enabled: ${isSecure}`);
79
79
 
80
80
  // --- 7. List / Tags Input (New) ---
81
81
  const keywords = await MepCLI.list({
@@ -83,7 +83,7 @@ async function runComprehensiveDemo() {
83
83
  initial: ["cli", "mep"],
84
84
  validate: (tags) => tags.length > 0 || "Please add at least one keyword."
85
85
  });
86
- console.log(`\n List Result: Keywords: [${keywords.join(', ')}]`);
86
+ console.log(`\n List Result: Keywords: [${keywords.join(', ')}]`);
87
87
 
88
88
  // --- 8. Slider / Scale (New) ---
89
89
  const brightness = await MepCLI.slider({
@@ -94,7 +94,7 @@ async function runComprehensiveDemo() {
94
94
  step: 5,
95
95
  unit: "%"
96
96
  });
97
- console.log(`\n Slider Result: Brightness: ${brightness}%`);
97
+ console.log(`\n Slider Result: Brightness: ${brightness}%`);
98
98
 
99
99
  // --- 9. Rating Prompt (New) ---
100
100
  const userRating = await MepCLI.rating({
@@ -103,7 +103,7 @@ async function runComprehensiveDemo() {
103
103
  max: 5,
104
104
  initial: 5
105
105
  });
106
- console.log(`\n Rating Result: You rated it: ${userRating}/5`);
106
+ console.log(`\n Rating Result: You rated it: ${userRating}/5`);
107
107
 
108
108
  // --- 10. Date / Time Picker (New) ---
109
109
  // We capture 'now' once to ensure initial >= min
@@ -113,14 +113,14 @@ async function runComprehensiveDemo() {
113
113
  initial: now,
114
114
  min: now // Cannot be in the past
115
115
  });
116
- console.log(`\n Date Result: Release set for: ${releaseDate.toLocaleString()}`);
116
+ console.log(`\n Date Result: Release set for: ${releaseDate.toLocaleString()}`);
117
117
 
118
118
  // --- 11. File Path Selector (New) ---
119
119
  const configPath = await MepCLI.file({
120
120
  message: "Select configuration file (Tab to autocomplete):",
121
121
  basePath: process.cwd()
122
122
  });
123
- console.log(`\n File Result: Path: ${configPath}`);
123
+ console.log(`\n File Result: Path: ${configPath}`);
124
124
 
125
125
  // --- 12. Multi-Select Autocomplete (New) ---
126
126
  const linters = await MepCLI.multiSelect({
@@ -135,7 +135,7 @@ async function runComprehensiveDemo() {
135
135
  ],
136
136
  min: 1
137
137
  });
138
- console.log(`\n MultiSelect Result: Linters: [${linters.join(', ')}]`);
138
+ console.log(`\n MultiSelect Result: Linters: [${linters.join(', ')}]`);
139
139
 
140
140
  // --- 13. Autocomplete Prompt (New) ---
141
141
  const city = await MepCLI.autocomplete({
@@ -155,14 +155,14 @@ async function runComprehensiveDemo() {
155
155
  return cities.filter(c => c.title.toLowerCase().includes(query.toLowerCase()));
156
156
  }
157
157
  });
158
- console.log(`\n Autocomplete Result: City code: ${city}`);
158
+ console.log(`\n Autocomplete Result: City code: ${city}`);
159
159
 
160
160
  // --- 14. Sort Prompt (New) ---
161
161
  const priorities = await MepCLI.sort({
162
162
  message: "Rank your top priorities (Space to grab/drop, Arrows to move):",
163
163
  items: ["Performance", "Security", "Features", "Usability", "Cost"]
164
164
  });
165
- console.log(`\n Sort Result: Priorities: [${priorities.join(', ')}]`);
165
+ console.log(`\n Sort Result: Priorities: [${priorities.join(', ')}]`);
166
166
 
167
167
  // --- 15. Table Prompt (New) ---
168
168
  const userId = await MepCLI.table({
@@ -175,21 +175,72 @@ async function runComprehensiveDemo() {
175
175
  { value: 4, row: ["004", "David", "Manager", "Active"] },
176
176
  ]
177
177
  });
178
- console.log(`\n Table Result: Selected User ID: ${userId}`);
178
+ console.log(`\n Table Result: Selected User ID: ${userId}`);
179
179
 
180
180
  // --- 16. Confirm Prompt (Simple Yes/No) ---
181
181
  const proceed = await MepCLI.confirm({
182
182
  message: "Ready to deploy the project now?",
183
183
  initial: true
184
184
  });
185
- console.log(`\n Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
186
-
187
- // --- 17. Spin Utility (Loading/Async Task Indicator) ---
185
+ console.log(`\n Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
186
+
187
+ // --- 17. Editor Prompt (New) ---
188
+ const bio = await MepCLI.editor({
189
+ message: "Write your biography (opens default editor):",
190
+ initial: "Hi, I am a developer...",
191
+ extension: ".md",
192
+ waitUserInput: true
193
+ });
194
+ console.log(`\n Editor Result: Biography length: ${bio.length} chars`);
195
+
196
+ // --- 18. Keypress Prompt (New) ---
197
+ console.log("\n--- Press any key to continue to the Tree Prompt Demo... ---");
198
+ const key = await MepCLI.keypress({
199
+ message: "Press any key to proceed (or 'q' to quit):",
200
+ keys: ['q', 'enter', 'space', 'escape'] // Optional whitelist, or leave undefined for any
201
+ });
202
+ console.log(`\n Keypress Result: You pressed '${key}'`);
203
+ if (key === 'q') return;
204
+
205
+ // --- 19. Tree Prompt (New) ---
206
+ const selectedFile = await MepCLI.tree({
207
+ message: "Select a file from the project structure (Space to toggle, Enter to select):",
208
+ data: [
209
+ {
210
+ title: "src",
211
+ value: "src",
212
+ children: [
213
+ { title: "index.ts", value: "src/index.ts" },
214
+ { title: "utils.ts", value: "src/utils.ts" },
215
+ {
216
+ title: "prompts",
217
+ value: "src/prompts",
218
+ expanded: true,
219
+ children: [
220
+ { title: "text.ts", value: "src/prompts/text.ts" },
221
+ { title: "select.ts", value: "src/prompts/select.ts" }
222
+ ]
223
+ }
224
+ ]
225
+ },
226
+ {
227
+ title: "package.json",
228
+ value: "package.json"
229
+ },
230
+ {
231
+ title: "README.md",
232
+ value: "README.md"
233
+ }
234
+ ]
235
+ });
236
+ console.log(`\n Tree Result: Selected path: ${selectedFile}`);
237
+
238
+ // --- 20. Spin Utility (Loading/Async Task Indicator) ---
188
239
  const s = MepCLI.spinner("Finalizing configuration and deploying...").start();
189
240
  await new Promise(resolve => setTimeout(resolve, 1500)); // Simulates a 1.5 second async task
190
241
  s.success();
191
242
 
192
- console.log("\n--- Deployment successful! All MepCLI features demonstrated! ---");
243
+ console.log("\n--- Deployment successful! All MepCLI features (including Editor) demonstrated! ---");
193
244
 
194
245
  } catch (e) {
195
246
  // Global handler for Ctrl+C closure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "0.5.0",
3
+ "version": "0.5.5",
4
4
  "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,10 @@
17
17
  "scripts": {
18
18
  "build": "tsc",
19
19
  "prepublishOnly": "npm run build",
20
- "test": "ts-node example.ts"
20
+ "test": "jest",
21
+ "demo": "ts-node example.ts",
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix"
21
24
  },
22
25
  "keywords": [
23
26
  "cli",
@@ -30,8 +33,15 @@
30
33
  "author": "CodeTease",
31
34
  "license": "MIT",
32
35
  "devDependencies": {
36
+ "@eslint/js": "^9",
37
+ "@types/jest": "^30",
33
38
  "@types/node": "^22",
39
+ "eslint": "^9",
40
+ "globals": "^17",
41
+ "jest": "^30",
42
+ "ts-jest": "^29",
34
43
  "ts-node": "^10",
35
- "typescript": "^5"
44
+ "typescript": "^5",
45
+ "typescript-eslint": "^8"
36
46
  }
37
47
  }