@kikkimo/claude-launcher 2.4.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,54 +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
+ * @param {Function|null} hintCallback - Optional sync callback(selectedIndex) returning hint string or null
63
64
  */
64
- displayMenu(clearScreen = true, versionInfo = null) {
65
- // Clear screen and display header + menu together (like old version)
66
- if (clearScreen) {
67
- console.clear();
68
- }
69
- console.log('');
70
- console.log(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
71
- console.log(colors.orange + ' │' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
72
- console.log(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
73
- 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('');
74
72
 
75
- // Display version info if provided (between banner and navigation tips, like Claude Code)
76
73
  if (versionInfo) {
77
- console.log(versionInfo);
78
- console.log('');
74
+ lines.push(versionInfo);
75
+ lines.push('');
79
76
  }
80
77
 
81
- console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
82
- console.log('');
78
+ lines.push(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
79
+ lines.push('');
83
80
 
84
- // Display menu options
85
81
  this.menuOptions.forEach((option, index) => {
86
82
  if (index === this.selectedIndex) {
87
- // Pad the selected option to ensure complete background coverage
88
- const paddedOption = padStringToWidth(option, Math.max(40, option.length + 2));
89
- 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);
90
86
  } else {
91
- console.log(colors.gray + ' ' + option + colors.reset);
87
+ lines.push(colors.gray + ' ' + option + colors.reset);
92
88
  }
93
89
  });
94
90
 
95
- console.log('');
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('');
112
+ }
113
+
114
+ lines.push('');
115
+ screen.render(lines);
96
116
  }
97
117
 
98
118
  /**
@@ -105,20 +125,21 @@ class Menu {
105
125
 
106
126
  /**
107
127
  * Handle keyboard navigation
108
- * @param {boolean} clearScreen - Whether to clear screen on initial display (default: true)
109
128
  * @param {string} versionInfo - Optional version info to display
129
+ * @param {Function|null} hintCallback - Optional sync callback(selectedIndex) returning hint string or null
110
130
  */
111
- async navigate(clearScreen = true, versionInfo = null) {
131
+ async navigate(versionInfo = null, hintCallback = null) {
112
132
  // Guard against empty menu to prevent NaN from modulo operations
113
133
  if (!this.menuOptions || this.menuOptions.length === 0) {
114
- console.log(colors.yellow + ' Warning: No menu options available' + colors.reset);
134
+ screen.write(colors.yellow + ' Warning: No menu options available' + colors.reset + '\n');
115
135
  return -1; // Return cancel/exit code
116
136
  }
117
137
 
118
138
  this.versionInfo = versionInfo; // Store for redrawing
139
+ this.hintCallback = hintCallback; // Store for redrawing
119
140
 
120
141
  return new Promise((resolve, reject) => {
121
- this.displayMenu(clearScreen, versionInfo);
142
+ this.displayMenu(versionInfo, hintCallback);
122
143
 
123
144
  if (process.stdin.isTTY) {
124
145
  const scope = stdinManager.acquire('raw', {
@@ -171,15 +192,16 @@ class Menu {
171
192
  switch (key) {
172
193
  case '\u001b[A': // Up arrow
173
194
  this.selectedIndex = (this.selectedIndex - 1 + this.menuOptions.length) % this.menuOptions.length;
174
- this.displayMenu(true, this.versionInfo);
195
+ this.displayMenu(this.versionInfo, this.hintCallback);
175
196
  break;
176
197
 
177
198
  case '\u001b[B': // Down arrow
178
199
  this.selectedIndex = (this.selectedIndex + 1) % this.menuOptions.length;
179
- this.displayMenu(true, this.versionInfo);
200
+ this.displayMenu(this.versionInfo, this.hintCallback);
180
201
  break;
181
202
 
182
203
  case '\r': // Enter
204
+ case ' ': // Space
183
205
  safeResolve(this.selectedIndex);
184
206
  break;
185
207
 
@@ -207,7 +229,7 @@ class Menu {
207
229
 
208
230
  const rl = lineScope.createReadline();
209
231
 
210
- 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');
211
233
 
212
234
  rl.on('line', (input) => {
213
235
  const choice = parseInt(input.trim());
@@ -220,7 +242,7 @@ class Menu {
220
242
  lineScope.release();
221
243
  resolve(-1);
222
244
  } else {
223
- 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');
224
246
  }
225
247
  });
226
248
  }
@@ -230,7 +252,7 @@ class Menu {
230
252
  async selectFromList(title, items, activeIndex = -1) {
231
253
  // Guard against empty items list to prevent NaN from modulo operations
232
254
  if (!items || items.length === 0) {
233
- 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');
234
256
  return null; // Return null to indicate no selection
235
257
  }
236
258
 
@@ -239,12 +261,12 @@ class Menu {
239
261
  let selectedIndex = activeIndex >= 0 ? activeIndex : 0;
240
262
 
241
263
  const displayList = () => {
242
- console.clear();
243
- console.log('');
244
- console.log(colors.bright + colors.orange + `[*] ${title}` + colors.reset);
245
- console.log('');
246
- console.log(colors.gray + ' Use ↑↓ arrow keys to navigate, Enter to select, Esc to cancel' + colors.reset);
247
- 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('');
248
270
 
249
271
  items.forEach((item, index) => {
250
272
  const isActive = index === activeIndex;
@@ -253,19 +275,20 @@ class Menu {
253
275
 
254
276
  if (index === selectedIndex) {
255
277
  const itemText = `${item.name}${activeIndicator}`;
256
- const paddedItem = padStringToWidth(itemText, Math.max(40, itemText.length + 2));
257
- 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);
258
280
  if (item.details) {
259
281
  item.details.forEach(detail => {
260
- console.log(colors.gray + ' ' + detail + colors.reset);
282
+ lines.push(colors.gray + ' ' + detail + colors.reset);
261
283
  });
262
284
  }
263
285
  } else {
264
- console.log(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
286
+ lines.push(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
265
287
  }
266
288
  });
267
289
 
268
- console.log('');
290
+ lines.push('');
291
+ screen.render(lines);
269
292
  };
270
293
 
271
294
  return new Promise((resolve, reject) => {
@@ -358,7 +381,7 @@ class Menu {
358
381
 
359
382
  const rl = lineScope.createReadline();
360
383
 
361
- 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');
362
385
 
363
386
  rl.on('line', (input) => {
364
387
  const choice = parseInt(input.trim());
@@ -371,7 +394,7 @@ class Menu {
371
394
  lineScope.release();
372
395
  resolve(null);
373
396
  } else {
374
- 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');
375
398
  }
376
399
  });
377
400
  }