mepcli 0.6.0 → 0.6.1

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/dist/base.d.ts CHANGED
@@ -2,8 +2,7 @@ import { MouseEvent } from './types';
2
2
  import { detectCapabilities } from './utils';
3
3
  /**
4
4
  * Abstract base class for all prompts.
5
- * Handles common logic like stdin management, raw mode, and cleanup
6
- * to enforce DRY (Don't Repeat Yourself) principles.
5
+ * Implements a Robust Linear Scan Diffing Engine.
7
6
  */
8
7
  export declare abstract class Prompt<T, O> {
9
8
  protected options: O;
@@ -15,48 +14,20 @@ export declare abstract class Prompt<T, O> {
15
14
  private _inputParser;
16
15
  private _onKeyHandler?;
17
16
  private _onDataHandler?;
18
- protected lastRenderHeight: number;
19
17
  protected lastRenderLines: string[];
18
+ protected lastRenderHeight: number;
20
19
  protected capabilities: ReturnType<typeof detectCapabilities>;
21
20
  constructor(options: O);
22
- /**
23
- * Renders the UI. Must be implemented by subclasses.
24
- * @param firstRender Indicates if this is the initial render.
25
- */
26
21
  protected abstract render(firstRender: boolean): void;
27
- /**
28
- * Handles specific key inputs. Must be implemented by subclasses.
29
- * @param char The string representation of the key.
30
- * @param key The raw buffer.
31
- */
32
22
  protected abstract handleInput(char: string, key: Buffer): void;
33
- /**
34
- * Optional method to handle mouse events.
35
- * Subclasses can override this to implement mouse interaction.
36
- */
37
23
  protected handleMouse(_event: MouseEvent): void;
38
24
  protected print(text: string): void;
39
- /**
40
- * Starts the prompt interaction.
41
- * Sets up raw mode and listeners, returning a Promise.
42
- */
43
25
  run(): Promise<T>;
44
- /**
45
- * Cleans up listeners and restores stdin state.
46
- */
47
26
  protected cleanup(): void;
48
- /**
49
- * Submits the final value and resolves the promise.
50
- */
51
27
  protected submit(result: T): void;
52
28
  /**
53
- * Render Method with Diffing (Virtual DOM for CLI).
54
- * Calculates new lines, compares with old lines, and updates only changed parts.
55
- */
56
- /**
57
- * Renders the current frame by clearing the previous output and writing the new content.
58
- * This approach ("Move Up -> Erase Down -> Print") is more robust against artifacts
59
- * than line-by-line diffing, especially when the number of lines changes (e.g., filtering).
29
+ * Renders the frame using a linear scan diffing algorithm.
30
+ * Prevents flicker and handles height changes (expand/collapse) robustly.
60
31
  */
61
32
  protected renderFrame(content: string): void;
62
33
  protected stripAnsi(str: string): string;
package/dist/base.js CHANGED
@@ -6,32 +6,22 @@ const input_1 = require("./input");
6
6
  const utils_1 = require("./utils");
7
7
  /**
8
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.
9
+ * Implements a Robust Linear Scan Diffing Engine.
11
10
  */
12
11
  class Prompt {
13
12
  constructor(options) {
14
- // Smart Cursor State
15
- this.lastRenderHeight = 0;
16
13
  this.lastRenderLines = [];
14
+ this.lastRenderHeight = 0;
17
15
  this.options = options;
18
16
  this.stdin = process.stdin;
19
17
  this.stdout = process.stdout;
20
18
  this._inputParser = new input_1.InputParser();
21
19
  this.capabilities = (0, utils_1.detectCapabilities)();
22
20
  }
23
- /**
24
- * Optional method to handle mouse events.
25
- * Subclasses can override this to implement mouse interaction.
26
- */
27
21
  handleMouse(_event) { }
28
22
  print(text) {
29
23
  this.stdout.write(text);
30
24
  }
31
- /**
32
- * Starts the prompt interaction.
33
- * Sets up raw mode and listeners, returning a Promise.
34
- */
35
25
  run() {
36
26
  return new Promise((resolve, reject) => {
37
27
  this._resolve = resolve;
@@ -41,24 +31,15 @@ class Prompt {
41
31
  }
42
32
  this.stdin.resume();
43
33
  this.stdin.setEncoding('utf8');
44
- // Enable Mouse Tracking if supported and requested
45
- // Default to true if capabilities support it, unless explicitly disabled in options
46
34
  const shouldEnableMouse = this.options.mouse !== false && this.capabilities.hasMouse;
47
35
  if (shouldEnableMouse) {
48
36
  this.print(ansi_1.ANSI.SET_ANY_EVENT_MOUSE + ansi_1.ANSI.SET_SGR_EXT_MODE_MOUSE);
49
37
  }
50
- // Initial render: Default to hidden cursor (good for menus)
51
- // Subclasses like TextPrompt will explicitly show it if needed.
52
- if (this.capabilities.isCI) {
53
- // In CI, maybe don't hide cursor or do nothing?
54
- // But for now follow standard flow.
55
- }
56
38
  this.print(ansi_1.ANSI.HIDE_CURSOR);
39
+ // Initial render
57
40
  this.render(true);
58
- // Setup Input Parser Listeners
59
41
  this._onKeyHandler = (char, buffer) => {
60
- // Global Exit Handler (Ctrl+C)
61
- if (char === '\u0003') {
42
+ if (char === '\u0003') { // Ctrl+C
62
43
  this.cleanup();
63
44
  this.print(ansi_1.ANSI.SHOW_CURSOR + '\n');
64
45
  if (this._reject)
@@ -68,7 +49,6 @@ class Prompt {
68
49
  this.handleInput(char, buffer);
69
50
  };
70
51
  this._inputParser.on('keypress', this._onKeyHandler);
71
- // Listen to mouse events
72
52
  this._inputParser.on('mouse', (event) => {
73
53
  this.handleMouse(event);
74
54
  });
@@ -78,9 +58,6 @@ class Prompt {
78
58
  this.stdin.on('data', this._onDataHandler);
79
59
  });
80
60
  }
81
- /**
82
- * Cleans up listeners and restores stdin state.
83
- */
84
61
  cleanup() {
85
62
  if (this._onDataHandler) {
86
63
  this.stdin.removeListener('data', this._onDataHandler);
@@ -88,7 +65,6 @@ class Prompt {
88
65
  if (this._onKeyHandler) {
89
66
  this._inputParser.removeListener('keypress', this._onKeyHandler);
90
67
  }
91
- // Disable Mouse Tracking
92
68
  this.print(ansi_1.ANSI.DISABLE_MOUSE);
93
69
  if (typeof this.stdin.setRawMode === 'function') {
94
70
  this.stdin.setRawMode(false);
@@ -96,51 +72,74 @@ class Prompt {
96
72
  this.stdin.pause();
97
73
  this.print(ansi_1.ANSI.SHOW_CURSOR);
98
74
  }
99
- /**
100
- * Submits the final value and resolves the promise.
101
- */
102
75
  submit(result) {
103
76
  this.cleanup();
104
77
  this.print('\n');
105
78
  if (this._resolve)
106
79
  this._resolve(result);
107
80
  }
108
- // --- Rendering Utilities ---
109
- /**
110
- * Render Method with Diffing (Virtual DOM for CLI).
111
- * Calculates new lines, compares with old lines, and updates only changed parts.
112
- */
113
81
  /**
114
- * Renders the current frame by clearing the previous output and writing the new content.
115
- * This approach ("Move Up -> Erase Down -> Print") is more robust against artifacts
116
- * than line-by-line diffing, especially when the number of lines changes (e.g., filtering).
82
+ * Renders the frame using a linear scan diffing algorithm.
83
+ * Prevents flicker and handles height changes (expand/collapse) robustly.
117
84
  */
118
85
  renderFrame(content) {
119
86
  const width = this.stdout.columns || 80;
120
87
  const rawLines = content.split('\n');
121
- // Truncate each line to fit terminal width to avoid wrapping issues
88
+ // Truncate lines to prevent wrapping artifacts
122
89
  const newLines = rawLines.map(line => this.truncate(line, width));
123
- // 1. Move cursor to the start of the current line
124
- this.print(ansi_1.ANSI.CURSOR_LEFT);
125
- // 2. Move cursor up to the top of the previously rendered frame
126
- if (this.lastRenderHeight > 0) {
127
- // If the previous render had multiple lines, move up to the first line
128
- if (this.lastRenderHeight > 1) {
129
- this.print(`\x1b[${this.lastRenderHeight - 1}A`);
130
- }
90
+ // 1. First Render Case
91
+ if (this.lastRenderLines.length === 0) {
92
+ this.print(newLines.join('\n'));
93
+ this.lastRenderLines = newLines;
94
+ this.lastRenderHeight = newLines.length;
95
+ return;
96
+ }
97
+ let outputBuffer = '';
98
+ // 2. Return Cursor to the Top of the Prompt
99
+ if (this.lastRenderHeight > 1) {
100
+ outputBuffer += `\x1b[${this.lastRenderHeight - 1}A`;
131
101
  }
132
- // 3. Clear everything from the cursor down
133
- // This ensures all previous content (including "ghost" lines) is removed
134
- this.print(ansi_1.ANSI.ERASE_DOWN);
135
- // 4. Print the new frame content
102
+ outputBuffer += '\r'; // Ensure column 0
103
+ // 3. Linear Scan & Update
136
104
  for (let i = 0; i < newLines.length; i++) {
137
- this.print(newLines[i]);
138
- // Add newline character between lines, but not after the last line
139
- if (i < newLines.length - 1) {
140
- this.print('\n');
105
+ const newLine = newLines[i];
106
+ // Logic for moving to the next line
107
+ if (i > 0) {
108
+ if (i < this.lastRenderLines.length) {
109
+ // Moving within the previously existing area.
110
+ // Use 'Down' (B) to avoid scrolling/shifting existing content.
111
+ outputBuffer += '\x1b[B\r';
112
+ }
113
+ else {
114
+ // Moving into NEW area (Append).
115
+ // Must use '\n' to create the new line.
116
+ outputBuffer += '\n';
117
+ }
118
+ }
119
+ // Printing logic
120
+ if (i < this.lastRenderLines.length) {
121
+ const oldLine = this.lastRenderLines[i];
122
+ if (newLine !== oldLine) {
123
+ // Overwrite existing line
124
+ outputBuffer += ansi_1.ANSI.ERASE_LINE + newLine;
125
+ }
126
+ }
127
+ else {
128
+ // Print new line (we are already at start due to '\n' above)
129
+ outputBuffer += newLine;
141
130
  }
142
131
  }
143
- // 5. Update state for the next render cycle
132
+ // 4. Handle Shrinkage (Clear garbage below)
133
+ if (newLines.length < this.lastRenderLines.length) {
134
+ // Move down to the first obsolete line
135
+ outputBuffer += '\n';
136
+ // Clear everything below
137
+ outputBuffer += ansi_1.ANSI.ERASE_DOWN;
138
+ // Move back up to the last valid line to maintain cursor state consistency
139
+ outputBuffer += `\x1b[A`;
140
+ }
141
+ this.print(outputBuffer);
142
+ // Update State
144
143
  this.lastRenderLines = newLines;
145
144
  this.lastRenderHeight = newLines.length;
146
145
  }
@@ -152,51 +151,28 @@ class Prompt {
152
151
  if (visualWidth <= width) {
153
152
  return str;
154
153
  }
155
- // Heuristic truncation using stringWidth
156
- // We iterate and sum width until we hit limit - 3
157
154
  let currentWidth = 0;
158
155
  let cutIndex = 0;
159
156
  let inAnsi = false;
160
157
  for (let i = 0; i < str.length; i++) {
161
- const code = str.charCodeAt(i);
162
- if (str[i] === '\x1b') {
158
+ if (str[i] === '\x1b')
163
159
  inAnsi = true;
164
- }
165
- if (inAnsi) {
166
- if ((str[i] >= '@' && str[i] <= '~') || (str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) {
167
- inAnsi = false;
168
- }
169
- }
170
- else {
171
- // Re-implement basic width logic here for the cut index finding
172
- let charWidth = 1;
173
- let cp = code;
174
- if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
175
- const next = str.charCodeAt(i + 1);
176
- if (next >= 0xDC00 && next <= 0xDFFF) {
177
- cp = (code - 0xD800) * 0x400 + (next - 0xDC00) + 0x10000;
178
- // i is incremented in main loop but we need to skip next char
179
- // We'll handle i increment in the loop
180
- }
181
- }
182
- if (cp >= 0x1100) { // Quick check for potentially wide
183
- // It's acceptable to be slightly aggressive on wide chars for truncation
184
- charWidth = 2;
185
- }
160
+ if (!inAnsi) {
161
+ const code = str.charCodeAt(i);
162
+ const charWidth = code > 255 ? 2 : 1;
186
163
  if (currentWidth + charWidth > width - 3) {
187
- cutIndex = i;
188
164
  break;
189
165
  }
190
166
  currentWidth += charWidth;
191
- if (cp > 0xFFFF) {
192
- i++; // Skip low surrogate
193
- }
167
+ }
168
+ else {
169
+ if (str[i] === 'm' || (str[i] >= 'A' && str[i] <= 'Z'))
170
+ inAnsi = false;
194
171
  }
195
172
  cutIndex = i + 1;
196
173
  }
197
174
  return str.substring(0, cutIndex) + '...' + ansi_1.ANSI.RESET;
198
175
  }
199
- // Helper to check for arrow keys including application mode
200
176
  isUp(char) {
201
177
  return char === '\u001b[A' || char === '\u001bOA';
202
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Zero-dependency, interactive CLI prompt for Node.js",
5
5
  "repository": {
6
6
  "type": "git",