@kikkimo/claude-launcher 2.5.0 → 3.0.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/lib/ui/menu.js CHANGED
@@ -5,8 +5,9 @@
5
5
  const readline = require('readline');
6
6
  const colors = require('./colors');
7
7
  const i18n = require('../i18n');
8
- const { padStringToWidth } = require('../utils/string-width');
8
+ const { padStringToWidth, getStringWidth } = require('../utils/string-width');
9
9
  const stdinManager = require('../utils/stdin-manager');
10
+ const screen = require('./screen');
10
11
 
11
12
  /**
12
13
  * Force cleanup stdin state before displaying any menu
@@ -27,7 +28,7 @@ function forceCleanupBeforeMenu() {
27
28
  } catch (error) {
28
29
  // Ignore cleanup errors but log for debugging
29
30
  if (process.env.DEBUG_STDIN) {
30
- console.error('[DEBUG] forceCleanupBeforeMenu error:', error.message);
31
+ screen.debug('[DEBUG] forceCleanupBeforeMenu error: ' + error.message);
31
32
  }
32
33
  }
33
34
  }
@@ -45,64 +46,73 @@ class Menu {
45
46
  // Force cleanup stdin state before any menu display
46
47
  forceCleanupBeforeMenu();
47
48
 
48
- // Use console.clear and console.log for proper screen clearing
49
- console.clear();
50
- console.log('');
51
- console.log(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
52
- console.log(colors.orange + ' ' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
53
- console.log(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
54
- console.log('');
55
- console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
56
- console.log('');
49
+ const lines = [];
50
+ lines.push('');
51
+ lines.push(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
52
+ lines.push(colors.orange + ' ' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
53
+ lines.push(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
54
+ lines.push('');
55
+ lines.push(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
56
+ lines.push('');
57
+ screen.render(lines);
57
58
  }
58
59
 
59
60
  /**
60
61
  * Display menu with current selection
61
- * @param {boolean} clearScreen - Whether to clear screen before displaying (default: true)
62
62
  * @param {string} versionInfo - Optional version info to display between banner and navigation
63
63
  * @param {Function|null} hintCallback - Optional sync callback(selectedIndex) returning hint string or null
64
64
  */
