agentvibes 5.0.0 → 5.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.
@@ -1,322 +1,325 @@
1
- /**
2
- * AgentVibes TUI — Shared Widget: Background Music Track Picker
3
- *
4
- * Inline modal list for selecting background music tracks.
5
- * Extracted from settings-tab.js for reuse across tabs.
6
- * Space previews track, Enter selects.
7
- */
8
-
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import { spawn } from 'node:child_process';
12
- import { destroyList } from './destroy-list.js';
13
- import { BRAND_PINK } from '../brand-colors.js';
14
- import { formatTrackName } from './format-utils.js';
15
- import { buildAudioEnv, detectMp3Player } from '../audio-env.js';
16
-
17
- const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
18
- let blessed;
19
- if (!IS_TEST) {
20
- const { default: b } = await import('blessed');
21
- blessed = b;
22
- }
23
-
24
- const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
25
- const _hintLabel = '{#455a64-fg}[Space] Preview [Enter] Select [Esc] Cancel{/#455a64-fg}';
26
-
27
- /**
28
- * Open a small volume input modal (0–100).
29
- * Left/Right arrows adjust by 5; type a number directly; Enter confirms.
30
- *
31
- * @param {object} screen - blessed screen
32
- * @param {number} currentVol - current volume (0-100)
33
- * @param {Function} onConfirm - called with volume (number) on Enter
34
- * @param {Function} [onClose] - called when modal closes (confirm or cancel)
35
- */
36
- export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
37
- if (IS_TEST) { onConfirm(currentVol ?? 70); return; }
38
- let vol = (Number.isFinite(currentVol) && currentVol >= 0 && currentVol <= 100)
39
- ? currentVol : 70;
40
-
41
- const box = blessed.box({
42
- parent: screen,
43
- top: 'center',
44
- left: 'center',
45
- width: 44,
46
- height: 11,
47
- border: { type: 'line' },
48
- tags: true,
49
- label: _modalTitle('Music Volume'),
50
- style: { border: { fg: 'bright-cyan' } },
51
- });
52
-
53
- blessed.text({
54
- parent: box,
55
- top: 1,
56
- left: 2,
57
- width: 38,
58
- tags: true,
59
- content: '{cyan-fg}Use ← → arrow keys to adjust volume{/cyan-fg}',
60
- });
61
-
62
- const barText = blessed.text({
63
- parent: box,
64
- top: 3,
65
- left: 2,
66
- width: 38,
67
- tags: true,
68
- content: '',
69
- });
70
-
71
- blessed.text({
72
- parent: box,
73
- top: 5,
74
- left: 2,
75
- width: 38,
76
- tags: true,
77
- content: '{white-fg}[← →] ±5 [0-9] number [Esc] Cancel{/white-fg}',
78
- });
79
-
80
- blessed.text({
81
- parent: box,
82
- top: 7,
83
- left: 2,
84
- width: 38,
85
- tags: true,
86
- content: '{white-fg}[Enter] Confirm then {bold}{cyan-fg}[Tab]{/cyan-fg}{/bold} → Save{/white-fg}',
87
- });
88
-
89
- function _renderBar() {
90
- const filled = Math.round(vol / 5);
91
- const empty = 20 - filled;
92
- const bar = '{bright-cyan-fg}' + '█'.repeat(filled) + '{/bright-cyan-fg}' +
93
- '{#263238-fg}' + '░'.repeat(empty) + '{/#263238-fg}';
94
- barText.setContent(`{white-fg}Volume:{/white-fg} ${bar} {bold}${vol}%{/bold}`);
95
- screen.render();
96
- }
97
- _renderBar();
98
- // Take focus so fieldList's key handlers don't fire while this dialog is open
99
- box.focus();
100
- screen.render();
101
-
102
- // Capture keypress directly on screen to avoid input mode issues
103
- let _digits = '';
104
- function _onKey(ch, key) {
105
- const name = key?.name ?? '';
106
- if (name === 'enter') { _close(true); return; }
107
- if (name === 'escape') { _close(false); return; }
108
- if (name === 'left') { vol = Math.max(0, vol - 5); _digits = ''; _renderBar(); return; }
109
- if (name === 'right') { vol = Math.min(100, vol + 5); _digits = ''; _renderBar(); return; }
110
- if (ch && /^[0-9]$/.test(ch)) {
111
- _digits += ch;
112
- const n = parseInt(_digits, 10);
113
- if (n >= 0 && n <= 100) { vol = n; _renderBar(); }
114
- if (_digits.length >= 3) _digits = '';
115
- }
116
- }
117
- screen.on('keypress', _onKey);
118
-
119
- function _close(confirm) {
120
- screen.removeListener('keypress', _onKey);
121
- box.destroy();
122
- screen.render();
123
- // Defer callbacks so the Enter keypress finishes propagating before fieldList
124
- // regains focus — otherwise the same Enter event re-opens the track picker.
125
- setTimeout(() => {
126
- if (confirm && onConfirm) onConfirm(vol);
127
- if (onClose) onClose();
128
- }, 0);
129
- }
130
- }
131
-
132
- const BUILT_IN_TRACKS = [
133
- { label: '🎻 Soft Flamenco', file: 'agentvibes_soft_flamenco_loop.mp3' },
134
- { label: '🌸 Bossa Nova', file: 'agent_vibes_bossa_nova_v2_loop.mp3' },
135
- { label: '🌊 Chillwave', file: 'agent_vibes_chillwave_v2_loop.mp3' },
136
- { label: '🪘 Gnawa Ambient', file: 'agent_vibes_ganawa_ambient_v2_loop.mp3' },
137
- ];
138
-
139
- /**
140
- * Open the background music track picker modal.
141
- * After selecting a track, prompts for volume (0-100) via openVolumeInput.
142
- *
143
- * @param {object} screen - blessed screen
144
- * @param {string} currentTrack - currently selected track filename
145
- * @param {number} currentVolume - currently set volume (0-100, default 70)
146
- * @param {Function} onSelect - called with (trackFile, volume)
147
- * @param {Function} [onClose] - called after modal fully closes
148
- */
149
- export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose, options = {}) {
150
- const tracksDir = path.join(process.cwd(), '.claude', 'audio', 'tracks');
151
- let tracks;
152
- try {
153
- const files = fs.readdirSync(tracksDir);
154
- tracks = files
155
- .filter(f => /\.mp3$/i.test(f))
156
- .sort()
157
- .map(f => ({ file: f, label: formatTrackName(f) }));
158
- } catch {
159
- tracks = BUILT_IN_TRACKS;
160
- }
161
-
162
- const COLORS = {
163
- btnFocus: '#2e7d32',
164
- btnFocusFg: '#ffffff',
165
- };
166
-
167
- const items = tracks.map(t =>
168
- t.file === currentTrack ? `● ${t.label}` : ` ${t.label}`
169
- );
170
- const currentIdx = tracks.findIndex(t => t.file === currentTrack);
171
-
172
- const listHeight = Math.min(tracks.length + 6, Math.floor(screen.rows * 0.7));
173
- const list = blessed.list({
174
- parent: screen,
175
- top: 'center',
176
- left: 'center',
177
- width: 54,
178
- height: listHeight,
179
- border: { type: 'line' },
180
- tags: true,
181
- label: _modalTitle('Select Track'),
182
- items,
183
- keys: true,
184
- vi: false,
185
- mouse: true,
186
- scrollable: true,
187
- scrollbar: { ch: '│', track: { bg: '#1e2a3a' }, style: { fg: COLORS.btnFocus } },
188
- style: {
189
- border: { fg: COLORS.btnFocus },
190
- selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
191
- item: { fg: '#e3f2fd' },
192
- },
193
- });
194
-
195
- // Helper: update hint text in the bottom border label
196
- function _setHint(text) {
197
- list.setLabel({ text: _modalTitle('Select Track'), side: 'left' });
198
- // Use _ prefix convention for bottom border content (blessed doesn't have setFooter)
199
- list._label2 && list._label2.destroy();
200
- list._label2 = blessed.text({
201
- parent: list,
202
- bottom: -1,
203
- left: 1,
204
- width: 50,
205
- height: 1,
206
- tags: true,
207
- content: text,
208
- style: { fg: '#e3f2fd' },
209
- });
210
- screen.render();
211
- }
212
-
213
- _setHint(_hintLabel);
214
-
215
- if (currentIdx >= 0) list.select(currentIdx);
216
- list.focus();
217
- screen.render();
218
-
219
- // Preview playback state
220
- const _spawnEnv = buildAudioEnv();
221
- const _mp3Player = detectMp3Player(_spawnEnv);
222
- let _previewProc = null;
223
- let _previewTrackId = null;
224
-
225
- function _killPreview() {
226
- if (_previewProc) {
227
- const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
228
- if (_isWin) {
229
- try { _previewProc.kill(); } catch {}
230
- } else {
231
- try { process.kill(-_previewProc.pid, 'SIGTERM'); } catch {}
232
- }
233
- _previewProc = null;
234
- }
235
- _previewTrackId = null;
236
- }
237
-
238
- function _previewTrack(trackFile) {
239
- // Toggle off if same track
240
- if (_previewTrackId === trackFile) {
241
- _killPreview();
242
- _setHint(_hintLabel);
243
- return;
244
- }
245
-
246
- _killPreview();
247
-
248
- const trackPath = path.resolve(tracksDir, trackFile);
249
- const safeBase = path.resolve(tracksDir);
250
- if (!trackPath.startsWith(safeBase + path.sep) && trackPath !== safeBase) return;
251
-
252
- if (!_mp3Player || !fs.existsSync(trackPath)) {
253
- _setHint('{red-fg}No MP3 player found or track missing{/red-fg}');
254
- setTimeout(() => {
255
- _setHint(_hintLabel);
256
- }, 3000);
257
- return;
258
- }
259
-
260
- _previewProc = spawn(_mp3Player.bin, _mp3Player.args(trackPath), {
261
- stdio: 'ignore', detached: true, env: _spawnEnv,
262
- });
263
- _previewTrackId = trackFile;
264
-
265
- const label = tracks.find(t => t.file === trackFile)?.label ?? trackFile;
266
- _setHint(`{bright-cyan-fg}♪ Previewing: ${label} (Space to stop){/bright-cyan-fg}`);
267
-
268
- _previewProc.on('exit', () => {
269
- if (_previewTrackId === trackFile) {
270
- _previewTrackId = null;
271
- _previewProc = null;
272
- _setHint(_hintLabel);
273
- }
274
- });
275
-
276
- _previewProc.on('error', () => {
277
- _previewTrackId = null;
278
- _previewProc = null;
279
- });
280
- }
281
-
282
- function _close(callback) {
283
- _killPreview();
284
- if (list._label2) list._label2.destroy();
285
- if (callback) {
286
- callback();
287
- destroyList(list, screen, onClose);
288
- } else {
289
- destroyList(list, screen, onClose);
290
- }
291
- }
292
-
293
- // Space = preview
294
- list.key(['space'], () => {
295
- const selected = tracks[list.selected];
296
- if (selected) _previewTrack(selected.file);
297
- });
298
-
299
- // Enter = select track; if skipVolume, return track only, otherwise prompt for volume
300
- list.key(['enter'], () => {
301
- const selected = tracks[list.selected];
302
- if (!selected) return;
303
- _killPreview();
304
- if (list._label2) list._label2.destroy();
305
- if (options.skipVolume) {
306
- destroyList(list, screen, null);
307
- setTimeout(() => {
308
- onSelect(selected.file);
309
- if (onClose) onClose();
310
- }, 0);
311
- } else {
312
- destroyList(list, screen, null);
313
- openVolumeInput(screen, currentVolume ?? 20, (volume) => {
314
- onSelect(selected.file, volume);
315
- }, onClose);
316
- }
317
- });
318
-
319
- list.key(['escape', 'q'], () => {
320
- _close();
321
- });
322
- }
1
+ /**
2
+ * AgentVibes TUI — Shared Widget: Background Music Track Picker
3
+ *
4
+ * Inline modal list for selecting background music tracks.
5
+ * Extracted from settings-tab.js for reuse across tabs.
6
+ * Space previews track, Enter selects.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { spawn } from 'node:child_process';
12
+ import { destroyList } from './destroy-list.js';
13
+ import { BRAND_PINK } from '../brand-colors.js';
14
+ import { formatTrackName } from './format-utils.js';
15
+ import { buildAudioEnv, detectMp3Player } from '../audio-env.js';
16
+
17
+ const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
18
+ let blessed;
19
+ if (!IS_TEST) {
20
+ const { default: b } = await import('blessed');
21
+ blessed = b;
22
+ }
23
+
24
+ const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
25
+ const _hintLabel = '{#455a64-fg}[Space] Preview [Enter] Select [Esc] Cancel{/#455a64-fg}';
26
+
27
+ /**
28
+ * Open a small volume input modal (0–100).
29
+ * Left/Right arrows adjust by 5; type a number directly; Enter confirms.
30
+ *
31
+ * @param {object} screen - blessed screen
32
+ * @param {number} currentVol - current volume (0-100)
33
+ * @param {Function} onConfirm - called with volume (number) on Enter
34
+ * @param {Function} [onClose] - called when modal closes (confirm or cancel)
35
+ */
36
+ export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
37
+ if (IS_TEST) { onConfirm(currentVol ?? 70); return; }
38
+ let vol = (Number.isFinite(currentVol) && currentVol >= 0 && currentVol <= 100)
39
+ ? currentVol : 70;
40
+
41
+ const box = blessed.box({
42
+ parent: screen,
43
+ top: 'center',
44
+ left: 'center',
45
+ width: 44,
46
+ height: 11,
47
+ border: { type: 'line' },
48
+ tags: true,
49
+ label: _modalTitle('Music Volume'),
50
+ style: { border: { fg: 'bright-cyan' } },
51
+ });
52
+
53
+ blessed.text({
54
+ parent: box,
55
+ top: 1,
56
+ left: 2,
57
+ width: 38,
58
+ tags: true,
59
+ content: '{cyan-fg}Use ← → arrow keys to adjust volume{/cyan-fg}',
60
+ });
61
+
62
+ const barText = blessed.text({
63
+ parent: box,
64
+ top: 3,
65
+ left: 2,
66
+ width: 38,
67
+ tags: true,
68
+ content: '',
69
+ });
70
+
71
+ blessed.text({
72
+ parent: box,
73
+ top: 5,
74
+ left: 2,
75
+ width: 38,
76
+ tags: true,
77
+ content: '{white-fg}[← →] ±5 [0-9] number [Esc] Cancel{/white-fg}',
78
+ });
79
+
80
+ blessed.text({
81
+ parent: box,
82
+ top: 7,
83
+ left: 2,
84
+ width: 38,
85
+ tags: true,
86
+ content: '{white-fg}[Enter] Confirm then {bold}{cyan-fg}[Tab]{/cyan-fg}{/bold} → Save{/white-fg}',
87
+ });
88
+
89
+ function _renderBar() {
90
+ const filled = Math.round(vol / 5);
91
+ const empty = 20 - filled;
92
+ const bar = '{bright-cyan-fg}' + '█'.repeat(filled) + '{/bright-cyan-fg}' +
93
+ '{#263238-fg}' + '░'.repeat(empty) + '{/#263238-fg}';
94
+ barText.setContent(`{white-fg}Volume:{/white-fg} ${bar} {bold}${vol}%{/bold}`);
95
+ screen.render();
96
+ }
97
+ _renderBar();
98
+ // Take focus so fieldList's key handlers don't fire while this dialog is open
99
+ box.focus();
100
+ screen.render();
101
+
102
+ // Capture keypress directly on screen to avoid input mode issues
103
+ let _digits = '';
104
+ function _onKey(ch, key) {
105
+ const name = key?.name ?? '';
106
+ if (name === 'enter') { _close(true); return; }
107
+ if (name === 'escape') { _close(false); return; }
108
+ if (name === 'left') { vol = Math.max(0, vol - 5); _digits = ''; _renderBar(); return; }
109
+ if (name === 'right') { vol = Math.min(100, vol + 5); _digits = ''; _renderBar(); return; }
110
+ if (ch && /^[0-9]$/.test(ch)) {
111
+ _digits += ch;
112
+ const n = parseInt(_digits, 10);
113
+ if (n >= 0 && n <= 100) { vol = n; _renderBar(); }
114
+ if (_digits.length >= 3) _digits = '';
115
+ }
116
+ }
117
+ screen.on('keypress', _onKey);
118
+
119
+ function _close(confirm) {
120
+ screen.removeListener('keypress', _onKey);
121
+ box.destroy();
122
+ screen.render();
123
+ // Defer callbacks so the Enter keypress finishes propagating before fieldList
124
+ // regains focus — otherwise the same Enter event re-opens the track picker.
125
+ setTimeout(() => {
126
+ if (confirm && onConfirm) onConfirm(vol);
127
+ if (onClose) onClose();
128
+ }, 0);
129
+ }
130
+ }
131
+
132
+ const BUILT_IN_TRACKS = [
133
+ { label: '🎻 Soft Flamenco', file: 'agentvibes_soft_flamenco_loop.mp3' },
134
+ { label: '🌸 Bossa Nova', file: 'agent_vibes_bossa_nova_v2_loop.mp3' },
135
+ { label: '🌊 Chillwave', file: 'agent_vibes_chillwave_v2_loop.mp3' },
136
+ { label: '🪘 Gnawa Ambient', file: 'agent_vibes_ganawa_ambient_v2_loop.mp3' },
137
+ ];
138
+
139
+ /**
140
+ * Open the background music track picker modal.
141
+ * After selecting a track, prompts for volume (0-100) via openVolumeInput.
142
+ *
143
+ * @param {object} screen - blessed screen
144
+ * @param {string} currentTrack - currently selected track filename
145
+ * @param {number} currentVolume - currently set volume (0-100, default 70)
146
+ * @param {Function} onSelect - called with (trackFile, volume)
147
+ * @param {Function} [onClose] - called after modal fully closes
148
+ */
149
+ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose, options = {}) {
150
+ const tracksDir = path.join(process.cwd(), '.claude', 'audio', 'tracks');
151
+ let tracks;
152
+ try {
153
+ const files = fs.readdirSync(tracksDir);
154
+ // Sort by the alphabetic part of the label (skip leading emoji/symbols)
155
+ // so the order reflects the track NAME, not the emoji codepoint.
156
+ const _sortKey = (s) => s.replace(/^[^a-zA-Z]+/, '');
157
+ tracks = files
158
+ .filter(f => /\.mp3$/i.test(f))
159
+ .map(f => ({ file: f, label: formatTrackName(f) }))
160
+ .sort((a, b) => _sortKey(a.label).localeCompare(_sortKey(b.label), undefined, { sensitivity: 'base' }));
161
+ } catch {
162
+ tracks = BUILT_IN_TRACKS;
163
+ }
164
+
165
+ const COLORS = {
166
+ btnFocus: '#2e7d32',
167
+ btnFocusFg: '#ffffff',
168
+ };
169
+
170
+ const items = tracks.map(t =>
171
+ t.file === currentTrack ? `● ${t.label}` : ` ${t.label}`
172
+ );
173
+ const currentIdx = tracks.findIndex(t => t.file === currentTrack);
174
+
175
+ const listHeight = Math.min(tracks.length + 6, Math.floor(screen.rows * 0.7));
176
+ const list = blessed.list({
177
+ parent: screen,
178
+ top: 'center',
179
+ left: 'center',
180
+ width: 54,
181
+ height: listHeight,
182
+ border: { type: 'line' },
183
+ tags: true,
184
+ label: _modalTitle('Select Track'),
185
+ items,
186
+ keys: true,
187
+ vi: false,
188
+ mouse: true,
189
+ scrollable: true,
190
+ scrollbar: { ch: '│', track: { bg: '#1e2a3a' }, style: { fg: COLORS.btnFocus } },
191
+ style: {
192
+ border: { fg: COLORS.btnFocus },
193
+ selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
194
+ item: { fg: '#e3f2fd' },
195
+ },
196
+ });
197
+
198
+ // Helper: update hint text in the bottom border label
199
+ function _setHint(text) {
200
+ list.setLabel({ text: _modalTitle('Select Track'), side: 'left' });
201
+ // Use _ prefix convention for bottom border content (blessed doesn't have setFooter)
202
+ list._label2 && list._label2.destroy();
203
+ list._label2 = blessed.text({
204
+ parent: list,
205
+ bottom: -1,
206
+ left: 1,
207
+ width: 50,
208
+ height: 1,
209
+ tags: true,
210
+ content: text,
211
+ style: { fg: '#e3f2fd' },
212
+ });
213
+ screen.render();
214
+ }
215
+
216
+ _setHint(_hintLabel);
217
+
218
+ if (currentIdx >= 0) list.select(currentIdx);
219
+ list.focus();
220
+ screen.render();
221
+
222
+ // Preview playback state
223
+ const _spawnEnv = buildAudioEnv();
224
+ const _mp3Player = detectMp3Player(_spawnEnv);
225
+ let _previewProc = null;
226
+ let _previewTrackId = null;
227
+
228
+ function _killPreview() {
229
+ if (_previewProc) {
230
+ const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
231
+ if (_isWin) {
232
+ try { _previewProc.kill(); } catch {}
233
+ } else {
234
+ try { process.kill(-_previewProc.pid, 'SIGTERM'); } catch {}
235
+ }
236
+ _previewProc = null;
237
+ }
238
+ _previewTrackId = null;
239
+ }
240
+
241
+ function _previewTrack(trackFile) {
242
+ // Toggle off if same track
243
+ if (_previewTrackId === trackFile) {
244
+ _killPreview();
245
+ _setHint(_hintLabel);
246
+ return;
247
+ }
248
+
249
+ _killPreview();
250
+
251
+ const trackPath = path.resolve(tracksDir, trackFile);
252
+ const safeBase = path.resolve(tracksDir);
253
+ if (!trackPath.startsWith(safeBase + path.sep) && trackPath !== safeBase) return;
254
+
255
+ if (!_mp3Player || !fs.existsSync(trackPath)) {
256
+ _setHint('{red-fg}No MP3 player found or track missing{/red-fg}');
257
+ setTimeout(() => {
258
+ _setHint(_hintLabel);
259
+ }, 3000);
260
+ return;
261
+ }
262
+
263
+ _previewProc = spawn(_mp3Player.bin, _mp3Player.args(trackPath), {
264
+ stdio: 'ignore', detached: true, env: _spawnEnv,
265
+ });
266
+ _previewTrackId = trackFile;
267
+
268
+ const label = tracks.find(t => t.file === trackFile)?.label ?? trackFile;
269
+ _setHint(`{bright-cyan-fg}♪ Previewing: ${label} (Space to stop){/bright-cyan-fg}`);
270
+
271
+ _previewProc.on('exit', () => {
272
+ if (_previewTrackId === trackFile) {
273
+ _previewTrackId = null;
274
+ _previewProc = null;
275
+ _setHint(_hintLabel);
276
+ }
277
+ });
278
+
279
+ _previewProc.on('error', () => {
280
+ _previewTrackId = null;
281
+ _previewProc = null;
282
+ });
283
+ }
284
+
285
+ function _close(callback) {
286
+ _killPreview();
287
+ if (list._label2) list._label2.destroy();
288
+ if (callback) {
289
+ callback();
290
+ destroyList(list, screen, onClose);
291
+ } else {
292
+ destroyList(list, screen, onClose);
293
+ }
294
+ }
295
+
296
+ // Space = preview
297
+ list.key(['space'], () => {
298
+ const selected = tracks[list.selected];
299
+ if (selected) _previewTrack(selected.file);
300
+ });
301
+
302
+ // Enter = select track; if skipVolume, return track only, otherwise prompt for volume
303
+ list.key(['enter'], () => {
304
+ const selected = tracks[list.selected];
305
+ if (!selected) return;
306
+ _killPreview();
307
+ if (list._label2) list._label2.destroy();
308
+ if (options.skipVolume) {
309
+ destroyList(list, screen, null);
310
+ setTimeout(() => {
311
+ onSelect(selected.file);
312
+ if (onClose) onClose();
313
+ }, 0);
314
+ } else {
315
+ destroyList(list, screen, null);
316
+ openVolumeInput(screen, currentVolume ?? 20, (volume) => {
317
+ onSelect(selected.file, volume);
318
+ }, onClose);
319
+ }
320
+ });
321
+
322
+ list.key(['escape', 'q'], () => {
323
+ _close();
324
+ });
325
+ }