mepcli 0.4.0 → 0.5.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 CHANGED
@@ -5,7 +5,7 @@
5
5
  ## Features
6
6
 
7
7
  - **Zero Dependency:** Keeps your project clean and fast.
8
- - **Comprehensive Prompts:** Includes `text`, `password`, `select`, `checkbox`, `confirm`, `number`, `toggle`, `list`, `slider`, `date`, `file`, `multiSelect`, and `rating`.
8
+ - **Comprehensive Prompts:** Includes `text`, `password`, `select`, `checkbox`, `confirm`, `number`, `toggle`, `list`, `slider`, `date`, `file`, `multiSelect`, `autocomplete`, `sort`, `table`, and `rating`.
9
9
  - **Mouse Support:** Built-in support for mouse interaction (SGR 1006 protocol). Scroll to navigate lists or change values; click to select.
10
10
  - **Responsive Input:** Supports cursor movement (Left/Right) and character insertion/deletion in text-based prompts.
11
11
  - **Validation:** Built-in support for input validation (sync and async) with custom error messages.
@@ -75,7 +75,36 @@ async function main() {
75
75
  initial: 5
76
76
  });
77
77
 
78
- console.log({ name, age, newsletter, lang, tools, stars });
78
+ // Autocomplete (Search)
79
+ const city = await MepCLI.autocomplete({
80
+ message: "Search for a city:",
81
+ suggest: async (query) => {
82
+ const cities = [
83
+ { title: "New York", value: "NY" },
84
+ { title: "London", value: "LDN" },
85
+ { title: "Paris", value: "PAR" }
86
+ ];
87
+ return cities.filter(c => c.title.toLowerCase().includes(query.toLowerCase()));
88
+ }
89
+ });
90
+
91
+ // Sort (Drag & Drop)
92
+ const priorities = await MepCLI.sort({
93
+ message: "Rank priorities:",
94
+ items: ["Speed", "Quality", "Cost"]
95
+ });
96
+
97
+ // Table (Data selection)
98
+ const user = await MepCLI.table({
99
+ message: "Select a user:",
100
+ columns: ["ID", "Name"],
101
+ data: [
102
+ { value: 1, row: ["001", "Alice"] },
103
+ { value: 2, row: ["002", "Bob"] }
104
+ ]
105
+ });
106
+
107
+ console.log({ name, age, newsletter, lang, tools, stars, city, priorities, user });
79
108
  }
80
109
 
81
110
  main();
@@ -98,15 +127,19 @@ main();
98
127
  * `rating(options)` - Star rating input.
99
128
  * `date(options)` - Date and time picker.
100
129
  * `file(options)` - File system navigator and selector.
101
- * `spin(message, promise)` - Display a spinner while waiting for a promise.
130
+ * `autocomplete(options)` - Searchable selection with async suggestions.
131
+ * `sort(options)` - Reorder a list of items.
132
+ * `table(options)` - Display data in columns and select rows.
133
+ * `spinner(message)` - Returns a `Spinner` instance for manual control (`start`, `stop`, `update`, `success`, `error`).
102
134
 
103
135
  ## Mouse Support
104
136
 
105
137
  MepCLI automatically detects modern terminals and enables **Mouse Tracking** (using SGR 1006 protocol).
106
138
 
107
139
  * **Scrolling:**
108
- * `select`, `multiSelect`, `checkbox`: Scroll to navigate the list.
140
+ * `select`, `multiSelect`, `checkbox`, `autocomplete`, `table`: Scroll to navigate the list.
109
141
  * `number`, `slider`, `rating`, `date`: Scroll to increment/decrement values or fields.
142
+ * `sort`: Scroll to navigate or reorder items (when grabbed).
110
143
  * `toggle`, `confirm`: Scroll to toggle the state.
111
144
  * **Configuration:**
112
145
  * Mouse support is enabled by default if the terminal supports it.
package/dist/core.d.ts CHANGED
@@ -1,13 +1,14 @@
1
- import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions, RatingOptions } from './types';
1
+ import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions, ListOptions, SliderOptions, DateOptions, FileOptions, MultiSelectOptions, RatingOptions, AutocompleteOptions, SortOptions, TableOptions } from './types';
2
+ import { Spinner } from './spinner';
2
3
  /**
3
4
  * Public Facade for MepCLI
4
5
  */
