mepcli 0.2.5 → 0.2.7

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.
@@ -61,18 +61,27 @@ class FilePrompt extends base_1.Prompt {
61
61
  .filter(f => f.startsWith(partial))
62
62
  .filter(f => {
63
63
  const fullPath = path.join(dir, f);
64
- const isDir = fs.statSync(fullPath).isDirectory();
65
- if (this.options.onlyDirectories && !isDir)
64
+ // Handle errors if file doesn't exist or permission denied
65
+ try {
66
+ const isDir = fs.statSync(fullPath).isDirectory();
67
+ if (this.options.onlyDirectories && !isDir)
68
+ return false;
69
+ if (this.options.extensions && !isDir) {
70
+ return this.options.extensions.some(ext => f.endsWith(ext));
71
+ }
72
+ return true;
73
+ }
74
+ catch (e) {
66
75
  return false;
67
- if (this.options.extensions && !isDir) {
68
- return this.options.extensions.some(ext => f.endsWith(ext));
69
76
  }
70
- return true;
71
77
  })
72
78
  .map(f => {
73
79
  const fullPath = path.join(dir, f);
74
- if (fs.statSync(fullPath).isDirectory())
75
- return f + '/';
80
+ try {
81
+ if (fs.statSync(fullPath).isDirectory())
82
+ return f + '/';
83
+ }
84
+ catch (e) { /* ignore */ }
76
85
  return f;
77
86
  });
78
87
  }
@@ -86,37 +95,51 @@ class FilePrompt extends base_1.Prompt {
86
95
  this.selectedSuggestion = -1;
87
96
  }
88
97
  render(firstRender) {
89
- this.print(ansi_1.ANSI.SHOW_CURSOR);
90
- if (!firstRender) {
91
- // Clear input line + suggestions
92
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT); // Input line
93
- // We need to track how many lines suggestions took
94
- // For now assume simple clear, or use ANSI.ERASE_DOWN if at bottom?
95
- // Safer to move up and clear
96
- this.print(ansi_1.ANSI.ERASE_DOWN);
97
- }
98
+ // Construct string
98
99
  const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
99
- this.print(`${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${this.input}`);
100
+ let output = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${this.input}`;
101
+ // Suggestions
100
102
  if (this.suggestions.length > 0) {
101
- this.print('\n');
103
+ output += '\n'; // Separate input from suggestions
102
104
  const maxShow = 5;
103
- this.suggestions.slice(0, maxShow).forEach((s, i) => {
105
+ const displayed = this.suggestions.slice(0, maxShow);
106
+ displayed.forEach((s, i) => {
107
+ if (i > 0)
108
+ output += '\n';
104
109
  if (i === this.selectedSuggestion) {
105
- this.print(`${theme_1.theme.main}❯ ${s}${ansi_1.ANSI.RESET}\n`);
110
+ output += `${theme_1.theme.main}❯ ${s}${ansi_1.ANSI.RESET}`;
106
111
  }
107
112
  else {
108
- this.print(` ${s}\n`);
113
+ output += ` ${s}`;
109
114
  }
110
115
  });
111
116
  if (this.suggestions.length > maxShow) {
112
- this.print(` ...and ${this.suggestions.length - maxShow} more\n`);
117
+ output += `\n ...and ${this.suggestions.length - maxShow} more`;
113
118
  }
114
- // Move cursor back to input line
115
- const lines = Math.min(this.suggestions.length, maxShow) + (this.suggestions.length > maxShow ? 2 : 1);
116
- this.print(`\x1b[${lines}A`);
117
- const inputLen = this.options.message.length + 3 + this.input.length;
118
- this.print(`\x1b[${inputLen}C`);
119
119
  }
120
+ this.renderFrame(output);
121
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
122
+ // Restore Cursor Logic
123
+ // We need to move up to the input line if we printed suggestions.
124
+ // The input line is always the first line (index 0).
125
+ // So we move up by (totalLines - 1).
126
+ const totalLines = this.lastRenderHeight; // renderFrame sets this
127
+ if (totalLines > 1) {
128
+ this.print(`\x1b[${totalLines - 1}A`);
129
+ }
130
+ // Move right
131
+ const prefix = `${icon} ${theme_1.theme.title}${this.options.message} `;
132
+ const prefixLen = this.stripAnsi(prefix).length;
133
+ // Cursor is usually at the end of input unless we add backspace support etc.
134
+ // The cursor property tracks it, but my handleInput simplified it.
135
+ // Let's rely on this.input.length for now since handleInput appends.
136
+ // Ah, handleInput logic below supports cursor pos theoretically but I only see appending?
137
+ // Actually handleInput doesn't support left/right in the original code, it supports down/up for suggestions.
138
+ // So cursor is always at end.
139
+ const targetCol = prefixLen + this.input.length;
140
+ this.print(ansi_1.ANSI.CURSOR_LEFT);
141
+ if (targetCol > 0)
142
+ this.print(`\x1b[${targetCol}C`);
120
143
  }
121
144
  handleInput(char) {
122
145
  if (char === '\t') { // Tab
@@ -13,32 +13,37 @@ class ListPrompt extends base_1.Prompt {
13
13
  this.value = options.initial || [];
14
14
  }
15
15
  render(firstRender) {
16
- this.print(ansi_1.ANSI.SHOW_CURSOR);
17
- if (!firstRender) {
18
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
19
- if (this.errorMsg) {
20
- this.print(ansi_1.ANSI.UP + ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
21
- }
22
- }
16
+ // Prepare content
23
17
  const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
24
- this.print(`${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `);
18
+ let mainLine = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `;
25
19
  // Render Tags
26
20
  if (this.value.length > 0) {
27
21
  this.value.forEach((tag) => {
28
- this.print(`${theme_1.theme.main}[${tag}]${ansi_1.ANSI.RESET} `);
22
+ mainLine += `${theme_1.theme.main}[${tag}]${ansi_1.ANSI.RESET} `;
29
23
  });
30
24
  }
31
25
  // Render Current Input
32
- this.print(`${this.currentInput}`);
26
+ mainLine += `${this.currentInput}`;
27
+ let output = mainLine;
33
28
  if (this.errorMsg) {
34
- this.print(`\n${ansi_1.ANSI.ERASE_LINE}${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
29
+ output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
30
+ }
31
+ // Use Double Buffering
32
+ this.renderFrame(output);
33
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
34
+ // If we printed an error, the cursor is at the end of the error line.
35
+ // We need to move it back to the end of the input line.
36
+ if (this.errorMsg) {
37
+ // Move up one line (since error is always on the next line in this simple implementation)
35
38
  this.print(ansi_1.ANSI.UP);
36
- // Return cursor
37
- const promptLen = this.options.message.length + 3;
38
- let tagsLen = 0;
39
- this.value.forEach((tag) => tagsLen += tag.length + 3); // [tag] + space
40
- const inputLen = this.currentInput.length;
41
- this.print(`\x1b[1000D\x1b[${promptLen + tagsLen + inputLen}C`);
39
+ // Move to the correct column.
40
+ // We need to calculate visual length of mainLine to place cursor correctly.
41
+ // stripAnsi is available in base class now.
42
+ const visualLength = this.stripAnsi(mainLine).length;
43
+ this.print(ansi_1.ANSI.CURSOR_LEFT); // Go to start
44
+ if (visualLength > 0) {
45
+ this.print(`\x1b[${visualLength}C`);
46
+ }
42
47
  }
43
48
  }
44
49
  handleInput(char) {
@@ -1,13 +1,13 @@
1
1
  import { Prompt } from '../base';
2
2
  import { MultiSelectOptions } from '../types';
3
- export declare class MultiSelectPrompt extends Prompt<any[], MultiSelectOptions> {
3
+ export declare class MultiSelectPrompt<V> extends Prompt<any[], MultiSelectOptions<V>> {
4
4
  private selectedIndex;
5
5
  private checkedState;
6
6
  private searchBuffer;
7
7
  private scrollTop;
8
8
  private readonly pageSize;
9
9
  private errorMsg;
10
- constructor(options: MultiSelectOptions);
10
+ constructor(options: MultiSelectOptions<V>);
11
11
  private getFilteredChoices;
12
12
  protected render(firstRender: boolean): void;
13
13
  protected handleInput(char: string): void;
@@ -24,31 +24,6 @@ class MultiSelectPrompt extends base_1.Prompt {
24
24
  .filter(c => c.title.toLowerCase().includes(this.searchBuffer.toLowerCase()));
25
25
  }
26
26
  render(firstRender) {
27
- this.print(ansi_1.ANSI.HIDE_CURSOR);
28
- // This is tricky because height changes with filter.
29
- // Simplified clearing:
30
- if (!firstRender) {
31
- this.print(ansi_1.ANSI.ERASE_DOWN); // Clear everything below cursor
32
- // But we need to move cursor up to start of prompt
33
- // We can store last height?
34
- }
35
- // Wait, standard render loop usually assumes fixed position or we manually handle it.
36
- // Let's use ERASE_LINE + UP loop like SelectPrompt but simpler since we have full screen control in a way
37
- // Actually, let's just clear screen? No, that's bad.
38
- // Let's stick to SelectPrompt's strategy.
39
- if (!firstRender) {
40
- // Hack: Just clear last 10 lines to be safe? No.
41
- // We will implement proper tracking later if needed, for now standard clear
42
- // Let's re-use SelectPrompt logic structure if possible, but distinct implementation here.
43
- // Simplest: Always move to top of prompt and erase down.
44
- // Assuming we track how many lines we printed.
45
- }
46
- // ... Implementation detail: use a simpler clear strategy:
47
- // Move to start of prompt line (we need to track lines printed in previous frame)
48
- if (this.lastRenderLines) {
49
- this.print(`\x1b[${this.lastRenderLines}A`);
50
- this.print(ansi_1.ANSI.ERASE_DOWN);
51
- }
52
27
  let output = '';
53
28
  const choices = this.getFilteredChoices();
54
29
  // Adjust Scroll
@@ -65,27 +40,26 @@ class MultiSelectPrompt extends base_1.Prompt {
65
40
  const searchStr = this.searchBuffer ? ` ${theme_1.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
66
41
  output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${searchStr}\n`;
67
42
  if (choices.length === 0) {
68
- output += ` ${theme_1.theme.muted}No results found${ansi_1.ANSI.RESET}\n`;
43
+ output += ` ${theme_1.theme.muted}No results found${ansi_1.ANSI.RESET}`; // No newline at end
69
44
  }
70
45
  else {
71
46
  const visible = choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
72
47
  visible.forEach((choice, index) => {
48
+ if (index > 0)
49
+ output += '\n';
73
50
  const actualIndex = this.scrollTop + index;
74
51
  const cursor = actualIndex === this.selectedIndex ? `${theme_1.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
75
52
  const isChecked = this.checkedState[choice.originalIndex];
76
53
  const checkbox = isChecked
77
54
  ? `${theme_1.theme.success}◉${ansi_1.ANSI.RESET}`
78
55
  : `${theme_1.theme.muted}◯${ansi_1.ANSI.RESET}`;
79
- output += `${cursor} ${checkbox} ${choice.title}\n`;
56
+ output += `${cursor} ${checkbox} ${choice.title}`;
80
57
  });
81
58
  }
82
59
  if (this.errorMsg) {
83
- output += `${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}\n`;
60
+ output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
84
61
  }
85
- this.print(output);
86
- // Count lines
87
- const lines = 1 + (choices.length === 0 ? 1 : Math.min(choices.length, this.pageSize)) + (this.errorMsg ? 1 : 0);
88
- this.lastRenderLines = lines;
62
+ this.renderFrame(output);
89
63
  }
90
64
  handleInput(char) {
91
65
  const choices = this.getFilteredChoices();
@@ -1,8 +1,8 @@
1
1
  import { Prompt } from '../base';
2
2
  import { NumberOptions } from '../types';
3
3
  export declare class NumberPrompt extends Prompt<number, NumberOptions> {
4
- private cursor;
5
4
  private stringValue;
5
+ private cursor;
6
6
  private errorMsg;
7
7
  constructor(options: NumberOptions);
8
8
  protected render(firstRender: boolean): void;
@@ -8,45 +8,47 @@ const theme_1 = require("../theme");
8
8
  class NumberPrompt extends base_1.Prompt {
9
9
  constructor(options) {
10
10
  super(options);
11
+ this.stringValue = '';
11
12
  this.cursor = 0;
12
13
  this.errorMsg = '';
13
- this.value = options.initial ?? 0;
14
- this.stringValue = this.value.toString();
14
+ // We work with string for editing, but value property stores the parsed number ultimately
15
+ // Initialize stringValue from initial
16
+ this.stringValue = options.initial !== undefined ? options.initial.toString() : '';
15
17
  this.cursor = this.stringValue.length;
16
18
  }
17
19
  render(firstRender) {
18
- this.print(ansi_1.ANSI.SHOW_CURSOR);
19
- if (!firstRender) {
20
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
21
- if (this.errorMsg) {
22
- this.print(ansi_1.ANSI.UP + ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
23
- }
24
- }
25
- // 1. Render the Prompt Message
26
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
20
+ // Prepare content
27
21
  const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
28
- this.print(`${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `);
29
- // 2. Render the Value
30
- this.print(`${theme_1.theme.main}${this.stringValue}${ansi_1.ANSI.RESET}`);
31
- // 3. Handle Error Message
22
+ // Prefix
23
+ let output = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `;
24
+ // Value
25
+ output += `${theme_1.theme.main}${this.stringValue}${ansi_1.ANSI.RESET}`;
26
+ if (this.errorMsg) {
27
+ output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
28
+ }
29
+ this.renderFrame(output);
30
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
31
+ // Restore cursor position
32
+ // If errorMsg, we are on the line below the input.
32
33
  if (this.errorMsg) {
33
- this.print(`\n${ansi_1.ANSI.ERASE_LINE}${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
34
34
  this.print(ansi_1.ANSI.UP);
35
- const promptLen = this.options.message.length + 3;
36
- const valLen = this.stringValue.length;
37
- this.print(`\x1b[1000D\x1b[${promptLen + valLen}C`);
38
35
  }
39
- // 4. Position Cursor
40
- const diff = this.stringValue.length - this.cursor;
41
- if (diff > 0) {
42
- this.print(`\x1b[${diff}D`);
36
+ // Calculate visual offset
37
+ const prefix = `${icon} ${theme_1.theme.title}${this.options.message} `;
38
+ const prefixLen = this.stripAnsi(prefix).length;
39
+ const targetCol = prefixLen + this.cursor;
40
+ this.print(ansi_1.ANSI.CURSOR_LEFT);
41
+ if (targetCol > 0) {
42
+ this.print(`\x1b[${targetCol}C`);
43
43
  }
44
44
  }
45
45
  handleInput(char) {
46
46
  // Enter
47
47
  if (char === '\r' || char === '\n') {
48
48
  const num = parseFloat(this.stringValue);
49
- if (isNaN(num)) {
49
+ if (this.stringValue.trim() === '' || isNaN(num)) {
50
+ // Check if empty is allowed?
51
+ // If not required? Assuming required for number prompt usually
50
52
  this.errorMsg = 'Please enter a valid number.';
51
53
  this.render(false);
52
54
  return;
@@ -61,9 +63,6 @@ class NumberPrompt extends base_1.Prompt {
61
63
  this.render(false);
62
64
  return;
63
65
  }
64
- if (this.errorMsg) {
65
- this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
66
- }
67
66
  this.submit(num);
68
67
  return;
69
68
  }
@@ -73,6 +72,8 @@ class NumberPrompt extends base_1.Prompt {
73
72
  num += (this.options.step ?? 1);
74
73
  if (this.options.max !== undefined && num > this.options.max)
75
74
  num = this.options.max;
75
+ // Round to avoid float errors
76
+ num = Math.round(num * 10000) / 10000;
76
77
  this.stringValue = num.toString();
77
78
  this.cursor = this.stringValue.length;
78
79
  this.errorMsg = '';
@@ -85,6 +86,8 @@ class NumberPrompt extends base_1.Prompt {
85
86
  num -= (this.options.step ?? 1);
86
87
  if (this.options.min !== undefined && num < this.options.min)
87
88
  num = this.options.min;
89
+ // Round to avoid float errors
90
+ num = Math.round(num * 10000) / 10000;
88
91
  this.stringValue = num.toString();
89
92
  this.cursor = this.stringValue.length;
90
93
  this.errorMsg = '';
@@ -118,14 +121,7 @@ class NumberPrompt extends base_1.Prompt {
118
121
  return;
119
122
  }
120
123
  // Numeric Input (and . and -)
121
- // Simple paste support for numbers is also good
122
124
  if (/^[0-9.\-]+$/.test(char)) {
123
- // Basic validation for pasted content
124
- if (char.includes('-') && (this.cursor !== 0 || this.stringValue.includes('-') || char.lastIndexOf('-') > 0)) {
125
- // If complex paste fails simple checks, ignore or let user correct
126
- // For now, strict check on single char logic is preserved if we want,
127
- // but let's allow pasting valid number strings
128
- }
129
125
  // Allow if it looks like a number part
130
126
  this.stringValue = this.stringValue.slice(0, this.cursor) + char + this.stringValue.slice(this.cursor);
131
127
  this.cursor += char.length;
@@ -1,16 +1,14 @@
1
1
  import { Prompt } from '../base';
2
2
  import { SelectOptions } from '../types';
3
- export declare class SelectPrompt extends Prompt<any, SelectOptions> {
3
+ export declare class SelectPrompt<V> extends Prompt<any, SelectOptions<V>> {
4
4
  private selectedIndex;
5
5
  private searchBuffer;
6
6
  private scrollTop;
7
7
  private readonly pageSize;
8
- constructor(options: SelectOptions);
8
+ constructor(options: SelectOptions<V>);
9
9
  private isSeparator;
10
10
  private findNextSelectableIndex;
11
11
  private getFilteredChoices;
12
- private lastRenderHeight;
13
- protected renderWrapper(firstRender: boolean): void;
14
12
  protected render(firstRender: boolean): void;
15
13
  protected handleInput(char: string): void;
16
14
  }
@@ -12,8 +12,6 @@ class SelectPrompt extends base_1.Prompt {
12
12
  this.searchBuffer = '';
13
13
  this.scrollTop = 0;
14
14
  this.pageSize = 7;
15
- // Custom render to handle variable height clearing
16
- this.lastRenderHeight = 0;
17
15
  // Find first non-separator index
18
16
  this.selectedIndex = this.findNextSelectableIndex(-1, 1);
19
17
  }
@@ -51,10 +49,7 @@ class SelectPrompt extends base_1.Prompt {
51
49
  return c.title.toLowerCase().includes(this.searchBuffer.toLowerCase());
52
50
  });
53
51
  }
54
- renderWrapper(firstRender) {
55
- if (!firstRender && this.lastRenderHeight > 0) {
56
- this.print(`\x1b[${this.lastRenderHeight}A`);
57
- }
52
+ render(firstRender) {
58
53
  let output = '';
59
54
  const choices = this.getFilteredChoices();
60
55
  // Adjust Scroll Top
@@ -70,44 +65,35 @@ class SelectPrompt extends base_1.Prompt {
70
65
  }
71
66
  // Header
72
67
  const searchStr = this.searchBuffer ? ` ${theme_1.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
73
- output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${searchStr}\n`;
68
+ // Note: We avoid ERASE_LINE here because renderFrame handles full redraw
69
+ output += `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${searchStr}\n`;
74
70
  if (choices.length === 0) {
75
- output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT} ${theme_1.theme.muted}No results found${ansi_1.ANSI.RESET}\n`;
71
+ output += ` ${theme_1.theme.muted}No results found${ansi_1.ANSI.RESET}`;
72
+ // We can omit newline at the very end if we want, but usually it's better to be consistent.
73
+ // renderFrame adds newline via truncate logic? No, it joins.
74
+ // So if I want a line break, I must add it.
76
75
  }
77
76
  else {
78
77
  const visibleChoices = choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
79
78
  visibleChoices.forEach((choice, index) => {
80
79
  const actualIndex = this.scrollTop + index;
81
- output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`;
80
+ if (index > 0)
81
+ output += '\n'; // Separator between items
82
82
  if (this.isSeparator(choice)) {
83
- output += ` ${ansi_1.ANSI.DIM}${choice.text || '────────'}${ansi_1.ANSI.RESET}\n`;
83
+ output += ` ${ansi_1.ANSI.DIM}${choice.text || '────────'}${ansi_1.ANSI.RESET}`;
84
84
  }
85
85
  else {
86
86
  if (actualIndex === this.selectedIndex) {
87
- output += `${theme_1.theme.main}❯ ${choice.title}${ansi_1.ANSI.RESET}\n`;
87
+ output += `${theme_1.theme.main}❯ ${choice.title}${ansi_1.ANSI.RESET}`;
88
88
  }
89
89
  else {
90
- output += ` ${choice.title}\n`;
90
+ output += ` ${choice.title}`;
91
91
  }
92
92
  }
93
93
  });
94
94
  }
95
- this.print(output);
96
- // Clear remaining lines if list shrunk
97
- const visibleCount = Math.min(choices.length, this.pageSize);
98
- const currentHeight = visibleCount + 1 + (choices.length === 0 ? 1 : 0);
99
- const linesToClear = this.lastRenderHeight - currentHeight;
100
- if (linesToClear > 0) {
101
- for (let i = 0; i < linesToClear; i++) {
102
- this.print(`${ansi_1.ANSI.ERASE_LINE}\n`);
103
- }
104
- this.print(`\x1b[${linesToClear}A`); // Move back up
105
- }
106
- this.lastRenderHeight = currentHeight;
107
- }
108
- render(firstRender) {
109
- this.print(ansi_1.ANSI.HIDE_CURSOR);
110
- this.renderWrapper(firstRender);
95
+ // No manual printing. Pass to renderFrame.
96
+ this.renderFrame(output);
111
97
  }
112
98
  handleInput(char) {
113
99
  const choices = this.getFilteredChoices();
@@ -121,7 +107,7 @@ class SelectPrompt extends base_1.Prompt {
121
107
  if (this.isSeparator(choices[this.selectedIndex]))
122
108
  return;
123
109
  this.cleanup();
124
- this.print(ansi_1.ANSI.SHOW_CURSOR);
110
+ // Cursor is shown by cleanup
125
111
  if (this._resolve)
126
112
  this._resolve(choices[this.selectedIndex].value);
127
113
  return;
@@ -11,10 +11,6 @@ class SliderPrompt extends base_1.Prompt {
11
11
  this.value = options.initial ?? options.min;
12
12
  }
13
13
  render(firstRender) {
14
- this.print(ansi_1.ANSI.HIDE_CURSOR);
15
- if (!firstRender) {
16
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
17
- }
18
14
  const width = 20;
19
15
  const range = this.options.max - this.options.min;
20
16
  const ratio = (this.value - this.options.min) / range;
@@ -27,7 +23,8 @@ class SliderPrompt extends base_1.Prompt {
27
23
  bar += '─';
28
24
  }
29
25
  const unit = this.options.unit || '';
30
- this.print(`${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} [${bar}] ${this.value}${unit}`);
26
+ const output = `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} [${bar}] ${this.value}${unit}`;
27
+ this.renderFrame(output);
31
28
  }
32
29
  handleInput(char) {
33
30
  if (char === '\r' || char === '\n') {
@@ -37,10 +34,14 @@ class SliderPrompt extends base_1.Prompt {
37
34
  const step = this.options.step || 1;
38
35
  if (this.isLeft(char)) { // Left
39
36
  this.value = Math.max(this.options.min, this.value - step);
37
+ // Round to avoid float errors
38
+ this.value = Math.round(this.value * 10000) / 10000;
40
39
  this.render(false);
41
40
  }
42
41
  if (this.isRight(char)) { // Right
43
42
  this.value = Math.min(this.options.max, this.value + step);
43
+ // Round to avoid float errors
44
+ this.value = Math.round(this.value * 10000) / 10000;
44
45
  this.render(false);
45
46
  }
46
47
  }
@@ -4,9 +4,10 @@ export declare class TextPrompt extends Prompt<string, TextOptions> {
4
4
  private errorMsg;
5
5
  private cursor;
6
6
  private hasTyped;
7
- private renderLines;
7
+ private segments;
8
8
  constructor(options: TextOptions);
9
9
  protected render(firstRender: boolean): void;
10
+ private getSegmentWidth;
10
11
  protected handleInput(char: string): void;
11
12
  private validateAndSubmit;
12
13
  }