@kikkimo/claude-launcher 2.5.0 → 3.1.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,78 @@ 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
+ * @param {string} navigationKey - i18n key for navigation hint (default: 'navigation.use_arrows')
64
65
  */
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('');
66
+ displayMenu(versionInfo = null, hintCallback = null, navigationKey = 'navigation.use_arrows') {
67
+ this._navigationKey = navigationKey;
68
+ const isTTY = process.stdin.isTTY;
69
+ const lines = [];
70
+ lines.push('');
71
+ lines.push(colors.orange + ' ┌──────────────────────────────────────────────────────┐' + colors.reset);
72
+ lines.push(colors.orange + ' ' + colors.white + colors.bright + ' Claude Code Launcher ' + colors.orange + '│' + colors.reset);
73
+ lines.push(colors.orange + ' └──────────────────────────────────────────────────────┘' + colors.reset);
74
+ lines.push('');
75
75
 
76
- // Display version info if provided (between banner and navigation tips, like Claude Code)
77
76
  if (versionInfo) {
78
- console.log(versionInfo);
79
- console.log('');
77
+ lines.push(versionInfo);
78
+ lines.push('');
80
79
  }
81
80
 
82
- console.log(colors.gray + ' ' + i18n.tSync('navigation.use_arrows') + colors.reset);
83
- console.log('');
81
+ lines.push(colors.gray + ' ' + i18n.tSync(navigationKey) + colors.reset);
82
+ lines.push('');
84
83
 
85
- // Display menu options
86
84
  this.menuOptions.forEach((option, index) => {
85
+ const prefix = isTTY ? '' : (index + 1) + '. ';
87
86
  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);
87
+ const prefixedOption = prefix + option;
88
+ const displayWidth = getStringWidth(prefixedOption);
89
+ const paddedOption = padStringToWidth(prefixedOption, Math.max(40, displayWidth + 2));
90
+ lines.push(colors.orange + ' → ' + colors.black + colors.bgAmber + paddedOption + colors.reset);
91
91
  } else {
92
- console.log(colors.gray + ' ' + option + colors.reset);
92
+ lines.push(colors.gray + ' ' + prefix + option + colors.reset);
93
93
  }
94
94
  });
95
95
 
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
- }
96
+ // Fixed 4-line hint area for stable menu height
97
+ const hintText = hintCallback ? hintCallback(this.selectedIndex) : null;
98
+ if (hintText) {
99
+ const hintLines = hintText.split('\n').slice(0, 4);
100
+ while (hintLines.length < 4) hintLines.push('');
101
+ lines.push('');
102
+ hintLines.forEach((line, i) => {
103
+ if (line === '') {
104
+ lines.push('');
105
+ } else if (i === 0) {
106
+ lines.push(colors.green + ' \u2139 ' + line + colors.reset);
107
+ } else {
108
+ lines.push(colors.gray + ' ' + line + colors.reset);
109
+ }
110
+ });
111
+ } else {
112
+ lines.push('');
113
+ lines.push('');
114
+ lines.push('');
115
+ lines.push('');
116
+ lines.push('');
103
117
  }
104
118
 
105
- console.log('');
119
+ lines.push('');
120
+ screen.render(lines);
106
121
  }
107
122
 
108
123
  /**
@@ -115,14 +130,14 @@ class Menu {
115
130
 
116
131
  /**
117
132
  * Handle keyboard navigation
118
- * @param {boolean} clearScreen - Whether to clear screen on initial display (default: true)
119
133
  * @param {string} versionInfo - Optional version info to display
120
134
  * @param {Function|null} hintCallback - Optional sync callback(selectedIndex) returning hint string or null
135
+ * @param {string} navigationKey - i18n key for navigation hint (default: 'navigation.use_arrows')
121
136
  */