5
6
  export declare class MepCLI {
6
7
  static theme: ThemeConfig;
7
8
  /**
8
- * Shows a spinner while a promise is pending.
9
+ * Creates a new Spinner instance.
9
10
  */
10
- static spin<T>(message: string, taskPromise: Promise<T>): Promise<T>;
11
+ static spinner(message: string): Spinner;
11
12
  static text(options: TextOptions): Promise<string>;
12
13
  static select<const V>(options: SelectOptions<V>): Promise<V>;
13
14
  static checkbox<const V>(options: CheckboxOptions<V>): Promise<V[]>;
@@ -21,4 +22,7 @@ export declare class MepCLI {
21
22
  static file(options: FileOptions): Promise<string>;
22
23
  static multiSelect<const V>(options: MultiSelectOptions<V>): Promise<V[]>;
23
24
  static rating(options: RatingOptions): Promise<number>;
25
+ static autocomplete<const V>(options: AutocompleteOptions<V>): Promise<V>;
26
+ static sort(options: SortOptions): Promise<string[]>;
27
+ static table<const V>(options: TableOptions<V>): Promise<V>;
24
28
  }
package/dist/core.js CHANGED
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MepCLI = void 0;
4
- const ansi_1 = require("./ansi");
5
4
  const theme_1 = require("./theme");
6
- const symbols_1 = require("./symbols");
5
+ const spinner_1 = require("./spinner");
7
6
  const text_1 = require("./prompts/text");
8
7
  const select_1 = require("./prompts/select");
9
8
  const checkbox_1 = require("./prompts/checkbox");
@@ -16,34 +15,18 @@ const date_1 = require("./prompts/date");
16
15
  const file_1 = require("./prompts/file");
17
16
  const multi_select_1 = require("./prompts/multi-select");
18
17
  const rating_1 = require("./prompts/rating");
18
+ const autocomplete_1 = require("./prompts/autocomplete");
19
+ const sort_1 = require("./prompts/sort");
20
+ const table_1 = require("./prompts/table");
19
21
  /**
20
22
  * Public Facade for MepCLI
21
23
  */
22
24
  class MepCLI {
23
25
  /**
24
- * Shows a spinner while a promise is pending.
26
+ * Creates a new Spinner instance.
25
27
  */
26
- static async spin(message, taskPromise) {
27
- const frames = symbols_1.symbols.spinner;
28
- let i = 0;
29
- process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
30
- const interval = setInterval(() => {
31
- process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.main}${frames[i]}${ansi_1.ANSI.RESET} ${message}`);
32
- i = (i + 1) % frames.length;
33
- }, 80);
34
- try {
35
- const result = await taskPromise;
36
- clearInterval(interval);
37
- process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}${symbols_1.symbols.tick}${ansi_1.ANSI.RESET} ${message}\n`);
38
- process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
39
- return result;
40
- }
41
- catch (error) {
42
- clearInterval(interval);
43
- process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.error}${symbols_1.symbols.cross}${ansi_1.ANSI.RESET} ${message}\n`);
44
- process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
45
- throw error;
46
- }
28
+ static spinner(message) {
29
+ return new spinner_1.Spinner(message);
47
30
  }
48
31
  static text(options) {
49
32
  return new text_1.TextPrompt(options).run();
@@ -84,6 +67,15 @@ class MepCLI {
84
67
  static rating(options) {
85
68
  return new rating_1.RatingPrompt(options).run();
86
69
  }
70
+ static autocomplete(options) {
71
+ return new autocomplete_1.AutocompletePrompt(options).run();
72
+ }
73
+ static sort(options) {
74
+ return new sort_1.SortPrompt(options).run();
75
+ }
76
+ static table(options) {
77
+ return new table_1.TablePrompt(options).run();
78
+ }
87
79
  }
88
80
  exports.MepCLI = MepCLI;
89
81
  MepCLI.theme = theme_1.theme;
@@ -0,0 +1,22 @@
1
+ import { Prompt } from '../base';
2
+ import { AutocompleteOptions, MouseEvent } from '../types';
3
+ export declare class AutocompletePrompt<V> extends Prompt<V, AutocompleteOptions<V>> {
4
+ private input;
5
+ private choices;
6
+ private selectedIndex;
7
+ private loading;
8
+ private debounceTimer?;
9
+ private spinnerFrame;
10
+ private spinnerTimer?;
11
+ private scrollTop;
12
+ private readonly pageSize;
13
+ private hasSearched;
14
+ constructor(options: AutocompleteOptions<V>);
15
+ private search;
16
+ private startSpinner;
17
+ private stopSpinner;
18
+ protected cleanup(): void;
19
+ protected render(firstRender: boolean): void;
20
+ protected handleInput(char: string): void;
21
+ protected handleMouse(event: MouseEvent): void;
22
+ }
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AutocompletePrompt = void 0;
4
+ const ansi_1 = require("../ansi");
5
+ const base_1 = require("../base");
6
+ const theme_1 = require("../theme");
7
+ const symbols_1 = require("../symbols");
8
+ const utils_1 = require("../utils");
9
+ class AutocompletePrompt extends base_1.Prompt {
10
+ constructor(options) {
11
+ super(options);
12
+ this.input = '';
13
+ this.choices = [];
14
+ this.selectedIndex = 0;
15
+ this.loading = false;
16
+ this.spinnerFrame = 0;
17
+ this.scrollTop = 0;
18
+ this.pageSize = 7;
19
+ this.hasSearched = false;
20
+ this.input = options.initial || '';
21
+ this.search(this.input);
22
+ }
23
+ search(query) {
24
+ if (this.debounceTimer)
25
+ clearTimeout(this.debounceTimer);
26
+ this.loading = true;
27
+ this.startSpinner();
28
+ this.render(false);
29
+ this.debounceTimer = setTimeout(async () => {
30
+ try {
31
+ const results = await this.options.suggest(query);
32
+ if (query !== this.input)
33
+ return;
34
+ this.choices = results.slice(0, this.options.limit || 10);
35
+ this.selectedIndex = 0;
36
+ this.scrollTop = 0;
37
+ this.hasSearched = true;
38
+ }
39
+ catch (err) {
40
+ this.choices = [];
41
+ }
42
+ finally {
43
+ this.loading = false;
44
+ this.stopSpinner();
45
+ this.render(false);
46
+ }
47
+ }, 300); // 300ms debounce
48
+ }
49
+ startSpinner() {
50
+ if (this.spinnerTimer)
51
+ return;
52
+ this.spinnerTimer = setInterval(() => {
53
+ this.spinnerFrame = (this.spinnerFrame + 1) % symbols_1.symbols.spinner.length;
54
+ this.render(false);
55
+ }, 80);
56
+ }
57
+ stopSpinner() {
58
+ if (this.spinnerTimer) {
59
+ clearInterval(this.spinnerTimer);
60
+ this.spinnerTimer = undefined;
61
+ }
62
+ }
63
+ cleanup() {
64
+ this.stopSpinner();
65
+ super.cleanup();
66
+ }
67
+ render(firstRender) {
68
+ // --- FIX START: Restore cursor position ---
69
+ // renderFrame() in Base assumes the cursor is at the bottom of the output.
70
+ // Since Autocomplete moves the cursor to the top (for input) after rendering,
71
+ // we must manually move it back down before the next render cycle.
72
+ if (this.lastRenderHeight > 1) {
73
+ this.print(`\x1b[${this.lastRenderHeight - 1}B`); // Move down
74
+ }
75
+ // --- FIX END ---
76
+ let output = '';
77
+ // Header
78
+ const icon = this.loading ? `${theme_1.theme.main}${symbols_1.symbols.spinner[this.spinnerFrame]}${ansi_1.ANSI.RESET}` : `${theme_1.theme.success}?${ansi_1.ANSI.RESET}`;
79
+ output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${this.input}`;
80
+ output += '\n';
81
+ if (this.loading) {
82
+ output += ` ${theme_1.theme.muted}Searching...${ansi_1.ANSI.RESET}`;
83
+ }
84
+ else if (this.choices.length === 0 && this.hasSearched) {
85
+ output += ` ${theme_1.theme.muted}${this.options.fallback || 'No results found.'}${ansi_1.ANSI.RESET}`;
86
+ }
87
+ else if (this.choices.length > 0) {
88
+ // Render list similar to SelectPrompt
89
+ if (this.selectedIndex < this.scrollTop) {
90
+ this.scrollTop = this.selectedIndex;
91
+ }
92
+ else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
93
+ this.scrollTop = this.selectedIndex - this.pageSize + 1;
94
+ }
95
+ const visibleChoices = this.choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
96
+ visibleChoices.forEach((choice, index) => {
97
+ const actualIndex = this.scrollTop + index;
98
+ if (index > 0)
99
+ output += '\n';
100
+ if (actualIndex === this.selectedIndex) {
101
+ output += `${theme_1.theme.main}${symbols_1.symbols.pointer} ${choice.title}${ansi_1.ANSI.RESET}`;
102
+ }
103
+ else {
104
+ output += ` ${choice.title}`;
105
+ }
106
+ });
107
+ }
108
+ this.renderFrame(output);
109
+ // Position cursor at the end of the input string on the first line
110
+ const lines = output.split('\n');
111
+ const height = lines.length;
112
+ // Move up (height - 1)
113
+ if (height > 1) {
114
+ this.print(`\x1b[${height - 1}A`);
115
+ }
116
+ this.print(`\r`);
117
+ const firstLine = lines[0];
118
+ const visualLen = (0, utils_1.stringWidth)(this.stripAnsi(firstLine));
119
+ this.print(`\x1b[${visualLen}C`);
120
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
121
+ }
122
+ handleInput(char) {
123
+ // Enter
124
+ if (char === '\r' || char === '\n') {
125
+ if (this.choices.length > 0) {
126
+ this.submit(this.choices[this.selectedIndex].value);
127
+ }
128
+ return;
129
+ }
130
+ // Backspace
131
+ if (char === '\u0008' || char === '\x7f') {
132
+ if (this.input.length > 0) {
133
+ this.input = this.input.slice(0, -1);
134
+ this.search(this.input);
135
+ }
136
+ return;
137
+ }
138
+ // Up
139
+ if (this.isUp(char)) {
140
+ if (this.choices.length > 0) {
141
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.choices.length - 1;
142
+ this.render(false);
143
+ }
144
+ return;
145
+ }
146
+ // Down
147
+ if (this.isDown(char)) {
148
+ if (this.choices.length > 0) {
149
+ this.selectedIndex = this.selectedIndex < this.choices.length - 1 ? this.selectedIndex + 1 : 0;
150
+ this.render(false);
151
+ }
152
+ return;
153
+ }
154
+ // Typing
155
+ if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
156
+ this.input += char;
157
+ this.search(this.input);
158
+ }
159
+ }
160
+ handleMouse(event) {
161
+ if (this.choices.length === 0)
162
+ return;
163
+ if (event.action === 'scroll') {
164
+ if (event.scroll === 'up') {
165
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.choices.length - 1;
166
+ this.render(false);
167
+ }
168
+ else if (event.scroll === 'down') {
169
+ this.selectedIndex = this.selectedIndex < this.choices.length - 1 ? this.selectedIndex + 1 : 0;
170
+ this.render(false);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ exports.AutocompletePrompt = AutocompletePrompt;
@@ -51,10 +51,6 @@ class CheckboxPrompt extends base_1.Prompt {
51
51
  // Indication of more items
52
52
  if (choices.length > this.pageSize) {
53
53
  const progress = ` ${this.scrollTop + 1}-${Math.min(this.scrollTop + this.pageSize, choices.length)} of ${choices.length}`;
54
- // Maybe add this to the header or footer?
55
- // Let's add it to footer or header. Adding to header is cleaner.
56
- // But I already wrote header.
57
- // Let's just append it at the bottom if I want, or ignore for now to keep UI minimal.
58
54
  }
59
55
  if (this.errorMsg) {
60
56
  output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
@@ -131,12 +131,6 @@ class FilePrompt extends base_1.Prompt {
131
131
  // Move right
132
132
  const prefix = `${icon} ${theme_1.theme.title}${this.options.message} `;
133
133
  const prefixLen = this.stripAnsi(prefix).length;
134
- // Cursor is usually at the end of input unless we add backspace support etc.
135
- // The cursor property tracks it, but my handleInput simplified it.
136
- // Let's rely on this.input.length for now since handleInput appends.
137
- // Ah, handleInput logic below supports cursor pos theoretically but I only see appending?
138
- // Actually handleInput doesn't support left/right in the original code, it supports down/up for suggestions.
139
- // So cursor is always at end.
140
134
  const targetCol = prefixLen + this.input.length;
141
135
  this.print(ansi_1.ANSI.CURSOR_LEFT);
142
136
  if (targetCol > 0)
@@ -0,0 +1,14 @@
1
+ import { Prompt } from '../base';
2
+ import { SortOptions, MouseEvent } from '../types';
3
+ export declare class SortPrompt extends Prompt<string[], SortOptions> {
4
+ private items;
5
+ private selectedIndex;
6
+ private grabbedIndex;
7
+ private scrollTop;
8
+ private readonly pageSize;
9
+ constructor(options: SortOptions);
10
+ protected render(firstRender: boolean): void;
11
+ protected handleInput(char: string): void;
12
+ private swap;
13
+ protected handleMouse(event: MouseEvent): void;
14
+ }
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SortPrompt = 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 SortPrompt extends base_1.Prompt {
9
+ constructor(options) {
10
+ super(options);
11
+ this.selectedIndex = 0;
12
+ this.grabbedIndex = null;
13
+ this.scrollTop = 0;
14
+ this.pageSize = 10;
15
+ this.items = [...options.items];
16
+ }
17
+ render(firstRender) {
18
+ // Adjust Scroll Top
19
+ if (this.selectedIndex < this.scrollTop) {
20
+ this.scrollTop = this.selectedIndex;
21
+ }
22
+ else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
23
+ this.scrollTop = this.selectedIndex - this.pageSize + 1;
24
+ }
25
+ // Ensure valid scroll (handle list shrinking?) - list doesn't shrink here but good practice
26
+ this.scrollTop = Math.max(0, Math.min(this.scrollTop, this.items.length - this.pageSize));
27
+ if (this.scrollTop < 0)
28
+ this.scrollTop = 0;
29
+ let output = '';
30
+ // Header
31
+ output += `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${theme_1.theme.muted}(Press <space> to grab, arrows to move, <enter> to confirm)${ansi_1.ANSI.RESET}\n`;
32
+ const visibleItems = this.items.slice(this.scrollTop, this.scrollTop + this.pageSize);
33
+ visibleItems.forEach((item, index) => {
34
+ const actualIndex = this.scrollTop + index;
35
+ if (index > 0)
36
+ output += '\n';
37
+ const isSelected = actualIndex === this.selectedIndex;
38
+ const isGrabbed = actualIndex === this.grabbedIndex;
39
+ // Pointer
40
+ let prefix = ' ';
41
+ if (isSelected) {
42
+ if (isGrabbed) {
43
+ prefix = `${theme_1.theme.main}${symbols_1.symbols.pointer}${symbols_1.symbols.pointer} `; // Double pointer for grabbed?
44
+ }
45
+ else {
46
+ prefix = `${theme_1.theme.main}${symbols_1.symbols.pointer} `;
47
+ }
48
+ }
49
+ else if (isGrabbed) {
50
+ // Should not happen if we move grabbed item with cursor
51
+ // But if logic differs, maybe.
52
+ }
53
+ // Item Content
54
+ let content = item;
55
+ if (isGrabbed) {
56
+ content = `${ansi_1.ANSI.BOLD}${theme_1.theme.main}${content}${ansi_1.ANSI.RESET}`;
57
+ }
58
+ else if (isSelected) {
59
+ content = `${theme_1.theme.main}${content}${ansi_1.ANSI.RESET}`;
60
+ }
61
+ // Index indicator? Maybe not needed, minimalist.
62
+ output += `${prefix}${content}`;
63
+ });
64
+ this.renderFrame(output);
65
+ }
66
+ handleInput(char) {
67
+ // Enter
68
+ if (char === '\r' || char === '\n') {
69
+ this.submit(this.items);
70
+ return;
71
+ }
72
+ // Space (Grab/Drop)
73
+ if (char === ' ') {
74
+ if (this.grabbedIndex === null) {
75
+ this.grabbedIndex = this.selectedIndex;
76
+ }
77
+ else {
78
+ this.grabbedIndex = null;
79
+ }
80
+ this.render(false);
81
+ return;
82
+ }
83
+ // Up
84
+ if (this.isUp(char)) {
85
+ if (this.grabbedIndex !== null) {
86
+ // Move item up
87
+ if (this.selectedIndex > 0) {
88
+ const newIndex = this.selectedIndex - 1;
89
+ this.swap(this.selectedIndex, newIndex);
90
+ this.selectedIndex = newIndex;
91
+ this.grabbedIndex = newIndex;
92
+ }
93
+ }
94
+ else {
95
+ // Move cursor up
96
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.items.length - 1;
97
+ }
98
+ this.render(false);
99
+ return;
100
+ }
101
+ // Down
102
+ if (this.isDown(char)) {
103
+ if (this.grabbedIndex !== null) {
104
+ // Move item down
105
+ if (this.selectedIndex < this.items.length - 1) {
106
+ const newIndex = this.selectedIndex + 1;
107
+ this.swap(this.selectedIndex, newIndex);
108
+ this.selectedIndex = newIndex;
109
+ this.grabbedIndex = newIndex;
110
+ }
111
+ }
112
+ else {
113
+ // Move cursor down
114
+ this.selectedIndex = this.selectedIndex < this.items.length - 1 ? this.selectedIndex + 1 : 0;
115
+ }
116
+ this.render(false);
117
+ return;
118
+ }
119
+ }
120
+ swap(i, j) {
121
+ [this.items[i], this.items[j]] = [this.items[j], this.items[i]];
122
+ }
123
+ // Mouse support?
124
+ // Drag and drop is hard with just clicks/scroll.
125
+ // Maybe click to grab, scroll to move?
126
+ handleMouse(event) {
127
+ // Simple scroll support for navigation
128
+ if (event.action === 'scroll') {
129
+ if (event.scroll === 'up') {
130
+ if (this.grabbedIndex !== null) {
131
+ if (this.selectedIndex > 0) {
132
+ const newIndex = this.selectedIndex - 1;
133
+ this.swap(this.selectedIndex, newIndex);
134
+ this.selectedIndex = newIndex;
135
+ this.grabbedIndex = newIndex;
136
+ }
137
+ }
138
+ else {
139
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.items.length - 1;
140
+ }
141
+ this.render(false);
142
+ }
143
+ else if (event.scroll === 'down') {
144
+ if (this.grabbedIndex !== null) {
145
+ if (this.selectedIndex < this.items.length - 1) {
146
+ const newIndex = this.selectedIndex + 1;
147
+ this.swap(this.selectedIndex, newIndex);
148
+ this.selectedIndex = newIndex;
149
+ this.grabbedIndex = newIndex;
150
+ }
151
+ }
152
+ else {
153
+ this.selectedIndex = this.selectedIndex < this.items.length - 1 ? this.selectedIndex + 1 : 0;
154
+ }
155
+ this.render(false);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ exports.SortPrompt = SortPrompt;
@@ -0,0 +1,14 @@
1
+ import { Prompt } from '../base';
2
+ import { TableOptions, MouseEvent } from '../types';
3
+ export declare class TablePrompt<V> extends Prompt<V, TableOptions<V>> {
4
+ private selectedIndex;
5
+ private scrollTop;
6
+ private readonly pageSize;
7
+ private colWidths;
8
+ constructor(options: TableOptions<V>);
9
+ private calculateColWidths;
10
+ protected render(firstRender: boolean): void;
11
+ private pad;
12
+ protected handleInput(char: string): void;
13
+ protected handleMouse(event: MouseEvent): void;
14
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TablePrompt = 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 TablePrompt extends base_1.Prompt {
10
+ constructor(options) {
11
+ super(options);
12
+ this.selectedIndex = 0;
13
+ this.scrollTop = 0;
14
+ this.colWidths = [];
15
+ this.pageSize = options.rows || 7;
16
+ this.calculateColWidths();
17
+ }
18
+ calculateColWidths() {
19
+ const { columns, data } = this.options;
20
+ this.colWidths = columns.map(c => (0, utils_1.stringWidth)(c));
21
+ data.forEach(row => {
22
+ row.row.forEach((cell, idx) => {
23
+ if (idx < this.colWidths.length) {
24
+ this.colWidths[idx] = Math.max(this.colWidths[idx], (0, utils_1.stringWidth)(cell));
25
+ }
26
+ });
27
+ });
28
+ // Add padding
29
+ this.colWidths = this.colWidths.map(w => w + 2);
30
+ }
31
+ render(firstRender) {
32
+ // Scroll Logic
33
+ if (this.selectedIndex < this.scrollTop) {
34
+ this.scrollTop = this.selectedIndex;
35
+ }
36
+ else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
37
+ this.scrollTop = this.selectedIndex - this.pageSize + 1;
38
+ }
39
+ const maxScroll = Math.max(0, this.options.data.length - this.pageSize);
40
+ this.scrollTop = Math.min(this.scrollTop, maxScroll);
41
+ let output = '';
42
+ // Title
43
+ 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`;
44
+ // Table Header
45
+ let headerStr = ' '; // Indent for pointer space
46
+ this.options.columns.forEach((col, i) => {
47
+ headerStr += this.pad(col, this.colWidths[i]);
48
+ });
49
+ output += `${ansi_1.ANSI.BOLD}${headerStr}${ansi_1.ANSI.RESET}\n`;
50
+ // Table Body
51
+ const visibleRows = this.options.data.slice(this.scrollTop, this.scrollTop + this.pageSize);
52
+ visibleRows.forEach((item, index) => {
53
+ const actualIndex = this.scrollTop + index;
54
+ if (index > 0)
55
+ output += '\n';
56
+ const isSelected = actualIndex === this.selectedIndex;
57
+ const pointer = isSelected ? `${theme_1.theme.main}${symbols_1.symbols.pointer}${ansi_1.ANSI.RESET} ` : ' ';
58
+ let rowStr = '';
59
+ item.row.forEach((cell, colIdx) => {
60
+ const width = this.colWidths[colIdx];
61
+ let cellStr = this.pad(cell, width);
62
+ if (isSelected) {
63
+ cellStr = `${theme_1.theme.main}${cellStr}${ansi_1.ANSI.RESET}`;
64
+ }
65
+ rowStr += cellStr;
66
+ });
67
+ output += `${pointer}${rowStr}`;
68
+ });
69
+ this.renderFrame(output);
70
+ }
71
+ pad(str, width) {
72
+ const len = (0, utils_1.stringWidth)(str);
73
+ if (len >= width)
74
+ return str;
75
+ return str + ' '.repeat(width - len);
76
+ }
77
+ handleInput(char) {
78
+ if (char === '\r' || char === '\n') {
79
+ this.submit(this.options.data[this.selectedIndex].value);
80
+ return;
81
+ }
82
+ if (this.isUp(char)) {
83
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.data.length - 1;
84
+ this.render(false);
85
+ return;
86
+ }
87
+ if (this.isDown(char)) {
88
+ this.selectedIndex = this.selectedIndex < this.options.data.length - 1 ? this.selectedIndex + 1 : 0;
89
+ this.render(false);
90
+ return;
91
+ }
92
+ }
93
+ handleMouse(event) {
94
+ if (event.action === 'scroll') {
95
+ if (event.scroll === 'up') {
96
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.data.length - 1;
97
+ this.render(false);
98
+ }
99
+ else if (event.scroll === 'down') {
100
+ this.selectedIndex = this.selectedIndex < this.options.data.length - 1 ? this.selectedIndex + 1 : 0;
101
+ this.render(false);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ exports.TablePrompt = TablePrompt;
@@ -118,25 +118,6 @@ class TextPrompt extends base_1.Prompt {
118
118
  // But cursorRelativeCol is global? No, we reset it on newline.
119
119
  // So cursorRelativeCol above was correct for the current line.
120
120
  if (isCursorLine) {
121
- // Check if we need to scroll
122
- // We need visual width of the line up to cursor.
123
- // cursorRelativeCol holds that.
124
- // If visual position > maxContentLen, we scroll.
125
- // This logic is similar to before but needs to use widths.
126
- // For simplicity, let's stick to the previous slice logic but apply it to SEGMENTS if possible.
127
- // But slicing segments for display is safer.
128
- // Let's implement simple tail truncation for now to keep it robust.
129
- // Ideally we scroll, but scrolling with variable width chars is complex.
130
- // "Good Enough": if it fits, show it. If not, truncate end.
131
- // If cursor is beyond end, scroll (slice from left).
132
- // Simplified: just show visibleLine truncated by base class renderFrame?
133
- // But renderFrame truncates blindly. We want the cursor visible.
134
- // Let's leave scrolling out for this specific "Backspace" fix task unless it's critical.
135
- // The user asked for "Backspace Emoji fix".
136
- // The scrolling logic is secondary but important.
137
- // I will preserve the existing simple scrolling logic but using segments?
138
- // No, let's just use the string for display and let renderFrame truncate.
139
- // Fix: Ensure we don't crash or show garbage.
140
121
  }
141
122
  displayValueLines.push(theme_1.theme.main + visibleLine + ansi_1.ANSI.RESET);
142
123
  });
@@ -0,0 +1,33 @@
1
+ export declare class Spinner {
2
+ private text;
3
+ private timer?;
4
+ private frameIndex;
5
+ private isSpinning;
6
+ constructor(text: string);
7
+ /**
8
+ * Starts the spinner animation.
9
+ */
10
+ start(): this;
11
+ /**
12
+ * Stops the spinner animation.
13
+ */
14
+ stop(): this;
15
+ /**
16
+ * Updates the spinner text.
17
+ */
18
+ update(text: string): this;
19
+ /**
20
+ * Stops the spinner and shows a success message.
21
+ */
22
+ success(message?: string): this;
23
+ /**
24
+ * Stops the spinner and shows an error message.
25
+ */
26
+ error(message?: string): this;
27
+ /**
28
+ * Stops the spinner and clears the line.
29
+ */
30
+ clear(): this;
31
+ private render;
32
+ private handleSignal;
33
+ }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Spinner = void 0;
4
+ const ansi_1 = require("./ansi");
5
+ const symbols_1 = require("./symbols");
6
+ const theme_1 = require("./theme");
7
+ class Spinner {
8
+ constructor(text) {
9
+ this.frameIndex = 0;
10
+ this.isSpinning = false;
11
+ this.handleSignal = () => {
12
+ this.stop();
13
+ process.exit(0);
14
+ };
15
+ this.text = text;
16
+ }
17
+ /**
18
+ * Starts the spinner animation.
19
+ */
20
+ start() {
21
+ if (this.isSpinning)
22
+ return this;
23
+ this.isSpinning = true;
24
+ process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
25
+ // Render immediately
26
+ this.render();
27
+ // Start loop
28
+ this.timer = setInterval(() => {
29
+ this.render();
30
+ }, 80);
31
+ // Register signal handler to restore cursor on Ctrl+C
32
+ process.on('SIGINT', this.handleSignal);
33
+ return this;
34
+ }
35
+ /**
36
+ * Stops the spinner animation.
37
+ */
38
+ stop() {
39
+ if (this.timer) {
40
+ clearInterval(this.timer);
41
+ this.timer = undefined;
42
+ }
43
+ if (this.isSpinning) {
44
+ this.isSpinning = false;
45
+ process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
46
+ process.removeListener('SIGINT', this.handleSignal);
47
+ }
48
+ return this;
49
+ }
50
+ /**
51
+ * Updates the spinner text.
52
+ */
53
+ update(text) {
54
+ this.text = text;
55
+ return this;
56
+ }
57
+ /**
58
+ * Stops the spinner and shows a success message.
59
+ */
60
+ success(message) {
61
+ this.stop();
62
+ const text = message ?? this.text;
63
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${theme_1.theme.success}${symbols_1.symbols.tick}${ansi_1.ANSI.RESET} ${text}\n`);
64
+ return this;
65
+ }
66
+ /**
67
+ * Stops the spinner and shows an error message.
68
+ */
69
+ error(message) {
70
+ this.stop();
71
+ const text = message ?? this.text;
72
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${theme_1.theme.error}${symbols_1.symbols.cross}${ansi_1.ANSI.RESET} ${text}\n`);
73
+ return this;
74
+ }
75
+ /**
76
+ * Stops the spinner and clears the line.
77
+ */
78
+ clear() {
79
+ this.stop();
80
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
81
+ return this;
82
+ }
83
+ render() {
84
+ const frame = symbols_1.symbols.spinner[this.frameIndex];
85
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${theme_1.theme.main}${frame}${ansi_1.ANSI.RESET} ${this.text}`);
86
+ this.frameIndex = (this.frameIndex + 1) % symbols_1.symbols.spinner.length;
87
+ }
88
+ }
89
+ exports.Spinner = Spinner;
package/dist/types.d.ts CHANGED
@@ -91,3 +91,21 @@ export interface FileOptions extends BaseOptions {
91
91
  }
92
92
  export interface MultiSelectOptions<V> extends CheckboxOptions<V> {
93
93
  }
94
+ export interface AutocompleteOptions<V> extends BaseOptions {
95
+ suggest: (input: string) => Promise<SelectChoice<V>[]>;
96
+ limit?: number;
97
+ fallback?: string;
98
+ initial?: string;
99
+ }
100
+ export interface SortOptions extends BaseOptions {
101
+ items: string[];
102
+ }
103
+ export interface TableRow<V> {
104
+ value: V;
105
+ row: string[];
106
+ }
107
+ export interface TableOptions<V> extends BaseOptions {
108
+ columns: string[];
109
+ data: TableRow<V>[];
110
+ rows?: number;
111
+ }
package/example.ts CHANGED
@@ -4,11 +4,11 @@ import { MepCLI } from './src'; // Or 'mepcli' if installed via NPM
4
4
  * Runs a comprehensive demo showcasing all MepCLI prompt types and utilities.
5
5
  * This demonstrates all core functionalities including Text, Password, Select,
6
6
  * Checkbox, Number, Toggle, Confirm, List, Slider, Date, File, MultiSelect,
7
- * and the Spin utility.
7
+ * Autocomplete, Sort, Table, and the Spin utility.
8
8
  */
9
9
  async function runComprehensiveDemo() {
10
10
  console.clear();
11
- console.log("--- MepCLI Comprehensive Demo (All 12 Prompts + Spin Utility) ---\n");
11
+ console.log("--- MepCLI Comprehensive Demo (All 15 Prompts + Spin Utility) ---\n");
12
12
 
13
13
  try {
14
14
  // --- 1. Text Prompt (Input with Validation and initial value) ---
@@ -137,18 +137,58 @@ async function runComprehensiveDemo() {
137
137
  });
138
138
  console.log(`\n✅ MultiSelect Result: Linters: [${linters.join(', ')}]`);
139
139
 
140
- // --- 13. Confirm Prompt (Simple Yes/No) ---
140
+ // --- 13. Autocomplete Prompt (New) ---
141
+ const city = await MepCLI.autocomplete({
142
+ message: "Search for a city (simulated async):",
143
+ suggest: async (query) => {
144
+ const cities = [
145
+ { title: "New York", value: "NY" },
146
+ { title: "London", value: "LDN" },
147
+ { title: "Paris", value: "PAR" },
148
+ { title: "Tokyo", value: "TKY" },
149
+ { title: "Berlin", value: "BER" },
150
+ { title: "San Francisco", value: "SF" },
151
+ { title: "Toronto", value: "TOR" }
152
+ ];
153
+ // Simulate delay
154
+ await new Promise(r => setTimeout(r, 400));
155
+ return cities.filter(c => c.title.toLowerCase().includes(query.toLowerCase()));
156
+ }
157
+ });
158
+ console.log(`\n✅ Autocomplete Result: City code: ${city}`);
159
+
160
+ // --- 14. Sort Prompt (New) ---
161
+ const priorities = await MepCLI.sort({
162
+ message: "Rank your top priorities (Space to grab/drop, Arrows to move):",
163
+ items: ["Performance", "Security", "Features", "Usability", "Cost"]
164
+ });
165
+ console.log(`\n✅ Sort Result: Priorities: [${priorities.join(', ')}]`);
166
+
167
+ // --- 15. Table Prompt (New) ---
168
+ const userId = await MepCLI.table({
169
+ message: "Select a user from the database:",
170
+ columns: ["ID", "Name", "Role", "Status"],
171
+ data: [
172
+ { value: 1, row: ["001", "Alice", "Admin", "Active"] },
173
+ { value: 2, row: ["002", "Bob", "Dev", "Offline"] },
174
+ { value: 3, row: ["003", "Charlie", "User", "Active"] },
175
+ { value: 4, row: ["004", "David", "Manager", "Active"] },
176
+ ]
177
+ });
178
+ console.log(`\n✅ Table Result: Selected User ID: ${userId}`);
179
+
180
+ // --- 16. Confirm Prompt (Simple Yes/No) ---
141
181
  const proceed = await MepCLI.confirm({
142
182
  message: "Ready to deploy the project now?",
143
183
  initial: true
144
184
  });
145
185
  console.log(`\n✅ Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
146
186
 
147
- // --- 14. Spin Utility (Loading/Async Task Indicator) ---
148
- await MepCLI.spin(
149
- "Finalizing configuration and deploying...",
150
- new Promise(resolve => setTimeout(resolve, 1500)) // Simulates a 1.5 second async task
151
- );
187
+ // --- 17. Spin Utility (Loading/Async Task Indicator) ---
188
+ const s = MepCLI.spinner("Finalizing configuration and deploying...").start();
189
+ await new Promise(resolve => setTimeout(resolve, 1500)); // Simulates a 1.5 second async task
190
+ s.success();
191
+
152
192
  console.log("\n--- Deployment successful! All MepCLI features demonstrated! ---");
153
193
 
154
194
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
5
  "repository": {
6
6
  "type": "git",