65
- displayMenu(clearScreen = true, versionInfo = null, hintCallback = null) {
66
- // Clear screen and display header + menu together (like old version)
67
- if (clearScreen) {
68
- console.clear();
69
- }
70
- console.log('');
71
- console.log(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
72
- console.log(colors.orange + ' │' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
73
- console.log(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
74
- console.log('');
65
+ displayMenu(versionInfo = null, hintCallback = null) {
66
+ const lines = [];
67
+ lines.push('');
68
+ lines.push(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
69
+ lines.push(colors.orange + ' │' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
70
+ lines.push(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
71
+ lines.push('');
75
72
 
76
- // Display version info if provided (between banner and navigation tips, like Claude Code)
77
73
  if (versionInfo) {
78
- console.log(versionInfo);
79
- console.log('');
74
+ lines.push(versionInfo);
75
+ lines.push('');
80
76
  }
81
77
 
82
- console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
83
- console.log('');
78
+ lines.push(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
79
+ lines.push('');
84
80
 
85
- // Display menu options
86
81
  this.menuOptions.forEach((option, index) => {
87
82
  if (index === this.selectedIndex) {
88
- // Pad the selected option to ensure complete background coverage
89
- const paddedOption = padStringToWidth(option, Math.max(40, option.length + 2));
90
- console.log(colors.orange + ' → ' + colors.black + colors.bgAmber + paddedOption + colors.reset);
83
+ const displayWidth = getStringWidth(option);
84
+ const paddedOption = padStringToWidth(option, Math.max(40, displayWidth + 2));
85
+ lines.push(colors.orange + ' → ' + colors.black + colors.bgAmber + paddedOption + colors.reset);
91
86
  } else {
92
- console.log(colors.gray + ' ' + option + colors.reset);
87
+ lines.push(colors.gray + ' ' + option + colors.reset);
93
88
  }
94
89
  });
95
90
 
96
- // Render dynamic hint if callback provided
97
- if (hintCallback) {
98
- const hintText = hintCallback(this.selectedIndex);
99
- if (hintText) {
100
- console.log('');
101
- console.log(colors.green + ' \u2139 ' + hintText + colors.reset);
102
- }
91
+ // Fixed 4-line hint area for stable menu height
92
+ const hintText = hintCallback ? hintCallback(this.selectedIndex) : null;
93
+ if (hintText) {
94
+ const hintLines = hintText.split('\n').slice(0, 4);
95
+ while (hintLines.length < 4) hintLines.push('');
96
+ lines.push('');
97
+ hintLines.forEach((line, i) => {
98
+ if (line === '') {
99
+ lines.push('');
100
+ } else if (i === 0) {
101
+ lines.push(colors.green + ' \u2139 ' + line + colors.reset);
102
+ } else {
103
+ lines.push(colors.gray + ' ' + line + colors.reset);
104
+ }
105
+ });
106
+ } else {
107
+ lines.push('');
108
+ lines.push('');
109
+ lines.push('');
110
+ lines.push('');
111
+ lines.push('');
103
112
  }
104
113
 
105
- console.log('');
114
+ lines.push('');
115
+ screen.render(lines);
106
116
  }
107
117
 
108
118
  /**
@@ -115,14 +125,13 @@ class Menu {
115
125
 
116
126
  /**
117
127
  * Handle keyboard navigation
118
- * @param {boolean} clearScreen - Whether to clear screen on initial display (default: true)
119
128
  * @param {string} versionInfo - Optional version info to display
120
129
  * @param {Function|null} hintCallback - Optional sync callback(selectedIndex) returning hint string or null
121
130
  */
122
- async navigate(clearScreen = true, versionInfo = null, hintCallback = null) {
131
+ async navigate(versionInfo = null, hintCallback = null) {
123
132
  // Guard against empty menu to prevent NaN from modulo operations
124
133
  if (!this.menuOptions || this.menuOptions.length === 0) {
125
- console.log(colors.yellow + ' Warning: No menu options available' + colors.reset);
134
+ screen.write(colors.yellow + ' Warning: No menu options available' + colors.reset + '\n');
126
135
  return -1; // Return cancel/exit code
127
136
  }
128
137
 
@@ -130,7 +139,7 @@ class Menu {
130
139
  this.hintCallback = hintCallback; // Store for redrawing
131
140
 
132
141
  return new Promise((resolve, reject) => {
133
- this.displayMenu(clearScreen, versionInfo, hintCallback);
142
+ this.displayMenu(versionInfo, hintCallback);
134
143
 
135
144
  if (process.stdin.isTTY) {
136
145
  const scope = stdinManager.acquire('raw', {
@@ -183,15 +192,16 @@ class Menu {
183
192
  switch (key) {
184
193
  case '\u001b[A': // Up arrow
185
194
  this.selectedIndex = (this.selectedIndex - 1 + this.menuOptions.length) % this.menuOptions.length;
186
- this.displayMenu(true, this.versionInfo, this.hintCallback);
195
+ this.displayMenu(this.versionInfo, this.hintCallback);
187
196
  break;
188
197
 
189
198
  case '\u001b[B': // Down arrow
190
199
  this.selectedIndex = (this.selectedIndex + 1) % this.menuOptions.length;
191
- this.displayMenu(true, this.versionInfo, this.hintCallback);
200
+ this.displayMenu(this.versionInfo, this.hintCallback);
192
201
  break;
193
202
 
194
203
  case '\r': // Enter
204
+ case ' ': // Space
195
205
  safeResolve(this.selectedIndex);
196
206
  break;
197
207
 
@@ -219,7 +229,7 @@ class Menu {
219
229
 
220
230
  const rl = lineScope.createReadline();
221
231
 
222
- console.log(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', this.menuOptions.length) + colors.reset);
232
+ screen.write(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', this.menuOptions.length) + colors.reset + '\n');
223
233
 
224
234
  rl.on('line', (input) => {
225
235
  const choice = parseInt(input.trim());
@@ -232,7 +242,7 @@ class Menu {
232
242
  lineScope.release();
233
243
  resolve(-1);
234
244
  } else {
235
- console.log(colors.red + ' Invalid selection. Please enter 1-' + this.menuOptions.length + '.' + colors.reset);
245
+ screen.write(colors.red + ' Invalid selection. Please enter 1-' + this.menuOptions.length + '.' + colors.reset + '\n');
236
246
  }
237
247
  });
238
248
  }
@@ -242,7 +252,7 @@ class Menu {
242
252
  async selectFromList(title, items, activeIndex = -1) {
243
253
  // Guard against empty items list to prevent NaN from modulo operations
244
254
  if (!items || items.length === 0) {
245
- console.log(colors.yellow + ' Warning: No items available to select' + colors.reset);
255
+ screen.write(colors.yellow + ' Warning: No items available to select' + colors.reset + '\n');
246
256
  return null; // Return null to indicate no selection
247
257
  }
248
258
 
@@ -251,12 +261,12 @@ class Menu {
251
261
  let selectedIndex = activeIndex >= 0 ? activeIndex : 0;
252
262
 
253
263
  const displayList = () => {
254
- console.clear();
255
- console.log('');
256
- console.log(colors.bright + colors.orange + `[*] ${title}` + colors.reset);
257
- console.log('');
258
- console.log(colors.gray + ' Use ↑↓ arrow keys to navigate, Enter to select, Esc to cancel' + colors.reset);
259
- console.log('');
264
+ const lines = [];
265
+ lines.push('');
266
+ lines.push(colors.bright + colors.orange + `[*] ${title}` + colors.reset);
267
+ lines.push('');
268
+ lines.push(colors.gray + ' Use ↑↓ arrow keys to navigate, Enter to select, Esc to cancel' + colors.reset);
269
+ lines.push('');
260
270
 
261
271
  items.forEach((item, index) => {
262
272
  const isActive = index === activeIndex;
@@ -265,19 +275,20 @@ class Menu {
265
275
 
266
276
  if (index === selectedIndex) {
267
277
  const itemText = `${item.name}${activeIndicator}`;
268
- const paddedItem = padStringToWidth(itemText, Math.max(40, itemText.length + 2));
269
- console.log(colors.orange + prefix + colors.black + colors.bgAmber + paddedItem + colors.reset);
278
+ const paddedItem = padStringToWidth(itemText, Math.max(40, getStringWidth(itemText) + 2));
279
+ lines.push(colors.orange + prefix + colors.black + colors.bgAmber + paddedItem + colors.reset);
270
280
  if (item.details) {
271
281
  item.details.forEach(detail => {
272
- console.log(colors.gray + ' ' + detail + colors.reset);
282
+ lines.push(colors.gray + ' ' + detail + colors.reset);
273
283
  });
274
284
  }
275
285
  } else {
276
- console.log(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
286
+ lines.push(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
277
287
  }
278
288
  });
279
289
 
280
- console.log('');
290
+ lines.push('');
291
+ screen.render(lines);
281
292
  };
282
293
 
283
294
  return new Promise((resolve, reject) => {
@@ -370,7 +381,7 @@ class Menu {
370
381
 
371
382
  const rl = lineScope.createReadline();
372
383
 
373
- console.log(colors.yellow + ' Arrow keys not available. Enter item number (1-' + items.length + ') or q to cancel:' + colors.reset);
384
+ screen.write(colors.yellow + ' Arrow keys not available. Enter item number (1-' + items.length + ') or q to cancel:' + colors.reset + '\n');
374
385
 
375
386
  rl.on('line', (input) => {
376
387
  const choice = parseInt(input.trim());
@@ -383,7 +394,7 @@ class Menu {
383
394
  lineScope.release();
384
395
  resolve(null);
385
396
  } else {
386
- console.log(colors.red + ' Invalid selection. Please enter 1-' + items.length + '.' + colors.reset);
397
+ screen.write(colors.red + ' Invalid selection. Please enter 1-' + items.length + '.' + colors.reset + '\n');
387
398
  }
388
399
  });
389
400
  }