@naxodev/opencode-music-player 0.2.5 → 0.3.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/dist/index.js ADDED
@@ -0,0 +1,2272 @@
1
+ // @bun
2
+ // index.tsx
3
+ import { createComponent as _$createComponent3 } from "@opentui/solid";
4
+ import { Plugin } from "@opencode/plugin/tui";
5
+
6
+ // system-media.ts
7
+ import {
8
+ baselineCapabilities,
9
+ createReconnectingMusicSessionClient
10
+ } from "@naxodev/music-core";
11
+
12
+ // artwork.ts
13
+ import { mkdtemp, rm } from "fs/promises";
14
+ import { tmpdir } from "os";
15
+ import { join } from "path";
16
+ import { PNG } from "pngjs";
17
+ import {
18
+ acquireCatalogArtwork,
19
+ MAX_ARTWORK_BYTES,
20
+ selectCatalogResolution
21
+ } from "@naxodev/music-core";
22
+ import {
23
+ selectCatalogResolution as selectCatalogResolution2,
24
+ selectCatalogTrack,
25
+ readLimitedResponse,
26
+ downloadCatalogImage
27
+ } from "@naxodev/music-core";
28
+ var MAX_CONVERTED_PNG_BYTES = 1e6;
29
+ var MAX_IMAGE_DIMENSION = 4096;
30
+ var MAX_IMAGE_PIXELS = 12000000;
31
+ var CONVERSION_TIMEOUT_MS = 3000;
32
+ function imageDimensionsAreSafe(width, height) {
33
+ return Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0 && width <= MAX_IMAGE_DIMENSION && height <= MAX_IMAGE_DIMENSION && width * height <= MAX_IMAGE_PIXELS;
34
+ }
35
+ async function runCommandWithTimeout(command, timeoutMs) {
36
+ const proc = Bun.spawn(command, { stdout: "pipe", stderr: "ignore" });
37
+ let timedOut = false;
38
+ const timer = setTimeout(() => {
39
+ timedOut = true;
40
+ proc.kill(9);
41
+ }, timeoutMs);
42
+ try {
43
+ const [out, code] = await Promise.all([
44
+ new Response(proc.stdout).text(),
45
+ proc.exited
46
+ ]);
47
+ return { code, out, timed_out: timedOut };
48
+ } finally {
49
+ clearTimeout(timer);
50
+ }
51
+ }
52
+ function dimensionsFromSips(output) {
53
+ const width = Number(output.match(/pixelWidth:\s*(\d+)/)?.[1]);
54
+ const height = Number(output.match(/pixelHeight:\s*(\d+)/)?.[1]);
55
+ return imageDimensionsAreSafe(width, height) ? { width, height } : null;
56
+ }
57
+ async function squarePng(bytes, size) {
58
+ if (bytes.byteLength === 0 || bytes.byteLength > MAX_ARTWORK_BYTES)
59
+ return null;
60
+ const dir = await mkdtemp(join(tmpdir(), "opencode-music-art-"));
61
+ const input = join(dir, "input");
62
+ const output = join(dir, "art.png");
63
+ try {
64
+ await Bun.write(input, bytes);
65
+ const inspected = await runCommandWithTimeout(["sips", "-g", "pixelWidth", "-g", "pixelHeight", input], CONVERSION_TIMEOUT_MS);
66
+ if (inspected.timed_out || inspected.code !== 0 || !dimensionsFromSips(inspected.out)) {
67
+ return null;
68
+ }
69
+ const converted = await runCommandWithTimeout([
70
+ "sips",
71
+ "-z",
72
+ String(size),
73
+ String(size),
74
+ "-s",
75
+ "format",
76
+ "png",
77
+ input,
78
+ "--out",
79
+ output
80
+ ], CONVERSION_TIMEOUT_MS);
81
+ if (converted.timed_out || converted.code !== 0)
82
+ return null;
83
+ const file = Bun.file(output);
84
+ if (file.size === 0 || file.size > MAX_CONVERTED_PNG_BYTES)
85
+ return null;
86
+ return new Uint8Array(await file.arrayBuffer());
87
+ } finally {
88
+ await rm(dir, { recursive: true, force: true });
89
+ }
90
+ }
91
+ var hex = (r, g, b) => `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
92
+ function presentation(pngBytes) {
93
+ const image = PNG.sync.read(Buffer.from(pngBytes));
94
+ const cells = [];
95
+ let accent = "#7aa2f7";
96
+ let accentScore = -1;
97
+ for (let y = 0;y < image.height; y += 2) {
98
+ const row = [];
99
+ for (let x = 0;x < image.width; x++) {
100
+ const colors = [y, Math.min(y + 1, image.height - 1)].map((py) => {
101
+ const offset = (py * image.width + x) * 4;
102
+ const r = image.data[offset] ?? 0;
103
+ const g = image.data[offset + 1] ?? 0;
104
+ const b = image.data[offset + 2] ?? 0;
105
+ const max = Math.max(r, g, b) / 255;
106
+ const min = Math.min(r, g, b) / 255;
107
+ const light = (max + min) / 2;
108
+ const score = (max - min) * (1 - Math.abs(light - 0.5) * 2);
109
+ if (score > accentScore) {
110
+ accentScore = score;
111
+ accent = hex(r, g, b);
112
+ }
113
+ return hex(r, g, b);
114
+ });
115
+ row.push({ upper: colors[0], lower: colors[1] });
116
+ }
117
+ cells.push(row);
118
+ }
119
+ return { cells, accent };
120
+ }
121
+ async function resolveArtworkDetails(id, target, nativeBase64, legacyId = id, fetcher = fetch, signal) {
122
+ if (signal?.aborted)
123
+ return { artwork: null, duration_ms: target.duration_ms };
124
+ const candidates = [];
125
+ if (nativeBase64) {
126
+ try {
127
+ const maxBase64Length = Math.ceil(MAX_ARTWORK_BYTES / 3) * 4;
128
+ if (nativeBase64.length <= maxBase64Length) {
129
+ const bytes = new Uint8Array(Buffer.from(nativeBase64, "base64"));
130
+ if (bytes.byteLength <= MAX_ARTWORK_BYTES)
131
+ candidates.push(bytes);
132
+ }
133
+ } catch {}
134
+ }
135
+ for (const bytes of candidates) {
136
+ try {
137
+ const nativePng = await squarePng(bytes, 300);
138
+ const thumbnail = nativePng ? await squarePng(nativePng, 24) : null;
139
+ if (nativePng && thumbnail) {
140
+ return {
141
+ artwork: {
142
+ id,
143
+ legacy_id: legacyId,
144
+ png_base64: Buffer.from(nativePng).toString("base64"),
145
+ ...presentation(thumbnail)
146
+ },
147
+ duration_ms: target.duration_ms
148
+ };
149
+ }
150
+ } catch {}
151
+ }
152
+ const catalog = await acquireCatalogArtwork(target, {
153
+ fetch: fetcher,
154
+ signal
155
+ });
156
+ if (catalog.kind !== "available" || signal?.aborted)
157
+ return {
158
+ artwork: null,
159
+ duration_ms: catalog.duration_ms ?? target.duration_ms
160
+ };
161
+ if (!catalog.bytes) {
162
+ return { artwork: null, duration_ms: catalog.duration_ms };
163
+ }
164
+ try {
165
+ const nativePng = await squarePng(catalog.bytes, 300);
166
+ const thumbnail = nativePng ? await squarePng(nativePng, 24) : null;
167
+ if (!nativePng || !thumbnail) {
168
+ return { artwork: null, duration_ms: catalog.duration_ms };
169
+ }
170
+ return {
171
+ artwork: {
172
+ id,
173
+ legacy_id: legacyId,
174
+ png_base64: Buffer.from(nativePng).toString("base64"),
175
+ ...presentation(thumbnail)
176
+ },
177
+ duration_ms: catalog.duration_ms
178
+ };
179
+ } catch {
180
+ return { artwork: null, duration_ms: catalog.duration_ms };
181
+ }
182
+ }
183
+
184
+ // system-media.ts
185
+ var MAX_ARTWORK_ENTRIES = 32;
186
+ var artworkCache = new Map;
187
+ var artworkJobs = new Map;
188
+ var waitingArtwork = new Map;
189
+ var sessionClientSequence = 0;
190
+ var createOpenCodeSessionClient = (signal) => createReconnectingMusicSessionClient({
191
+ clientId: `opencode-music-player-${++sessionClientSequence}`,
192
+ hostKind: "opencode",
193
+ capabilities: [...baselineCapabilities],
194
+ ...signal ? { signal } : {}
195
+ });
196
+ function artworkIdentityKey(identity) {
197
+ return JSON.stringify([
198
+ identity.uid,
199
+ identity.title,
200
+ identity.artist,
201
+ identity.album,
202
+ identity.duration_ms
203
+ ]);
204
+ }
205
+ function artworkCacheKey(identity) {
206
+ return JSON.stringify([
207
+ identity.title,
208
+ identity.artist,
209
+ identity.album,
210
+ identity.duration_ms
211
+ ]);
212
+ }
213
+ function identityFromTrack(track) {
214
+ return {
215
+ uid: track.id,
216
+ title: track.name,
217
+ artist: track.artists,
218
+ album: track.album,
219
+ duration_ms: track.duration_ms
220
+ };
221
+ }
222
+ function removeWaitingInterest(host) {
223
+ for (const [key, deferred] of waitingArtwork) {
224
+ deferred.interests.delete(host);
225
+ if (deferred.interests.size === 0)
226
+ waitingArtwork.delete(key);
227
+ }
228
+ }
229
+ function admitDeferredArtwork() {
230
+ const next = waitingArtwork.entries().next().value;
231
+ if (!next)
232
+ return;
233
+ const [key, deferred] = next;
234
+ waitingArtwork.delete(key);
235
+ for (const host of deferred.interests.keys()) {
236
+ if (!host.isActive())
237
+ deferred.interests.delete(host);
238
+ }
239
+ const leader = deferred.interests.values().next().value;
240
+ if (!leader) {
241
+ admitDeferredArtwork();
242
+ return;
243
+ }
244
+ artworkForTrack(key, leader.legacyKey, leader.target, leader.native, leader.resolver, leader.host, leader.identity, leader.now);
245
+ const admitted = artworkJobs.get(key);
246
+ if (!admitted)
247
+ return;
248
+ for (const interest of deferred.interests.values()) {
249
+ if (interest.host.isActive())
250
+ admitted.interests.set(interest.host, interest.identity);
251
+ }
252
+ }
253
+ function releaseArtworkSlot(key, entry) {
254
+ artworkJobs.delete(key);
255
+ if (entry) {
256
+ artworkCache.set(key, entry);
257
+ if (artworkCache.size > MAX_ARTWORK_ENTRIES) {
258
+ const oldest = artworkCache.keys().next().value;
259
+ if (oldest)
260
+ artworkCache.delete(oldest);
261
+ }
262
+ }
263
+ admitDeferredArtwork();
264
+ }
265
+ function settleArtworkEntry(key, entry) {
266
+ releaseArtworkSlot(key, entry);
267
+ }
268
+ function publishArtworkCompletion(entry, artwork) {
269
+ for (const [host, identity] of entry.interests) {
270
+ if (!host.isActive() || !host.accepts(identity))
271
+ continue;
272
+ host.publish({
273
+ type: "artwork-completion",
274
+ identity,
275
+ artwork,
276
+ duration_ms: entry.duration_ms
277
+ });
278
+ }
279
+ entry.interests.clear();
280
+ }
281
+ function removeArtworkInterests(host) {
282
+ removeWaitingInterest(host);
283
+ for (const entry of artworkCache.values())
284
+ entry.interests.delete(host);
285
+ for (const entry of artworkJobs.values()) {
286
+ entry.interests.delete(host);
287
+ if (!entry.interests.size)
288
+ entry.abort.abort();
289
+ }
290
+ }
291
+ function artworkForTrack(key, legacyKey, target, native, resolver, host, identity, now) {
292
+ let entry = artworkJobs.get(key) ?? artworkCache.get(key);
293
+ if (entry?.abort.signal.aborted)
294
+ entry = undefined;
295
+ if (!entry) {
296
+ if (artworkJobs.has(key) || artworkJobs.size >= MAX_ARTWORK_ENTRIES) {
297
+ removeWaitingInterest(host);
298
+ let deferred = waitingArtwork.get(key);
299
+ if (!deferred && waitingArtwork.size < MAX_ARTWORK_ENTRIES) {
300
+ deferred = { interests: new Map };
301
+ waitingArtwork.set(key, deferred);
302
+ }
303
+ if (deferred && deferred.interests.size < MAX_ARTWORK_ENTRIES)
304
+ deferred.interests.set(host, {
305
+ legacyKey,
306
+ target,
307
+ native,
308
+ resolver,
309
+ host,
310
+ identity,
311
+ now
312
+ });
313
+ return {
314
+ artwork: null,
315
+ duration_ms: target.duration_ms,
316
+ loading: !!deferred && deferred.interests.has(host)
317
+ };
318
+ }
319
+ entry = {
320
+ value: null,
321
+ duration_ms: target.duration_ms,
322
+ resolved: false,
323
+ pending: false,
324
+ abort: new AbortController,
325
+ interests: new Map
326
+ };
327
+ }
328
+ removeWaitingInterest(host);
329
+ if (!entry.pending && !entry.resolved) {
330
+ entry.pending = true;
331
+ artworkJobs.set(key, entry);
332
+ const activeEntry = entry;
333
+ (async () => {
334
+ let data = null;
335
+ try {
336
+ data = await native();
337
+ } catch {}
338
+ return resolver(key, target, data, legacyKey, undefined, activeEntry.abort.signal);
339
+ })().then((resolution) => {
340
+ activeEntry.value = resolution.artwork;
341
+ activeEntry.duration_ms = resolution.duration_ms;
342
+ activeEntry.resolved = true;
343
+ activeEntry.pending = false;
344
+ if (activeEntry.abort.signal.aborted) {
345
+ releaseArtworkSlot(key);
346
+ return;
347
+ }
348
+ settleArtworkEntry(key, activeEntry);
349
+ publishArtworkCompletion(activeEntry, resolution.artwork);
350
+ }, () => {
351
+ activeEntry.value = null;
352
+ activeEntry.resolved = true;
353
+ activeEntry.pending = false;
354
+ if (activeEntry.abort.signal.aborted) {
355
+ releaseArtworkSlot(key);
356
+ return;
357
+ }
358
+ settleArtworkEntry(key, activeEntry);
359
+ publishArtworkCompletion(activeEntry, null);
360
+ });
361
+ }
362
+ if (entry.pending)
363
+ entry.interests.set(host, identity);
364
+ return {
365
+ artwork: entry.value,
366
+ duration_ms: entry.duration_ms,
367
+ loading: entry.pending
368
+ };
369
+ }
370
+ function createSessionSystemMedia(overrides = {}) {
371
+ const factory = overrides.createClient ?? createOpenCodeSessionClient;
372
+ const resolver = overrides.resolveArtworkDetails ?? resolveArtworkDetails;
373
+ const now = overrides.now ?? Date.now;
374
+ const listeners = new Set;
375
+ const presentationListeners = new Set;
376
+ const acquisitionAbort = new AbortController;
377
+ let disposed = false;
378
+ let currentArtworkIdentity = null;
379
+ let currentArtworkKey = null;
380
+ let client;
381
+ let installed = false;
382
+ let latest;
383
+ let latestStatus;
384
+ let latestConnection;
385
+ let acquisitionError;
386
+ let publishedLifecycle;
387
+ let unsubscribers = [];
388
+ let clientReleased = false;
389
+ let disposal;
390
+ const releaseClient = async (next) => {
391
+ if (clientReleased)
392
+ return;
393
+ clientReleased = true;
394
+ await next.dispose();
395
+ };
396
+ const emit = (event) => {
397
+ if (disposed)
398
+ return;
399
+ for (const listener of [...listeners]) {
400
+ try {
401
+ listener(event);
402
+ } catch {}
403
+ }
404
+ };
405
+ const host = {
406
+ publish(event) {
407
+ if (disposed)
408
+ return;
409
+ for (const listener of [...presentationListeners]) {
410
+ try {
411
+ listener(event);
412
+ } catch {}
413
+ }
414
+ },
415
+ isActive: () => !disposed,
416
+ accepts: (identity) => currentArtworkIdentity === artworkIdentityKey(identity)
417
+ };
418
+ const lifecycle = () => {
419
+ if (latestConnection?.type === "reconnecting" || latestConnection?.type === "terminal")
420
+ return {
421
+ type: "lifecycle",
422
+ message: latestConnection.error.message,
423
+ source: "connection"
424
+ };
425
+ if (latestConnection?.type === "disposed")
426
+ return {
427
+ type: "lifecycle",
428
+ message: "music session is disposed",
429
+ source: "connection"
430
+ };
431
+ if (acquisitionError)
432
+ return {
433
+ type: "lifecycle",
434
+ message: acquisitionError,
435
+ source: "acquisition"
436
+ };
437
+ return {
438
+ type: "lifecycle",
439
+ message: latestStatus && latestStatus.kind !== "ready" ? latestStatus.message : null,
440
+ source: "provider"
441
+ };
442
+ };
443
+ const publishLifecycle = () => {
444
+ const event = lifecycle();
445
+ const key = `${event.source}:${event.message}`;
446
+ if (publishedLifecycle === key)
447
+ return;
448
+ publishedLifecycle = key;
449
+ emit(event);
450
+ };
451
+ const project = (state) => {
452
+ const nextKey = state?.state.track ? artworkCacheKey(identityFromTrack(state.state.track)) : null;
453
+ if (nextKey !== currentArtworkKey)
454
+ removeArtworkInterests(host);
455
+ currentArtworkKey = nextKey;
456
+ if (!state) {
457
+ currentArtworkIdentity = null;
458
+ return null;
459
+ }
460
+ if (!state.state.track) {
461
+ currentArtworkIdentity = null;
462
+ return state.state;
463
+ }
464
+ const track = state.state.track;
465
+ const identity = identityFromTrack(track);
466
+ currentArtworkIdentity = artworkIdentityKey(identity);
467
+ const artworkState = artworkForTrack(artworkCacheKey(identity), artworkIdentityKey(identity), {
468
+ title: track.name,
469
+ artist: track.artists,
470
+ album: track.album,
471
+ duration_ms: track.duration_ms
472
+ }, async () => {
473
+ const active = await clientPromise;
474
+ if (disposed || active !== client)
475
+ return null;
476
+ const result = await active.artwork({
477
+ id: track.id,
478
+ name: track.name,
479
+ artists: track.artists,
480
+ album: track.album,
481
+ duration_ms: track.duration_ms
482
+ });
483
+ return active === client && result.type === "available" ? result.base64 : null;
484
+ }, resolver, host, identity, now);
485
+ return {
486
+ ...state.state,
487
+ track: {
488
+ ...track,
489
+ artwork: artworkState.artwork,
490
+ artwork_loading: artworkState.loading
491
+ }
492
+ };
493
+ };
494
+ const install = (next) => {
495
+ if (installed)
496
+ return;
497
+ installed = true;
498
+ client = next;
499
+ latest = next.state;
500
+ latestStatus = next.status;
501
+ latestConnection = next.connection;
502
+ unsubscribers = [
503
+ next.subscribeState((state) => {
504
+ if (disposed)
505
+ return;
506
+ latest = state;
507
+ emit({ type: "snapshot", state: project(state) });
508
+ }),
509
+ next.subscribeStatus((status) => {
510
+ if (disposed)
511
+ return;
512
+ latestStatus = status;
513
+ publishLifecycle();
514
+ }),
515
+ next.subscribeConnection((connection) => {
516
+ if (disposed)
517
+ return;
518
+ latestConnection = connection;
519
+ publishLifecycle();
520
+ })
521
+ ];
522
+ };
523
+ const clientPromise = Promise.resolve().then(() => factory(acquisitionAbort.signal)).then((next) => {
524
+ if (disposed) {
525
+ releaseClient(next).catch((error) => {
526
+ console.error("Failed to dispose late music session client", error);
527
+ });
528
+ return next;
529
+ }
530
+ install(next);
531
+ return next;
532
+ });
533
+ clientPromise.catch((error) => {
534
+ if (disposed)
535
+ return;
536
+ acquisitionError = error instanceof Error ? error.message : String(error);
537
+ publishLifecycle();
538
+ });
539
+ const activeClient = async () => {
540
+ const next = await clientPromise;
541
+ if (disposed || next !== client)
542
+ throw new Error("music session is disposed");
543
+ return next;
544
+ };
545
+ return {
546
+ async player() {
547
+ await activeClient();
548
+ return project(latest);
549
+ },
550
+ async refreshArtwork() {
551
+ await activeClient();
552
+ const track = latest?.state.track;
553
+ if (!track)
554
+ return;
555
+ const key = artworkCacheKey(identityFromTrack(track));
556
+ if (artworkJobs.has(key))
557
+ return;
558
+ artworkCache.delete(key);
559
+ emit({ type: "snapshot", state: project(latest) });
560
+ },
561
+ async play() {
562
+ await (await activeClient()).play();
563
+ },
564
+ async pause() {
565
+ await (await activeClient()).pause();
566
+ },
567
+ async next() {
568
+ await (await activeClient()).next();
569
+ },
570
+ async previous() {
571
+ await (await activeClient()).previous();
572
+ },
573
+ async seek(positionMs) {
574
+ await (await activeClient()).seek(positionMs);
575
+ },
576
+ subscribe(listener) {
577
+ if (disposed)
578
+ return () => {};
579
+ let closed = false;
580
+ listeners.add(listener);
581
+ try {
582
+ if (latest)
583
+ listener({ type: "snapshot", state: project(latest) });
584
+ if (installed || acquisitionError)
585
+ listener(lifecycle());
586
+ } catch {}
587
+ return () => {
588
+ if (closed)
589
+ return;
590
+ closed = true;
591
+ listeners.delete(listener);
592
+ };
593
+ },
594
+ subscribePresentation(listener) {
595
+ if (disposed)
596
+ return () => {};
597
+ let closed = false;
598
+ presentationListeners.add(listener);
599
+ return () => {
600
+ if (closed)
601
+ return;
602
+ closed = true;
603
+ presentationListeners.delete(listener);
604
+ };
605
+ },
606
+ dispose() {
607
+ if (disposal)
608
+ return disposal;
609
+ disposed = true;
610
+ disposal = Promise.resolve().then(async () => {
611
+ const errors = [];
612
+ for (const unsubscribe of unsubscribers.splice(0)) {
613
+ try {
614
+ unsubscribe();
615
+ } catch (error) {
616
+ errors.push(error);
617
+ }
618
+ }
619
+ listeners.clear();
620
+ presentationListeners.clear();
621
+ removeArtworkInterests(host);
622
+ if (client) {
623
+ try {
624
+ await releaseClient(client);
625
+ } catch (error) {
626
+ errors.push(error);
627
+ }
628
+ }
629
+ if (errors.length)
630
+ throw new AggregateError(errors, "music session cleanup failed");
631
+ });
632
+ acquisitionAbort.abort();
633
+ return disposal;
634
+ }
635
+ };
636
+ }
637
+ function openNowPlayingApp() {
638
+ Bun.spawn(["open", "https://music.youtube.com"], {
639
+ stdout: "ignore",
640
+ stderr: "ignore"
641
+ });
642
+ }
643
+
644
+ // types.ts
645
+ import {
646
+ emptyPlayer as emptyCorePlayer,
647
+ formatMs,
648
+ isMac
649
+ } from "@naxodev/music-core";
650
+ function mergeArtworkCompletion(player, event) {
651
+ const track = player?.track;
652
+ if (!track || track.name !== event.identity.title || track.artists !== event.identity.artist || track.album !== event.identity.album || track.duration_ms > 0 && track.duration_ms !== event.identity.duration_ms) {
653
+ return player;
654
+ }
655
+ return {
656
+ ...player,
657
+ track: {
658
+ ...track,
659
+ artwork: event.artwork,
660
+ artwork_loading: false
661
+ }
662
+ };
663
+ }
664
+ function mergePlayerSnapshot(player, snapshot) {
665
+ const current = player?.track;
666
+ const next = snapshot.track;
667
+ if (!current || !next || current.name !== next.name || current.artists !== next.artists || current.album !== next.album || current.duration_ms > 0 && next.duration_ms > 0 && current.duration_ms !== next.duration_ms || current.artwork === null && current.artwork_loading !== false) {
668
+ return snapshot;
669
+ }
670
+ return {
671
+ ...snapshot,
672
+ track: {
673
+ ...next,
674
+ artwork: next.artwork ?? current.artwork,
675
+ artwork_loading: false
676
+ }
677
+ };
678
+ }
679
+
680
+ // ui.tsx
681
+ import { memo as _$memo } from "@opentui/solid";
682
+ import { effect as _$effect2 } from "@opentui/solid";
683
+ import { insertNode as _$insertNode2 } from "@opentui/solid";
684
+ import { insert as _$insert3 } from "@opentui/solid";
685
+ import { use as _$use3 } from "@opentui/solid";
686
+ import { setProp as _$setProp2 } from "@opentui/solid";
687
+ import { createElement as _$createElement3 } from "@opentui/solid";
688
+ import { createComponent as _$createComponent2 } from "@opentui/solid";
689
+ import { Show, createEffect, createMemo, createSignal, onCleanup as onCleanup3, onMount as onMount3 } from "solid-js";
690
+ import { MouseButton } from "@opentui/core";
691
+
692
+ // artwork.tsx
693
+ import { effect as _$effect } from "@opentui/solid";
694
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
695
+ import { insertNode as _$insertNode } from "@opentui/solid";
696
+ import { insert as _$insert } from "@opentui/solid";
697
+ import { createComponent as _$createComponent } from "@opentui/solid";
698
+ import { setProp as _$setProp } from "@opentui/solid";
699
+ import { use as _$use } from "@opentui/solid";
700
+ import { createElement as _$createElement } from "@opentui/solid";
701
+ import { For, onCleanup, onMount } from "solid-js";
702
+
703
+ // artwork-placement.ts
704
+ var CLEARED_STATE = {
705
+ transmitted: 0,
706
+ placement: null
707
+ };
708
+ function placementKey(placement) {
709
+ return [
710
+ placement.imageId,
711
+ placement.x,
712
+ placement.y,
713
+ placement.width,
714
+ placement.height
715
+ ].join(":");
716
+ }
717
+ function samePlacement(a, b) {
718
+ return a !== null && placementKey(a) === placementKey(b);
719
+ }
720
+ function planNativeArtworkPlacement(input) {
721
+ const { state, imageId, x, y, width, height } = input;
722
+ const desired = { imageId, x, y, width, height };
723
+ if (input.disposed || !input.kittySupported || !input.slotValid) {
724
+ if (state.transmitted !== 0) {
725
+ return {
726
+ actions: [{ type: "delete-image", imageId: state.transmitted }],
727
+ nextState: CLEARED_STATE
728
+ };
729
+ }
730
+ return { actions: [], nextState: state };
731
+ }
732
+ if (state.transmitted !== imageId) {
733
+ const actions2 = [];
734
+ if (state.transmitted !== 0) {
735
+ actions2.push({ type: "delete-image", imageId: state.transmitted });
736
+ } else {
737
+ actions2.push({ type: "delete-image", imageId });
738
+ }
739
+ actions2.push({
740
+ type: "transmit-and-display",
741
+ imageId,
742
+ x,
743
+ y,
744
+ width,
745
+ height
746
+ });
747
+ return {
748
+ actions: actions2,
749
+ nextState: { transmitted: imageId, placement: desired }
750
+ };
751
+ }
752
+ if (samePlacement(state.placement, desired)) {
753
+ return { actions: [], nextState: state };
754
+ }
755
+ const actions = [];
756
+ if (state.placement !== null || state.transmitted !== 0) {
757
+ actions.push({ type: "delete-placement", imageId });
758
+ }
759
+ actions.push({
760
+ type: "place",
761
+ imageId,
762
+ placementId: imageId,
763
+ x,
764
+ y,
765
+ width,
766
+ height
767
+ });
768
+ return {
769
+ actions,
770
+ nextState: { transmitted: imageId, placement: desired }
771
+ };
772
+ }
773
+
774
+ // kitty-graphics.ts
775
+ var APC = "\x1B_G";
776
+ var ST = "\x1B\\";
777
+ var CHUNK_SIZE = 4096;
778
+ function tmuxPassthrough(data) {
779
+ return `\x1BPtmux;${data.replaceAll("\x1B", "\x1B\x1B")}\x1B\\`;
780
+ }
781
+ function kittyTransmitPng(pngBase64, imageId) {
782
+ const chunks = [];
783
+ for (let offset = 0;offset < pngBase64.length; offset += CHUNK_SIZE) {
784
+ const chunk = pngBase64.slice(offset, offset + CHUNK_SIZE);
785
+ const more = offset + CHUNK_SIZE < pngBase64.length ? 1 : 0;
786
+ const control = offset === 0 ? `a=t,f=100,i=${imageId},q=2,m=${more}` : `m=${more}`;
787
+ chunks.push(`${APC}${control};${chunk}${ST}`);
788
+ }
789
+ return chunks;
790
+ }
791
+ function kittyDisplayPng(pngBase64, imageId, x, y, width, height) {
792
+ const chunks = kittyTransmitPng(pngBase64, imageId);
793
+ return chunks.map((command, index) => {
794
+ let next = command;
795
+ if (index === 0) {
796
+ next = next.replace(`${APC}a=t,f=100,i=${imageId},q=2,`, `\x1B7\x1B[${y + 1};${x + 1}H${APC}a=T,f=100,i=${imageId},p=${imageId},q=2,C=1,c=${width},r=${height},z=1,`);
797
+ }
798
+ return index === chunks.length - 1 ? `${next}\x1B8` : next;
799
+ });
800
+ }
801
+ function kittyPlace(imageId, placementId, x, y, width, height) {
802
+ return `\x1B7\x1B[${y + 1};${x + 1}H${APC}a=p,i=${imageId},p=${placementId},q=2,c=${width},r=${height},z=1;${ST}\x1B8`;
803
+ }
804
+ function kittyDelete(imageId) {
805
+ return `${APC}a=d,d=I,i=${imageId},q=2;${ST}`;
806
+ }
807
+ function kittyDeletePlacement(imageId) {
808
+ return `${APC}a=d,d=i,i=${imageId},q=2;${ST}`;
809
+ }
810
+ function kittyImageId(key) {
811
+ let hash = 2166136261;
812
+ for (let i = 0;i < key.length; i++) {
813
+ hash ^= key.charCodeAt(i);
814
+ hash = Math.imul(hash, 16777619);
815
+ }
816
+ return hash >>> 0 || 1;
817
+ }
818
+ function writeGraphics(renderer, data) {
819
+ const target = renderer;
820
+ if (!target.stdout || typeof target.realStdoutWrite !== "function")
821
+ return false;
822
+ const output = (value) => process.env.TMUX && !process.env.HERDR_ENV ? tmuxPassthrough(value) : value;
823
+ let cursorSaved = false;
824
+ try {
825
+ for (const command of typeof data === "string" ? [data] : data) {
826
+ if (command.includes("\x1B7"))
827
+ cursorSaved = true;
828
+ target.realStdoutWrite.call(target.stdout, output(command));
829
+ if (command.includes("\x1B8"))
830
+ cursorSaved = false;
831
+ }
832
+ return true;
833
+ } catch {
834
+ if (cursorSaved) {
835
+ try {
836
+ target.realStdoutWrite.call(target.stdout, output("\x1B8"));
837
+ } catch {}
838
+ }
839
+ return false;
840
+ }
841
+ }
842
+
843
+ // tmux-offset.ts
844
+ var ZERO = { x: 0, y: 0 };
845
+ function createTmuxOffsetCache() {
846
+ return { offset: null, slot: null };
847
+ }
848
+ function shouldUseTmuxOffset(env = process.env) {
849
+ return Boolean(env.TMUX) && !env.HERDR_ENV;
850
+ }
851
+ function slotKey(slot) {
852
+ return [slot.screenX, slot.screenY, slot.width, slot.height].join(":");
853
+ }
854
+ function sameSlotGeometry(a, b) {
855
+ if (a === null || b === null)
856
+ return false;
857
+ return slotKey(a) === slotKey(b);
858
+ }
859
+ function parseTmuxOffsetMessage(stdout) {
860
+ const [left, top, statusPosition, status] = stdout.trim().split("\t");
861
+ return {
862
+ x: Number.parseInt(left ?? "0", 10) || 0,
863
+ y: (Number.parseInt(top ?? "0", 10) || 0) + (statusPosition === "top" && status !== "off" ? 1 : 0)
864
+ };
865
+ }
866
+ function queryTmuxOffset() {
867
+ const result = Bun.spawnSync([
868
+ "tmux",
869
+ "display-message",
870
+ "-p",
871
+ "#{pane_left}\t#{pane_top}\t#{status-position}\t#{status}"
872
+ ]);
873
+ return {
874
+ ok: result.exitCode === 0,
875
+ stdout: result.stdout.toString()
876
+ };
877
+ }
878
+ function resolveTerminalOffset(options) {
879
+ const env = options.env ?? process.env;
880
+ if (!shouldUseTmuxOffset(env)) {
881
+ options.cache.offset = null;
882
+ options.cache.slot = null;
883
+ return ZERO;
884
+ }
885
+ if (options.cache.offset !== null && options.slot !== null && sameSlotGeometry(options.cache.slot, options.slot)) {
886
+ return options.cache.offset;
887
+ }
888
+ const query = options.query ?? queryTmuxOffset;
889
+ const result = query();
890
+ if (!result.ok) {
891
+ options.cache.offset = null;
892
+ options.cache.slot = null;
893
+ return ZERO;
894
+ }
895
+ const offset = parseTmuxOffsetMessage(result.stdout);
896
+ options.cache.offset = offset;
897
+ options.cache.slot = options.slot ? { ...options.slot } : null;
898
+ return offset;
899
+ }
900
+
901
+ // artwork.tsx
902
+ var tmuxOffsetCache = createTmuxOffsetCache();
903
+ var nativeArtworkImageId = kittyImageId("opencode-music-player:artwork");
904
+ var defaultOwnershipScope = {
905
+ latestOwner: 0
906
+ };
907
+ var nativeArtworkRuntimes = new WeakMap;
908
+ function nativeArtworkRuntime(renderer) {
909
+ const existing = nativeArtworkRuntimes.get(renderer);
910
+ if (existing)
911
+ return existing;
912
+ const created = {
913
+ latestOwner: 0,
914
+ state: {
915
+ transmitted: 0,
916
+ placement: null
917
+ },
918
+ identity: ""
919
+ };
920
+ nativeArtworkRuntimes.set(renderer, created);
921
+ return created;
922
+ }
923
+ function claimNativeArtworkOwnership(scope = defaultOwnershipScope) {
924
+ const owner = ++scope.latestOwner;
925
+ return {
926
+ isCurrent: () => owner === scope.latestOwner
927
+ };
928
+ }
929
+ function cleanupNativeArtwork(ownership, remove) {
930
+ if (ownership.isCurrent())
931
+ remove();
932
+ }
933
+ function imageIdForArtwork(_artworkId) {
934
+ return nativeArtworkImageId;
935
+ }
936
+ function legacyImageIdForArtwork(artworkId) {
937
+ return kittyImageId(artworkId);
938
+ }
939
+ function legacyImageIdForResolvedArtwork(artwork) {
940
+ return legacyImageIdForArtwork(artwork.legacy_id ?? artwork.id);
941
+ }
942
+ function terminalOffset(slot) {
943
+ return resolveTerminalOffset({
944
+ slot,
945
+ cache: tmuxOffsetCache
946
+ });
947
+ }
948
+ function supportsKittyGraphics(context) {
949
+ return !!context.renderer.capabilities?.kitty_graphics || process.env.TERM_PROGRAM?.toLowerCase() === "ghostty";
950
+ }
951
+ function copyState(next) {
952
+ return {
953
+ transmitted: next.transmitted,
954
+ placement: next.placement ? {
955
+ ...next.placement
956
+ } : null
957
+ };
958
+ }
959
+ function AlbumArtwork(props) {
960
+ const renderer = props.context.renderer;
961
+ const runtime = nativeArtworkRuntime(renderer);
962
+ const ownership = claimNativeArtworkOwnership(runtime);
963
+ let container;
964
+ let paintPending = false;
965
+ let disposed = false;
966
+ const applyAction = (action) => {
967
+ const renderer2 = props.context.renderer;
968
+ switch (action.type) {
969
+ case "delete-image":
970
+ return writeGraphics(renderer2, kittyDelete(action.imageId));
971
+ case "delete-placement":
972
+ return writeGraphics(renderer2, kittyDeletePlacement(action.imageId));
973
+ case "transmit-and-display": {
974
+ const commands = kittyDisplayPng(props.artwork.png_base64, action.imageId, action.x, action.y, action.width, action.height);
975
+ return writeGraphics(renderer2, commands);
976
+ }
977
+ case "place":
978
+ return writeGraphics(renderer2, kittyPlace(action.imageId, action.placementId, action.x, action.y, action.width, action.height));
979
+ }
980
+ };
981
+ const applyPlan = (plan) => {
982
+ for (const action of plan.actions) {
983
+ if (!applyAction(action))
984
+ return false;
985
+ }
986
+ return true;
987
+ };
988
+ const commitPlan = (plan) => {
989
+ runtime.state = copyState(plan.nextState);
990
+ };
991
+ const paintNativeImage = () => {
992
+ if (!ownership.isCurrent())
993
+ return;
994
+ if (runtime.identity !== props.artwork.id) {
995
+ runtime.identity = props.artwork.id;
996
+ writeGraphics(props.context.renderer, kittyDelete(legacyImageIdForResolvedArtwork(props.artwork)));
997
+ runtime.state = {
998
+ transmitted: 0,
999
+ placement: null
1000
+ };
1001
+ }
1002
+ const kittySupported = supportsKittyGraphics(props.context);
1003
+ const slotValid = !!container && !container.isDestroyed && container.width >= 1 && container.height >= 1;
1004
+ const slot = container ? {
1005
+ screenX: container.screenX,
1006
+ screenY: container.screenY,
1007
+ width: container.width,
1008
+ height: container.height
1009
+ } : null;
1010
+ const offset = terminalOffset(slot);
1011
+ const imageId = imageIdForArtwork(props.artwork.id);
1012
+ const x = slot ? slot.screenX + offset.x : 0;
1013
+ const y = slot ? slot.screenY + offset.y : 0;
1014
+ const width = slot?.width ?? 0;
1015
+ const height = slot?.height ?? 0;
1016
+ const plan = planNativeArtworkPlacement({
1017
+ state: runtime.state,
1018
+ imageId,
1019
+ x,
1020
+ y,
1021
+ width,
1022
+ height,
1023
+ kittySupported,
1024
+ slotValid,
1025
+ disposed
1026
+ });
1027
+ if (plan.actions.length === 0)
1028
+ return;
1029
+ if (applyPlan(plan))
1030
+ commitPlan(plan);
1031
+ };
1032
+ const scheduleNativeImage = () => {
1033
+ if (paintPending || disposed)
1034
+ return;
1035
+ paintPending = true;
1036
+ props.context.renderer.idle().then(() => {
1037
+ paintPending = false;
1038
+ if (!disposed && ownership.isCurrent())
1039
+ paintNativeImage();
1040
+ });
1041
+ };
1042
+ onMount(() => props.context.renderer.on("frame", scheduleNativeImage));
1043
+ onCleanup(() => {
1044
+ disposed = true;
1045
+ props.context.renderer.off("frame", scheduleNativeImage);
1046
+ setTimeout(() => {
1047
+ cleanupNativeArtwork(ownership, () => {
1048
+ if (!renderer.isDestroyed)
1049
+ writeGraphics(renderer, kittyDelete(nativeArtworkImageId));
1050
+ runtime.state = {
1051
+ transmitted: 0,
1052
+ placement: null
1053
+ };
1054
+ runtime.identity = "";
1055
+ });
1056
+ }, 0);
1057
+ });
1058
+ return (() => {
1059
+ var _el$ = _$createElement("box");
1060
+ _$use((value) => container = value, _el$);
1061
+ _$setProp(_el$, "width", 24);
1062
+ _$setProp(_el$, "height", 12);
1063
+ _$setProp(_el$, "flexDirection", "column");
1064
+ _$setProp(_el$, "overflow", "hidden");
1065
+ _$insert(_el$, _$createComponent(For, {
1066
+ get each() {
1067
+ return props.artwork.cells;
1068
+ },
1069
+ children: (row) => (() => {
1070
+ var _el$2 = _$createElement("text");
1071
+ _$insert(_el$2, _$createComponent(For, {
1072
+ each: row,
1073
+ children: (cell) => (() => {
1074
+ var _el$3 = _$createElement("span");
1075
+ _$insertNode(_el$3, _$createTextNode(`\u2580`));
1076
+ _$effect((_$p) => _$setProp(_el$3, "style", {
1077
+ fg: cell.upper,
1078
+ bg: cell.lower
1079
+ }, _$p));
1080
+ return _el$3;
1081
+ })()
1082
+ }));
1083
+ return _el$2;
1084
+ })()
1085
+ }));
1086
+ return _el$;
1087
+ })();
1088
+ }
1089
+
1090
+ // waveform.tsx
1091
+ import { insert as _$insert2 } from "@opentui/solid";
1092
+ import { use as _$use2 } from "@opentui/solid";
1093
+ import { createElement as _$createElement2 } from "@opentui/solid";
1094
+ import { createEngine, displayLevel, isFlat, sameTrackIdentity, stepEngine, waveformSeedKey } from "@naxodev/music-core";
1095
+ import { onCleanup as onCleanup2, onMount as onMount2 } from "solid-js";
1096
+ import { StyledText, fg } from "@opentui/core";
1097
+ function createWaveformCoordinator(options) {
1098
+ let player = null;
1099
+ let bars = 0;
1100
+ let requestedKey = "";
1101
+ let engineKey = "";
1102
+ let engine = null;
1103
+ let timer = null;
1104
+ const stop = () => {
1105
+ if (timer === null)
1106
+ return;
1107
+ options.scheduler.clearInterval(timer);
1108
+ timer = null;
1109
+ };
1110
+ const start = () => {
1111
+ if (timer !== null || !player?.track)
1112
+ return;
1113
+ timer = options.scheduler.setInterval(() => frame(), options.intervalMs);
1114
+ };
1115
+ const frame = (seek = false) => {
1116
+ const track = player?.track;
1117
+ if (!player || !track) {
1118
+ stop();
1119
+ return null;
1120
+ }
1121
+ const key = engineKey || requestedKey;
1122
+ if (!engine || engine.n !== bars || engineKey !== key) {
1123
+ engine = createEngine(bars, key);
1124
+ engineKey = key;
1125
+ }
1126
+ stepEngine(engine, {
1127
+ track_key: key,
1128
+ bars,
1129
+ progress_ms: player.progress_ms,
1130
+ fetched_at: player.fetched_at,
1131
+ is_playing: player.is_playing,
1132
+ duration_ms: track.duration_ms,
1133
+ now_ms: options.now(),
1134
+ seek
1135
+ });
1136
+ options.render(player, engine);
1137
+ if (player.is_playing || !isFlat(engine))
1138
+ start();
1139
+ else
1140
+ stop();
1141
+ return engine;
1142
+ };
1143
+ return {
1144
+ setInput: (next, trackIdentity, nextBars) => {
1145
+ if (!next?.track) {
1146
+ player = null;
1147
+ engine = null;
1148
+ engineKey = "";
1149
+ requestedKey = "";
1150
+ bars = nextBars;
1151
+ stop();
1152
+ options.clear();
1153
+ return;
1154
+ }
1155
+ if (bars !== nextBars || player?.track && !sameTrackIdentity(player.track, next.track)) {
1156
+ engine = null;
1157
+ engineKey = "";
1158
+ }
1159
+ player = next;
1160
+ requestedKey = trackIdentity;
1161
+ bars = nextBars;
1162
+ },
1163
+ frame,
1164
+ start,
1165
+ stop,
1166
+ dispose: () => {
1167
+ stop();
1168
+ player = null;
1169
+ engine = null;
1170
+ engineKey = "";
1171
+ requestedKey = "";
1172
+ }
1173
+ };
1174
+ }
1175
+ var BLOCK = [" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
1176
+ var BLUE = [
1177
+ "#1a1b26",
1178
+ "#24283b",
1179
+ "#3d59a1",
1180
+ "#414868",
1181
+ "#565f89",
1182
+ "#7aa2f7",
1183
+ "#89b4fa",
1184
+ "#b4c0f7",
1185
+ "#c0caf5"
1186
+ ];
1187
+ function blueFor(level, playing, theme) {
1188
+ if (!playing && level < 0.05)
1189
+ return theme.text.subdued;
1190
+ if (level <= 0.02)
1191
+ return BLUE[1];
1192
+ const idx = Math.min(BLUE.length - 1, Math.floor(level * (BLUE.length - 1)));
1193
+ return BLUE[Math.max(2, idx)];
1194
+ }
1195
+ function blockChar(level) {
1196
+ return BLOCK[Math.max(0, Math.min(8, Math.round(level * 8)))];
1197
+ }
1198
+ function Waveform(props) {
1199
+ const barCount = () => props.bars ?? (props.variant === "hero" ? 48 : 16);
1200
+ let text;
1201
+ const paint = (player, engine) => {
1202
+ if (!text)
1203
+ return;
1204
+ const playing = player.is_playing;
1205
+ text.content = new StyledText(Array.from({
1206
+ length: barCount()
1207
+ }, (_, index) => {
1208
+ const level = displayLevel(engine.levels[index] ?? 0, index, playing);
1209
+ return fg(blueFor(level, playing || level > 0.05, props.theme))(blockChar(level));
1210
+ }));
1211
+ };
1212
+ const coordinator = createWaveformCoordinator({
1213
+ now: () => Date.now(),
1214
+ scheduler: {
1215
+ setInterval,
1216
+ clearInterval: (timer) => clearInterval(timer)
1217
+ },
1218
+ intervalMs: props.variant === "hero" ? 48 : 64,
1219
+ render: paint,
1220
+ clear: () => {
1221
+ if (text)
1222
+ text.content = " ".repeat(barCount());
1223
+ }
1224
+ });
1225
+ const update = (player) => {
1226
+ coordinator.setInput(player, player?.track ? waveformSeedKey(player.track.name, player.track.id) : "", barCount());
1227
+ coordinator.frame();
1228
+ };
1229
+ const unsubscribe = props.source.subscribe(update);
1230
+ onMount2(() => coordinator.frame());
1231
+ onCleanup2(() => {
1232
+ unsubscribe();
1233
+ coordinator.dispose();
1234
+ });
1235
+ return (() => {
1236
+ var _el$ = _$createElement2("text");
1237
+ _$use2((element) => text = element, _el$);
1238
+ _$insert2(_el$, () => " ".repeat(barCount()));
1239
+ return _el$;
1240
+ })();
1241
+ }
1242
+
1243
+ // ui.tsx
1244
+ var t = (s) => s + "\uFE0E";
1245
+ var Icon = {
1246
+ prev: t("\u23EE"),
1247
+ next: t("\u23ED"),
1248
+ play: t("\u25B6"),
1249
+ pause: t("\u23F8"),
1250
+ scrub: "\u25CF"
1251
+ };
1252
+ var TRANSPORT_LAYOUT = {
1253
+ prevWidth: 6,
1254
+ playWidth: 10,
1255
+ nextWidth: 6,
1256
+ height: 2,
1257
+ gap: 1
1258
+ };
1259
+ var COMPACT_MARKER_WIDTH = 1;
1260
+ var COMPACT_TITLE_SEPARATOR = " ";
1261
+ var COMPACT_SEPARATOR = " - ";
1262
+ var COMPACT_SEEK_FRACTION = 0.8;
1263
+ function compactSeekRegionWidth(width) {
1264
+ if (!Number.isFinite(width) || width <= 0)
1265
+ return 0;
1266
+ const available = Math.floor(width);
1267
+ return Math.max(0, Math.min(available, Math.round(available * COMPACT_SEEK_FRACTION)));
1268
+ }
1269
+ function seekPositionForCell(cell, width, durationMs) {
1270
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(durationMs) || durationMs <= 0) {
1271
+ return null;
1272
+ }
1273
+ const lastCell = Math.max(0, Math.floor(width) - 1);
1274
+ const clampedCell = Math.max(0, Math.min(lastCell, Math.floor(cell)));
1275
+ const ratio = lastCell === 0 ? 0 : clampedCell / lastCell;
1276
+ return Math.round(ratio * durationMs);
1277
+ }
1278
+ var COMPACT_BUDGETS = {
1279
+ wide: {
1280
+ minWidth: 55,
1281
+ padding: 1,
1282
+ titleWidth: 28,
1283
+ artistWidth: 20
1284
+ },
1285
+ medium: {
1286
+ minWidth: 32,
1287
+ padding: 1,
1288
+ titleWidth: 28,
1289
+ artistWidth: 0
1290
+ },
1291
+ narrow: {
1292
+ minWidth: 6,
1293
+ padding: 1,
1294
+ titleWidth: 2,
1295
+ artistWidth: 0
1296
+ },
1297
+ markerOnly: {
1298
+ minWidth: 1,
1299
+ padding: 0,
1300
+ titleWidth: 0,
1301
+ artistWidth: 0
1302
+ }
1303
+ };
1304
+ function sanitizeTerminalText(value, fallback = "") {
1305
+ const clean = value.normalize("NFC").replace(/(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c|$)/g, "").replace(/(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ").replace(/\s+/g, " ").trim();
1306
+ return clean || fallback;
1307
+ }
1308
+ function truncate(value, width) {
1309
+ if (Bun.stringWidth(value) <= width)
1310
+ return value;
1311
+ if (width <= 1)
1312
+ return "\u2026";
1313
+ let result = "";
1314
+ for (const character of value) {
1315
+ if (Bun.stringWidth(result + character) > width - 1)
1316
+ break;
1317
+ result += character;
1318
+ }
1319
+ return `${result}\u2026`;
1320
+ }
1321
+ function compactPresentation(width, title, artist, playing) {
1322
+ const available = Math.max(1, Math.floor(width));
1323
+ const marker = playing ? Icon.pause : Icon.play;
1324
+ const cleanTitle = sanitizeTerminalText(title, "Unknown track");
1325
+ const cleanArtist = sanitizeTerminalText(artist);
1326
+ const tier = available >= COMPACT_BUDGETS.wide.minWidth ? "wide" : available >= COMPACT_BUDGETS.medium.minWidth ? "medium" : available >= COMPACT_BUDGETS.narrow.minWidth ? "narrow" : "markerOnly";
1327
+ const budget = COMPACT_BUDGETS[tier];
1328
+ if (tier === "markerOnly") {
1329
+ return {
1330
+ tier,
1331
+ marker,
1332
+ padding: budget.padding,
1333
+ title: null,
1334
+ artist: null
1335
+ };
1336
+ }
1337
+ const fixedWidth = COMPACT_MARKER_WIDTH + COMPACT_TITLE_SEPARATOR.length + budget.padding * 2;
1338
+ const titleWidth = tier === "narrow" ? available - fixedWidth : budget.titleWidth;
1339
+ return {
1340
+ tier,
1341
+ marker,
1342
+ padding: budget.padding,
1343
+ title: truncate(cleanTitle, titleWidth),
1344
+ artist: tier === "wide" && cleanArtist ? truncate(cleanArtist, budget.artistWidth) : null
1345
+ };
1346
+ }
1347
+ function CompactPlayer(props) {
1348
+ const theme = () => props.context.theme;
1349
+ let row;
1350
+ let content;
1351
+ let allocatedWidth = Number.MAX_SAFE_INTEGER;
1352
+ const track = createMemo(() => props.state.player?.track);
1353
+ const renderLine = () => {
1354
+ const current = track();
1355
+ if (!current)
1356
+ return "";
1357
+ const display = compactPresentation(allocatedWidth, current.name, current.artists, !!props.state.player?.is_playing);
1358
+ const padding = " ".repeat(display.padding);
1359
+ return `${padding}${display.marker}${display.title ? `${COMPACT_TITLE_SEPARATOR}${display.title}` : ""}${display.artist ? `${COMPACT_SEPARATOR}${display.artist}` : ""}${padding}`;
1360
+ };
1361
+ const updateContent = () => {
1362
+ const line = renderLine();
1363
+ if (content)
1364
+ content.content = line;
1365
+ };
1366
+ const updateWidth = function() {
1367
+ allocatedWidth = Math.max(1, this.width);
1368
+ updateContent();
1369
+ };
1370
+ createEffect(updateContent);
1371
+ onCleanup3(() => {
1372
+ if (row?.onSizeChange === updateWidth)
1373
+ row.onSizeChange = undefined;
1374
+ });
1375
+ return _$createComponent2(Show, {
1376
+ get when() {
1377
+ return track();
1378
+ },
1379
+ children: (_track) => (() => {
1380
+ var _el$ = _$createElement3("box"), _el$2 = _$createElement3("text");
1381
+ _$insertNode2(_el$, _el$2);
1382
+ _$use3((element) => {
1383
+ row = element;
1384
+ element.onSizeChange = updateWidth;
1385
+ }, _el$);
1386
+ _$setProp2(_el$, "id", "music-compact-player");
1387
+ _$setProp2(_el$, "width", "100%");
1388
+ _$setProp2(_el$, "alignSelf", "stretch");
1389
+ _$setProp2(_el$, "minWidth", 0);
1390
+ _$setProp2(_el$, "height", 1);
1391
+ _$setProp2(_el$, "flexDirection", "row");
1392
+ _$setProp2(_el$, "flexShrink", 0);
1393
+ _$setProp2(_el$, "overflow", "hidden");
1394
+ _$setProp2(_el$, "onMouseDown", (event) => {
1395
+ if (event.button !== MouseButton.LEFT)
1396
+ return;
1397
+ const current = props.state.player?.track;
1398
+ if (!current || !row)
1399
+ return;
1400
+ const seekWidth = compactSeekRegionWidth(row.width);
1401
+ const cell = Math.floor(event.x - row.x);
1402
+ const markerCell = compactPresentation(row.width, current.name, current.artists, !!props.state.player?.is_playing).padding;
1403
+ if (cell === markerCell) {
1404
+ event.preventDefault();
1405
+ event.stopPropagation();
1406
+ props.onPlayPause();
1407
+ return;
1408
+ }
1409
+ if (cell < 0 || cell >= seekWidth)
1410
+ return;
1411
+ const position = seekPositionForCell(cell, seekWidth, current.duration_ms);
1412
+ if (position === null)
1413
+ return;
1414
+ event.preventDefault();
1415
+ event.stopPropagation();
1416
+ props.onSeek(position);
1417
+ });
1418
+ _$use3((element) => {
1419
+ content = element;
1420
+ updateContent();
1421
+ }, _el$2);
1422
+ _$setProp2(_el$2, "wrapMode", "none");
1423
+ _$setProp2(_el$2, "truncate", true);
1424
+ _$insert3(_el$2, renderLine);
1425
+ _$effect2((_$p) => _$setProp2(_el$2, "fg", theme().text.action.primary.default, _$p));
1426
+ return _el$;
1427
+ })()
1428
+ });
1429
+ }
1430
+ function liveProgress(player, now = Date.now()) {
1431
+ if (!player?.track)
1432
+ return 0;
1433
+ if (!player.is_playing)
1434
+ return player.progress_ms;
1435
+ return Math.min(player.track.duration_ms, player.progress_ms + (Math.max(now, player.fetched_at) - player.fetched_at));
1436
+ }
1437
+ function progressSegments(progress, duration, width) {
1438
+ if (duration <= 0)
1439
+ return {
1440
+ left: "",
1441
+ thumb: "",
1442
+ right: "\u2500".repeat(width)
1443
+ };
1444
+ const ratio = Math.max(0, Math.min(1, progress / duration));
1445
+ const track = width - 1;
1446
+ const filled = Math.max(0, Math.min(track, Math.round(ratio * track)));
1447
+ return {
1448
+ left: "\u2501".repeat(filled),
1449
+ thumb: Icon.scrub,
1450
+ right: "\u2500".repeat(Math.max(0, track - filled))
1451
+ };
1452
+ }
1453
+ function ProgressBar(props) {
1454
+ let bar;
1455
+ let leftNode;
1456
+ let thumbNode;
1457
+ let rightNode;
1458
+ const width = () => Math.max(8, props.width ?? 28);
1459
+ const rendered = () => {
1460
+ const player = props.source.current();
1461
+ return progressSegments(liveProgress(player), player?.track?.duration_ms ?? 0, width());
1462
+ };
1463
+ const paint = () => {
1464
+ const next = rendered();
1465
+ if (leftNode)
1466
+ leftNode.children = [next.left];
1467
+ if (thumbNode)
1468
+ thumbNode.children = [next.thumb];
1469
+ if (rightNode)
1470
+ rightNode.children = [next.right];
1471
+ };
1472
+ onMount3(() => {
1473
+ const unsubscribe = props.source.subscribe(() => paint());
1474
+ const timer = setInterval(() => {
1475
+ if (props.source.current()?.is_playing)
1476
+ paint();
1477
+ }, 1000);
1478
+ onCleanup3(() => {
1479
+ unsubscribe();
1480
+ clearInterval(timer);
1481
+ });
1482
+ });
1483
+ return (() => {
1484
+ var _el$3 = _$createElement3("box"), _el$4 = _$createElement3("text"), _el$5 = _$createElement3("span"), _el$6 = _$createElement3("span"), _el$7 = _$createElement3("span");
1485
+ _$insertNode2(_el$3, _el$4);
1486
+ _$use3((element) => bar = element, _el$3);
1487
+ _$setProp2(_el$3, "id", "music-sidebar-seek");
1488
+ _$setProp2(_el$3, "height", 1);
1489
+ _$setProp2(_el$3, "onMouseDown", (event) => {
1490
+ if (event.button !== MouseButton.LEFT || !bar)
1491
+ return;
1492
+ const position = seekPositionForCell(event.x - bar.x, bar.width, props.source.current()?.track?.duration_ms ?? 0);
1493
+ if (position === null)
1494
+ return;
1495
+ event.preventDefault();
1496
+ event.stopPropagation();
1497
+ props.onSeek(position);
1498
+ });
1499
+ _$insertNode2(_el$4, _el$5);
1500
+ _$insertNode2(_el$4, _el$6);
1501
+ _$insertNode2(_el$4, _el$7);
1502
+ _$use3((element) => leftNode = element, _el$5);
1503
+ _$insert3(_el$5, () => rendered().left);
1504
+ _$use3((element) => thumbNode = element, _el$6);
1505
+ _$insert3(_el$6, () => rendered().thumb);
1506
+ _$use3((element) => rightNode = element, _el$7);
1507
+ _$insert3(_el$7, () => rendered().right);
1508
+ _$effect2((_p$) => {
1509
+ var _v$ = width(), _v$2 = {
1510
+ fg: props.accent ?? props.theme.text.action.primary.default
1511
+ }, _v$3 = {
1512
+ fg: props.accent ?? props.theme.text.action.primary.default
1513
+ }, _v$4 = {
1514
+ fg: props.theme.text.subdued
1515
+ };
1516
+ _v$ !== _p$.e && (_p$.e = _$setProp2(_el$3, "width", _v$, _p$.e));
1517
+ _v$2 !== _p$.t && (_p$.t = _$setProp2(_el$5, "style", _v$2, _p$.t));
1518
+ _v$3 !== _p$.a && (_p$.a = _$setProp2(_el$6, "style", _v$3, _p$.a));
1519
+ _v$4 !== _p$.o && (_p$.o = _$setProp2(_el$7, "style", _v$4, _p$.o));
1520
+ return _p$;
1521
+ }, {
1522
+ e: undefined,
1523
+ t: undefined,
1524
+ a: undefined,
1525
+ o: undefined
1526
+ });
1527
+ return _el$3;
1528
+ })();
1529
+ }
1530
+ function LiveElapsed(props) {
1531
+ let text;
1532
+ const paint = () => {
1533
+ if (text)
1534
+ text.content = formatMs(liveProgress(props.source.current()));
1535
+ };
1536
+ onMount3(() => {
1537
+ const unsubscribe = props.source.subscribe(() => paint());
1538
+ const timer = setInterval(() => {
1539
+ if (props.source.current()?.is_playing)
1540
+ paint();
1541
+ }, 1000);
1542
+ onCleanup3(() => {
1543
+ unsubscribe();
1544
+ clearInterval(timer);
1545
+ });
1546
+ });
1547
+ return (() => {
1548
+ var _el$8 = _$createElement3("text");
1549
+ _$use3((element) => text = element, _el$8);
1550
+ _$insert3(_el$8, () => formatMs(liveProgress(props.source.current())));
1551
+ _$effect2((_$p) => _$setProp2(_el$8, "fg", props.theme.text.subdued, _$p));
1552
+ return _el$8;
1553
+ })();
1554
+ }
1555
+ function IconBtn(props) {
1556
+ const [hover, setHover] = createSignal(false);
1557
+ const [press, setPress] = createSignal(false);
1558
+ const bg = () => {
1559
+ if (press() || props.active) {
1560
+ return props.theme.background.action.primary.default;
1561
+ }
1562
+ return props.theme.background.surface.offset;
1563
+ };
1564
+ const fg2 = () => {
1565
+ if (press() || props.active)
1566
+ return props.accent ?? props.theme.text.action.primary.default;
1567
+ if (props.primary) {
1568
+ return hover() ? props.theme.text.action.primary.default : props.theme.text.default;
1569
+ }
1570
+ return hover() ? props.theme.text.action.primary.default : props.theme.text.default;
1571
+ };
1572
+ const w = () => props.width ?? 3;
1573
+ const h = () => props.height ?? 1;
1574
+ return (() => {
1575
+ var _el$9 = _$createElement3("box"), _el$0 = _$createElement3("text");
1576
+ _$insertNode2(_el$9, _el$0);
1577
+ _$setProp2(_el$9, "onMouseDown", () => {
1578
+ setPress(true);
1579
+ props.onClick();
1580
+ });
1581
+ _$setProp2(_el$9, "onMouseUp", () => setPress(false));
1582
+ _$setProp2(_el$9, "onMouseOver", () => setHover(true));
1583
+ _$setProp2(_el$9, "onMouseOut", () => {
1584
+ setHover(false);
1585
+ setPress(false);
1586
+ });
1587
+ _$setProp2(_el$9, "flexShrink", 0);
1588
+ _$setProp2(_el$9, "alignItems", "center");
1589
+ _$setProp2(_el$9, "justifyContent", "center");
1590
+ _$insert3(_el$0, () => props.icon);
1591
+ _$effect2((_p$) => {
1592
+ var _v$5 = w(), _v$6 = h(), _v$7 = bg(), _v$8 = fg2();
1593
+ _v$5 !== _p$.e && (_p$.e = _$setProp2(_el$9, "width", _v$5, _p$.e));
1594
+ _v$6 !== _p$.t && (_p$.t = _$setProp2(_el$9, "height", _v$6, _p$.t));
1595
+ _v$7 !== _p$.a && (_p$.a = _$setProp2(_el$9, "backgroundColor", _v$7, _p$.a));
1596
+ _v$8 !== _p$.o && (_p$.o = _$setProp2(_el$0, "fg", _v$8, _p$.o));
1597
+ return _p$;
1598
+ }, {
1599
+ e: undefined,
1600
+ t: undefined,
1601
+ a: undefined,
1602
+ o: undefined
1603
+ });
1604
+ return _el$9;
1605
+ })();
1606
+ }
1607
+ function SidebarPlayer(props) {
1608
+ const theme = () => props.context.theme;
1609
+ let latestPlayer = props.state.player;
1610
+ const presentationListeners = new Set;
1611
+ const publishPlayer = (player2) => {
1612
+ latestPlayer = player2;
1613
+ for (const listener of [...presentationListeners]) {
1614
+ try {
1615
+ listener(player2);
1616
+ } catch {}
1617
+ }
1618
+ };
1619
+ const source = {
1620
+ current: () => latestPlayer,
1621
+ subscribe(listener) {
1622
+ presentationListeners.add(listener);
1623
+ try {
1624
+ listener(latestPlayer);
1625
+ } catch {}
1626
+ return () => presentationListeners.delete(listener);
1627
+ }
1628
+ };
1629
+ createEffect(() => publishPlayer(props.state.player));
1630
+ const unsubscribeSession = props.subscribe?.((state) => publishPlayer(state.player));
1631
+ onCleanup3(() => {
1632
+ unsubscribeSession?.();
1633
+ presentationListeners.clear();
1634
+ });
1635
+ const player = createMemo(() => props.state.player);
1636
+ const track = createMemo(() => player()?.track);
1637
+ const playing = createMemo(() => !!player()?.is_playing);
1638
+ const duration = createMemo(() => track()?.duration_ms ?? 0);
1639
+ return (() => {
1640
+ var _el$1 = _$createElement3("box"), _el$10 = _$createElement3("box"), _el$11 = _$createElement3("box"), _el$12 = _$createElement3("box"), _el$16 = _$createElement3("box");
1641
+ _$insertNode2(_el$1, _el$10);
1642
+ _$insertNode2(_el$1, _el$12);
1643
+ _$insertNode2(_el$1, _el$16);
1644
+ _$setProp2(_el$1, "flexDirection", "column");
1645
+ _$setProp2(_el$1, "border", ["top"]);
1646
+ _$setProp2(_el$1, "paddingLeft", 1);
1647
+ _$setProp2(_el$1, "paddingRight", 1);
1648
+ _$setProp2(_el$1, "paddingTop", 1);
1649
+ _$setProp2(_el$1, "paddingBottom", 1);
1650
+ _$setProp2(_el$1, "gap", 1);
1651
+ _$setProp2(_el$1, "flexShrink", 0);
1652
+ _$insert3(_el$1, _$createComponent2(Show, {
1653
+ get when() {
1654
+ return props.state.error;
1655
+ },
1656
+ children: (err) => (() => {
1657
+ var _el$17 = _$createElement3("text");
1658
+ _$insert3(_el$17, () => sanitizeTerminalText(err()));
1659
+ _$effect2((_$p) => _$setProp2(_el$17, "fg", theme().text.feedback.error.default, _$p));
1660
+ return _el$17;
1661
+ })()
1662
+ }), _el$10);
1663
+ _$insertNode2(_el$10, _el$11);
1664
+ _$setProp2(_el$10, "flexDirection", "row");
1665
+ _$setProp2(_el$10, "justifyContent", "center");
1666
+ _$setProp2(_el$11, "width", 24);
1667
+ _$setProp2(_el$11, "height", 12);
1668
+ _$setProp2(_el$11, "overflow", "hidden");
1669
+ _$insert3(_el$11, _$createComponent2(Show, {
1670
+ get when() {
1671
+ return track()?.artwork;
1672
+ },
1673
+ get fallback() {
1674
+ return (() => {
1675
+ var _el$18 = _$createElement3("box"), _el$19 = _$createElement3("text");
1676
+ _$insertNode2(_el$18, _el$19);
1677
+ _$setProp2(_el$18, "width", 24);
1678
+ _$setProp2(_el$18, "height", 12);
1679
+ _$setProp2(_el$18, "alignItems", "center");
1680
+ _$setProp2(_el$18, "justifyContent", "center");
1681
+ _$insert3(_el$19, (() => {
1682
+ var _c$ = _$memo(() => !!track());
1683
+ return () => _c$() ? track()?.artwork_loading ? "Loading artwork\u2026" : "Artwork unavailable" : "";
1684
+ })());
1685
+ _$effect2((_$p) => _$setProp2(_el$19, "fg", theme().text.subdued, _$p));
1686
+ return _el$18;
1687
+ })();
1688
+ },
1689
+ children: (artwork) => _$createComponent2(AlbumArtwork, {
1690
+ get context() {
1691
+ return props.context;
1692
+ },
1693
+ get artwork() {
1694
+ return artwork();
1695
+ }
1696
+ })
1697
+ }));
1698
+ _$insert3(_el$1, _$createComponent2(Show, {
1699
+ get when() {
1700
+ return track();
1701
+ },
1702
+ get fallback() {
1703
+ return (() => {
1704
+ var _el$20 = _$createElement3("text");
1705
+ _$insert3(_el$20, () => props.state.loading ? "Syncing\u2026" : "Nothing playing");
1706
+ _$effect2((_$p) => _$setProp2(_el$20, "fg", theme().text.subdued, _$p));
1707
+ return _el$20;
1708
+ })();
1709
+ },
1710
+ children: (t2) => (() => {
1711
+ var _el$21 = _$createElement3("box"), _el$22 = _$createElement3("text"), _el$23 = _$createElement3("b");
1712
+ _$insertNode2(_el$21, _el$22);
1713
+ _$setProp2(_el$21, "flexDirection", "column");
1714
+ _$setProp2(_el$21, "gap", 0);
1715
+ _$insertNode2(_el$22, _el$23);
1716
+ _$insert3(_el$23, () => sanitizeTerminalText(t2().name, "Unknown track"));
1717
+ _$insert3(_el$21, _$createComponent2(Show, {
1718
+ get when() {
1719
+ return t2().artists;
1720
+ },
1721
+ get children() {
1722
+ var _el$24 = _$createElement3("text");
1723
+ _$insert3(_el$24, () => sanitizeTerminalText(t2().artists));
1724
+ _$effect2((_$p) => _$setProp2(_el$24, "fg", theme().text.subdued, _$p));
1725
+ return _el$24;
1726
+ }
1727
+ }), null);
1728
+ _$insert3(_el$21, _$createComponent2(Show, {
1729
+ get when() {
1730
+ return t2().album;
1731
+ },
1732
+ get children() {
1733
+ var _el$25 = _$createElement3("text");
1734
+ _$insert3(_el$25, () => sanitizeTerminalText(t2().album));
1735
+ _$effect2((_$p) => _$setProp2(_el$25, "fg", theme().text.subdued, _$p));
1736
+ return _el$25;
1737
+ }
1738
+ }), null);
1739
+ _$effect2((_$p) => _$setProp2(_el$22, "fg", theme().text.default, _$p));
1740
+ return _el$21;
1741
+ })()
1742
+ }), _el$12);
1743
+ _$setProp2(_el$12, "flexDirection", "row");
1744
+ _$setProp2(_el$12, "justifyContent", "center");
1745
+ _$setProp2(_el$12, "overflow", "hidden");
1746
+ _$insert3(_el$12, _$createComponent2(Waveform, {
1747
+ get theme() {
1748
+ return theme();
1749
+ },
1750
+ source,
1751
+ bars: 24,
1752
+ variant: "hero"
1753
+ }));
1754
+ _$insert3(_el$1, _$createComponent2(Show, {
1755
+ get when() {
1756
+ return _$memo(() => !!track())() && duration() > 0;
1757
+ },
1758
+ get children() {
1759
+ var _el$13 = _$createElement3("box"), _el$14 = _$createElement3("box"), _el$15 = _$createElement3("text");
1760
+ _$insertNode2(_el$13, _el$14);
1761
+ _$setProp2(_el$13, "flexDirection", "column");
1762
+ _$setProp2(_el$13, "gap", 0);
1763
+ _$setProp2(_el$13, "overflow", "hidden");
1764
+ _$insert3(_el$13, _$createComponent2(ProgressBar, {
1765
+ get theme() {
1766
+ return theme();
1767
+ },
1768
+ source,
1769
+ width: 24,
1770
+ get accent() {
1771
+ return track()?.artwork?.accent;
1772
+ },
1773
+ get onSeek() {
1774
+ return props.onSeek;
1775
+ }
1776
+ }), _el$14);
1777
+ _$insertNode2(_el$14, _el$15);
1778
+ _$setProp2(_el$14, "flexDirection", "row");
1779
+ _$setProp2(_el$14, "justifyContent", "space-between");
1780
+ _$insert3(_el$14, _$createComponent2(LiveElapsed, {
1781
+ get theme() {
1782
+ return theme();
1783
+ },
1784
+ source
1785
+ }), _el$15);
1786
+ _$insert3(_el$15, () => formatMs(duration()));
1787
+ _$effect2((_$p) => _$setProp2(_el$15, "fg", theme().text.subdued, _$p));
1788
+ return _el$13;
1789
+ }
1790
+ }), _el$16);
1791
+ _$setProp2(_el$16, "flexDirection", "row");
1792
+ _$setProp2(_el$16, "justifyContent", "center");
1793
+ _$insert3(_el$16, _$createComponent2(IconBtn, {
1794
+ get theme() {
1795
+ return theme();
1796
+ },
1797
+ get icon() {
1798
+ return Icon.prev;
1799
+ },
1800
+ get width() {
1801
+ return TRANSPORT_LAYOUT.prevWidth;
1802
+ },
1803
+ get height() {
1804
+ return TRANSPORT_LAYOUT.height;
1805
+ },
1806
+ onClick: () => props.onPrev()
1807
+ }), null);
1808
+ _$insert3(_el$16, _$createComponent2(IconBtn, {
1809
+ get theme() {
1810
+ return theme();
1811
+ },
1812
+ get icon() {
1813
+ return _$memo(() => !!playing())() ? Icon.pause : Icon.play;
1814
+ },
1815
+ primary: true,
1816
+ get active() {
1817
+ return playing();
1818
+ },
1819
+ get accent() {
1820
+ return track()?.artwork?.accent;
1821
+ },
1822
+ get width() {
1823
+ return TRANSPORT_LAYOUT.playWidth;
1824
+ },
1825
+ get height() {
1826
+ return TRANSPORT_LAYOUT.height;
1827
+ },
1828
+ onClick: () => props.onPlayPause()
1829
+ }), null);
1830
+ _$insert3(_el$16, _$createComponent2(IconBtn, {
1831
+ get theme() {
1832
+ return theme();
1833
+ },
1834
+ get icon() {
1835
+ return Icon.next;
1836
+ },
1837
+ get width() {
1838
+ return TRANSPORT_LAYOUT.nextWidth;
1839
+ },
1840
+ get height() {
1841
+ return TRANSPORT_LAYOUT.height;
1842
+ },
1843
+ onClick: () => props.onNext()
1844
+ }), null);
1845
+ _$effect2((_p$) => {
1846
+ var _v$9 = playing() ? track()?.artwork?.accent ?? theme().text.action.primary.default : theme().border.default, _v$0 = TRANSPORT_LAYOUT.gap;
1847
+ _v$9 !== _p$.e && (_p$.e = _$setProp2(_el$1, "borderColor", _v$9, _p$.e));
1848
+ _v$0 !== _p$.t && (_p$.t = _$setProp2(_el$16, "gap", _v$0, _p$.t));
1849
+ return _p$;
1850
+ }, {
1851
+ e: undefined,
1852
+ t: undefined
1853
+ });
1854
+ return _el$1;
1855
+ })();
1856
+ }
1857
+
1858
+ // index.tsx
1859
+ var SEEK_END_MARGIN_MS = 1000;
1860
+ function seekTarget(positionMs, durationMs) {
1861
+ if (!Number.isFinite(positionMs) || typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs <= 0)
1862
+ return null;
1863
+ const ceiling = Math.max(0, durationMs - SEEK_END_MARGIN_MS);
1864
+ return Math.max(0, Math.min(ceiling, Math.round(positionMs)));
1865
+ }
1866
+ var controllerDependencies = {
1867
+ createSessionMedia: createSessionSystemMedia
1868
+ };
1869
+ function errMsg(error) {
1870
+ return error && typeof error === "object" && "message" in error ? String(error.message) : String(error);
1871
+ }
1872
+ function createController(context, dependencies = controllerDependencies) {
1873
+ const [session, setSessionStore] = context.storage.memory("music-player.session.v6", {
1874
+ initial: {
1875
+ loading: false,
1876
+ loadingOwners: [],
1877
+ error: null,
1878
+ player: null
1879
+ }
1880
+ });
1881
+ const sessionListeners = new Set;
1882
+ const setSession = (update) => {
1883
+ setSessionStore(update);
1884
+ for (const listener of [...sessionListeners]) {
1885
+ try {
1886
+ listener(session);
1887
+ } catch {}
1888
+ }
1889
+ };
1890
+ const loadingOwner = crypto.randomUUID();
1891
+ const media = dependencies.createSessionMedia();
1892
+ let disposed = false;
1893
+ let lifecycleGeneration = 0;
1894
+ let eventDisposer = null;
1895
+ let presentationDisposer = null;
1896
+ let lifecycleError = {
1897
+ message: null,
1898
+ source: undefined
1899
+ };
1900
+ let transportError = null;
1901
+ let receivedSnapshot = false;
1902
+ let snapshotEpoch = 0;
1903
+ let playbackIntent;
1904
+ let activeSeek = null;
1905
+ let latestSeek = null;
1906
+ const pending = new Set;
1907
+ const isActive = () => !disposed;
1908
+ const publishError = () => {
1909
+ if (!isActive())
1910
+ return;
1911
+ const message = lifecycleError.source === "connection" && lifecycleError.message ? lifecycleError.message : transportError ?? lifecycleError.message;
1912
+ setSession((draft) => {
1913
+ draft.error = message;
1914
+ });
1915
+ };
1916
+ const setLifecycleError = (message, source) => {
1917
+ lifecycleError = {
1918
+ message,
1919
+ source
1920
+ };
1921
+ publishError();
1922
+ };
1923
+ const setTransportError = (message) => {
1924
+ transportError = message;
1925
+ publishError();
1926
+ };
1927
+ const setLoadingOwner = (active) => {
1928
+ setSession((draft) => {
1929
+ const owners = new Set(draft.loadingOwners ?? []);
1930
+ if (active)
1931
+ owners.add(loadingOwner);
1932
+ else
1933
+ owners.delete(loadingOwner);
1934
+ draft.loadingOwners = [...owners];
1935
+ draft.loading = owners.size > 0;
1936
+ });
1937
+ };
1938
+ const updateLoading = () => {
1939
+ if (isActive())
1940
+ setLoadingOwner(pending.size > 0);
1941
+ };
1942
+ const settle = (resolve) => {
1943
+ if (!pending.delete(resolve))
1944
+ return;
1945
+ resolve();
1946
+ updateLoading();
1947
+ };
1948
+ const runCommand = (command, onSettled) => {
1949
+ if (!isActive())
1950
+ return Promise.resolve();
1951
+ const generation = lifecycleGeneration;
1952
+ return new Promise((resolve) => {
1953
+ pending.add(resolve);
1954
+ updateLoading();
1955
+ let result;
1956
+ try {
1957
+ result = command();
1958
+ } catch (error) {
1959
+ result = Promise.reject(error);
1960
+ }
1961
+ let succeeded = false;
1962
+ Promise.resolve(result).then(() => {
1963
+ succeeded = true;
1964
+ if (!isActive() || generation !== lifecycleGeneration)
1965
+ return;
1966
+ if (transportError !== null)
1967
+ setTransportError(null);
1968
+ }, (error) => {
1969
+ if (!isActive() || generation !== lifecycleGeneration)
1970
+ return;
1971
+ const message = sanitizeTerminalText(errMsg(error));
1972
+ setTransportError(message);
1973
+ context.ui.toast.show({
1974
+ title: "Music",
1975
+ message,
1976
+ variant: "error"
1977
+ });
1978
+ }).finally(() => {
1979
+ try {
1980
+ onSettled?.(succeeded);
1981
+ } finally {
1982
+ settle(resolve);
1983
+ }
1984
+ });
1985
+ });
1986
+ };
1987
+ const runSeek = (intent) => {
1988
+ activeSeek = intent;
1989
+ runCommand(() => media.seek(intent.positionMs), () => {
1990
+ if (activeSeek !== intent)
1991
+ return;
1992
+ activeSeek = null;
1993
+ for (const resolve of intent.resolves)
1994
+ resolve();
1995
+ intent.resolves = [];
1996
+ const next = latestSeek;
1997
+ latestSeek = null;
1998
+ if (isActive() && next)
1999
+ runSeek(next);
2000
+ else
2001
+ next?.resolves.splice(0).forEach((resolve) => resolve());
2002
+ });
2003
+ };
2004
+ const refreshAll = async () => {
2005
+ if (!isActive())
2006
+ return;
2007
+ const generation = lifecycleGeneration;
2008
+ try {
2009
+ const player = await media.player();
2010
+ if (!isActive() || generation !== lifecycleGeneration || receivedSnapshot)
2011
+ return;
2012
+ setSession((draft) => {
2013
+ draft.player = player;
2014
+ });
2015
+ } catch (error) {
2016
+ if (isActive() && generation === lifecycleGeneration)
2017
+ setTransportError(errMsg(error));
2018
+ }
2019
+ };
2020
+ const playPause = () => {
2021
+ const nextPlaying = !(playbackIntent?.target ?? !!session.player?.is_playing);
2022
+ const intent = {
2023
+ target: nextPlaying,
2024
+ startedAtEpoch: snapshotEpoch,
2025
+ settled: false
2026
+ };
2027
+ playbackIntent = intent;
2028
+ return runCommand(() => nextPlaying ? media.play() : media.pause(), (succeeded) => {
2029
+ if (playbackIntent !== intent)
2030
+ return;
2031
+ if (!succeeded) {
2032
+ playbackIntent = undefined;
2033
+ return;
2034
+ }
2035
+ intent.settled = true;
2036
+ if (snapshotEpoch > intent.startedAtEpoch && session.player?.is_playing === intent.target)
2037
+ playbackIntent = undefined;
2038
+ });
2039
+ };
2040
+ const seek = (positionMs) => {
2041
+ const target = seekTarget(positionMs, session.player?.track?.duration_ms);
2042
+ if (!session.player?.track || target === null || !isActive())
2043
+ return Promise.resolve();
2044
+ return new Promise((resolve) => {
2045
+ if (activeSeek) {
2046
+ if (latestSeek) {
2047
+ latestSeek.positionMs = target;
2048
+ latestSeek.resolves.push(resolve);
2049
+ } else {
2050
+ latestSeek = {
2051
+ positionMs: target,
2052
+ resolves: [resolve]
2053
+ };
2054
+ }
2055
+ return;
2056
+ }
2057
+ runSeek({
2058
+ positionMs: target,
2059
+ resolves: [resolve]
2060
+ });
2061
+ });
2062
+ };
2063
+ eventDisposer = media.subscribe((event) => {
2064
+ if (!isActive())
2065
+ return;
2066
+ if (event?.type === "snapshot") {
2067
+ receivedSnapshot = true;
2068
+ snapshotEpoch++;
2069
+ setSession((draft) => {
2070
+ draft.player = mergePlayerSnapshot(draft.player, event.state);
2071
+ });
2072
+ if (playbackIntent?.settled && snapshotEpoch > playbackIntent.startedAtEpoch && event.state.is_playing === playbackIntent.target)
2073
+ playbackIntent = undefined;
2074
+ return;
2075
+ }
2076
+ if (event?.type === "lifecycle") {
2077
+ setLifecycleError(event.message, event.source);
2078
+ if (event.source === "connection" && event.message !== null)
2079
+ playbackIntent = undefined;
2080
+ if (event.message !== null && latestSeek) {
2081
+ latestSeek.resolves.splice(0).forEach((resolve) => resolve());
2082
+ latestSeek = null;
2083
+ }
2084
+ }
2085
+ });
2086
+ presentationDisposer = media.subscribePresentation((event) => {
2087
+ if (!isActive())
2088
+ return;
2089
+ setSession((draft) => {
2090
+ draft.player = mergeArtworkCompletion(draft.player, event);
2091
+ });
2092
+ });
2093
+ refreshAll();
2094
+ return {
2095
+ session,
2096
+ subscribe(listener) {
2097
+ sessionListeners.add(listener);
2098
+ listener(session);
2099
+ return () => sessionListeners.delete(listener);
2100
+ },
2101
+ refreshAll,
2102
+ refreshArtwork: async () => {
2103
+ if (!isActive())
2104
+ return;
2105
+ try {
2106
+ await media.refreshArtwork();
2107
+ } catch (error) {
2108
+ if (isActive())
2109
+ setTransportError(errMsg(error));
2110
+ }
2111
+ },
2112
+ async openApp() {
2113
+ if (!isActive())
2114
+ return;
2115
+ try {
2116
+ openNowPlayingApp();
2117
+ context.ui.toast.show({
2118
+ title: "Music",
2119
+ message: "Play in any app \u2014 the sidebar uses system media",
2120
+ variant: "info"
2121
+ });
2122
+ } catch (error) {
2123
+ setTransportError(errMsg(error));
2124
+ }
2125
+ },
2126
+ playPause,
2127
+ seek,
2128
+ next: () => runCommand(() => media.next()),
2129
+ prev: () => runCommand(() => media.previous()),
2130
+ dispose() {
2131
+ if (disposed)
2132
+ return;
2133
+ disposed = true;
2134
+ lifecycleGeneration++;
2135
+ eventDisposer?.();
2136
+ eventDisposer = null;
2137
+ presentationDisposer?.();
2138
+ presentationDisposer = null;
2139
+ latestSeek?.resolves.splice(0).forEach((resolve) => resolve());
2140
+ latestSeek = null;
2141
+ activeSeek?.resolves.splice(0).forEach((resolve) => resolve());
2142
+ activeSeek = null;
2143
+ for (const resolve of [...pending])
2144
+ settle(resolve);
2145
+ media.dispose().catch(() => {});
2146
+ setLoadingOwner(false);
2147
+ sessionListeners.clear();
2148
+ }
2149
+ };
2150
+ }
2151
+ function AppHost(props) {
2152
+ const {
2153
+ context,
2154
+ ctrl
2155
+ } = props;
2156
+ context.keymap.layer(() => ({
2157
+ mode: "global",
2158
+ priority: 200,
2159
+ commands: [{
2160
+ id: "music.playpause",
2161
+ title: "Play / pause",
2162
+ group: "Music",
2163
+ bind: "ctrl+shift+p",
2164
+ palette: true,
2165
+ run: () => void ctrl.playPause()
2166
+ }, {
2167
+ id: "music.next",
2168
+ title: "Next track",
2169
+ group: "Music",
2170
+ bind: "ctrl+shift+right",
2171
+ palette: true,
2172
+ run: () => void ctrl.next()
2173
+ }, {
2174
+ id: "music.prev",
2175
+ title: "Previous track",
2176
+ group: "Music",
2177
+ bind: "ctrl+shift+left",
2178
+ palette: true,
2179
+ run: () => void ctrl.prev()
2180
+ }, {
2181
+ id: "music.open-app",
2182
+ title: "Open music site",
2183
+ group: "Music",
2184
+ palette: true,
2185
+ slash: {
2186
+ name: "music-app"
2187
+ },
2188
+ run: () => void ctrl.openApp()
2189
+ }, {
2190
+ id: "music.refresh-artwork",
2191
+ title: "Refresh artwork",
2192
+ group: "Music",
2193
+ palette: true,
2194
+ slash: {
2195
+ name: "music-artwork"
2196
+ },
2197
+ run: () => void ctrl.refreshArtwork()
2198
+ }]
2199
+ }));
2200
+ return _$createComponent3(CompactPlayer, {
2201
+ context,
2202
+ get state() {
2203
+ return ctrl.session;
2204
+ },
2205
+ onPlayPause: () => void ctrl.playPause(),
2206
+ onSeek: (positionMs) => void ctrl.seek(positionMs)
2207
+ });
2208
+ }
2209
+ function createMusicPlayerPlugin(options) {
2210
+ return Plugin.define({
2211
+ id: "music-player",
2212
+ setup(context) {
2213
+ if (!isMac()) {
2214
+ context.ui.toast.show({
2215
+ title: "Music",
2216
+ message: "System media control is macOS-only",
2217
+ variant: "warning"
2218
+ });
2219
+ }
2220
+ const ctrl = options?.createController?.(context) ?? createController(context, options?.createSessionMedia ? {
2221
+ ...controllerDependencies,
2222
+ createSessionMedia: options.createSessionMedia
2223
+ } : controllerDependencies);
2224
+ const observeSession = () => {
2225
+ ctrl.session.loading;
2226
+ ctrl.session.error;
2227
+ ctrl.session.player;
2228
+ };
2229
+ const unsubApp = context.ui.slot({
2230
+ append: "session.composer.top",
2231
+ render: () => {
2232
+ observeSession();
2233
+ return _$createComponent3(AppHost, {
2234
+ context,
2235
+ ctrl
2236
+ });
2237
+ }
2238
+ });
2239
+ const unsubSidebar = context.ui.slot({
2240
+ append: "sidebar.content",
2241
+ render: () => {
2242
+ observeSession();
2243
+ return _$createComponent3(SidebarPlayer, {
2244
+ context,
2245
+ get state() {
2246
+ return ctrl.session;
2247
+ },
2248
+ get subscribe() {
2249
+ return ctrl.subscribe;
2250
+ },
2251
+ onPlayPause: () => void ctrl.playPause(),
2252
+ onNext: () => void ctrl.next(),
2253
+ onPrev: () => void ctrl.prev(),
2254
+ onSeek: (positionMs) => void ctrl.seek(positionMs)
2255
+ });
2256
+ }
2257
+ });
2258
+ return () => {
2259
+ unsubApp();
2260
+ unsubSidebar();
2261
+ ctrl.dispose();
2262
+ };
2263
+ }
2264
+ });
2265
+ }
2266
+ var opencode_music_player_default = createMusicPlayerPlugin();
2267
+ export {
2268
+ opencode_music_player_default as default,
2269
+ createMusicPlayerPlugin,
2270
+ createController,
2271
+ SEEK_END_MARGIN_MS
2272
+ };