122
- async navigate(clearScreen = true, versionInfo = null, hintCallback = null) {
137
+ async navigate(versionInfo = null, hintCallback = null, navigationKey = 'navigation.use_arrows') {
123
138
  // Guard against empty menu to prevent NaN from modulo operations
124
139
  if (!this.menuOptions || this.menuOptions.length === 0) {
125
- console.log(colors.yellow + ' Warning: No menu options available' + colors.reset);
140
+ screen.write(colors.yellow + ' Warning: No menu options available' + colors.reset + '\n');
126
141
  return -1; // Return cancel/exit code
127
142
  }
128
143
 
@@ -130,7 +145,7 @@ class Menu {
130
145
  this.hintCallback = hintCallback; // Store for redrawing
131
146
 
132
147
  return new Promise((resolve, reject) => {
133
- this.displayMenu(clearScreen, versionInfo, hintCallback);
148
+ this.displayMenu(versionInfo, hintCallback, navigationKey);
134
149
 
135
150
  if (process.stdin.isTTY) {
136
151
  const scope = stdinManager.acquire('raw', {
@@ -183,15 +198,16 @@ class Menu {
183
198
  switch (key) {
184
199
  case '\u001b[A': // Up arrow
185
200
  this.selectedIndex = (this.selectedIndex - 1 + this.menuOptions.length) % this.menuOptions.length;
186
- this.displayMenu(true, this.versionInfo, this.hintCallback);
201
+ this.displayMenu(this.versionInfo, this.hintCallback, this._navigationKey);
187
202
  break;
188
203
 
189
204
  case '\u001b[B': // Down arrow
190
205
  this.selectedIndex = (this.selectedIndex + 1) % this.menuOptions.length;
191
- this.displayMenu(true, this.versionInfo, this.hintCallback);
206
+ this.displayMenu(this.versionInfo, this.hintCallback, this._navigationKey);
192
207
  break;
193
208
 
194
209
  case '\r': // Enter
210
+ case ' ': // Space
195
211
  safeResolve(this.selectedIndex);
196
212
  break;
197
213
 
@@ -219,7 +235,7 @@ class Menu {
219
235
 
220
236
  const rl = lineScope.createReadline();
221
237
 
222
- console.log(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', this.menuOptions.length) + colors.reset);
238
+ screen.write(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', this.menuOptions.length) + colors.reset + '\n');
223
239
 
224
240
  rl.on('line', (input) => {
225
241
  const choice = parseInt(input.trim());
@@ -232,7 +248,7 @@ class Menu {
232
248
  lineScope.release();
233
249
  resolve(-1);
234
250
  } else {
235
- console.log(colors.red + ' Invalid selection. Please enter 1-' + this.menuOptions.length + '.' + colors.reset);
251
+ screen.write(colors.red + ' ' + i18n.tSync('navigation.invalid_selection', this.menuOptions.length) + colors.reset + '\n');
236
252
  }
237
253
  });
238
254
  }
@@ -242,7 +258,7 @@ class Menu {
242
258
  async selectFromList(title, items, activeIndex = -1) {
243
259
  // Guard against empty items list to prevent NaN from modulo operations
244
260
  if (!items || items.length === 0) {
245
- console.log(colors.yellow + ' Warning: No items available to select' + colors.reset);
261
+ screen.write(colors.yellow + ' Warning: No items available to select' + colors.reset + '\n');
246
262
  return null; // Return null to indicate no selection
247
263
  }
248
264
 
@@ -251,12 +267,12 @@ class Menu {
251
267
  let selectedIndex = activeIndex >= 0 ? activeIndex : 0;
252
268
 
253
269
  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('');
270
+ const lines = [];
271
+ lines.push('');
272
+ lines.push(colors.bright + colors.orange + `[*] ${title}` + colors.reset);
273
+ lines.push('');
274
+ lines.push(colors.gray + ' ' + i18n.tSync('navigation.use_arrows_esc', i18n.tSync('navigation.action.select')) + colors.reset);
275
+ lines.push('');
260
276
 
261
277
  items.forEach((item, index) => {
262
278
  const isActive = index === activeIndex;
@@ -265,19 +281,20 @@ class Menu {
265
281
 
266
282
  if (index === selectedIndex) {
267
283
  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);
284
+ const paddedItem = padStringToWidth(itemText, Math.max(40, getStringWidth(itemText) + 2));
285
+ lines.push(colors.orange + prefix + colors.black + colors.bgAmber + paddedItem + colors.reset);
270
286
  if (item.details) {
271
287
  item.details.forEach(detail => {
272
- console.log(colors.gray + ' ' + detail + colors.reset);
288
+ lines.push(colors.gray + ' ' + detail + colors.reset);
273
289
  });
274
290
  }
275
291
  } else {
276
- console.log(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
292
+ lines.push(colors.gray + prefix + `${item.name}${activeIndicator}` + colors.reset);
277
293
  }
278
294
  });
279
295
 
280
- console.log('');
296
+ lines.push('');
297
+ screen.render(lines);
281
298
  };
282
299
 
283
300
  return new Promise((resolve, reject) => {
@@ -370,7 +387,7 @@ class Menu {
370
387
 
371
388
  const rl = lineScope.createReadline();
372
389
 
373
- console.log(colors.yellow + ' Arrow keys not available. Enter item number (1-' + items.length + ') or q to cancel:' + colors.reset);
390
+ screen.write(colors.yellow + ' ' + i18n.tSync('navigation.arrow_keys_not_available', items.length) + colors.reset + '\n');
374
391
 
375
392
  rl.on('line', (input) => {
376
393
  const choice = parseInt(input.trim());
@@ -383,7 +400,7 @@ class Menu {
383
400
  lineScope.release();
384
401
  resolve(null);
385
402
  } else {
386
- console.log(colors.red + ' Invalid selection. Please enter 1-' + items.length + '.' + colors.reset);
403
+ screen.write(colors.red + ' ' + i18n.tSync('navigation.invalid_selection', items.length) + colors.reset + '\n');
387
404
  }
388
405
  });
389
406
  }