@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/README.md +17 -14
- package/SUPPORT.md +1 -1
- package/dist/index.js +2272 -0
- package/dist/tui.js +1 -0
- package/package.json +29 -21
- package/artwork-placement.ts +0 -169
- package/artwork.ts +0 -424
- package/artwork.tsx +0 -246
- package/index.tsx +0 -476
- package/kitty-graphics.ts +0 -105
- package/system-media.ts +0 -571
- package/tmux-offset.ts +0 -120
- package/tui.tsx +0 -1
- package/types.ts +0 -143
- package/ui.tsx +0 -695
- package/waveform.tsx +0 -205
package/tmux-offset.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tmux pane-origin offset for absolute Kitty coordinates.
|
|
3
|
-
*
|
|
4
|
-
* Cache is keyed by OpenTUI **pre-offset** slot geometry so resize/layout
|
|
5
|
-
* changes force a re-query even when wall-clock TTL would still allow a hit.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type SlotGeometry = {
|
|
9
|
-
screenX: number
|
|
10
|
-
screenY: number
|
|
11
|
-
width: number
|
|
12
|
-
height: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type TmuxOffset = { x: number; y: number }
|
|
16
|
-
|
|
17
|
-
export type TmuxOffsetQueryResult = {
|
|
18
|
-
ok: boolean
|
|
19
|
-
stdout: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type TmuxOffsetCache = {
|
|
23
|
-
offset: TmuxOffset | null
|
|
24
|
-
slot: SlotGeometry | null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const ZERO: TmuxOffset = { x: 0, y: 0 }
|
|
28
|
-
|
|
29
|
-
export function createTmuxOffsetCache(): TmuxOffsetCache {
|
|
30
|
-
return { offset: null, slot: null }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** True only when running under tmux without Herdr (Herdr already owns passthrough). */
|
|
34
|
-
export function shouldUseTmuxOffset(
|
|
35
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
36
|
-
): boolean {
|
|
37
|
-
return Boolean(env.TMUX) && !env.HERDR_ENV
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function slotKey(slot: SlotGeometry): string {
|
|
41
|
-
return [slot.screenX, slot.screenY, slot.width, slot.height].join(":")
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function sameSlotGeometry(
|
|
45
|
-
a: SlotGeometry | null,
|
|
46
|
-
b: SlotGeometry | null,
|
|
47
|
-
): boolean {
|
|
48
|
-
if (a === null || b === null) return false
|
|
49
|
-
return slotKey(a) === slotKey(b)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Parse `tmux display-message -p '#{pane_left}\t#{pane_top}\t#{status-position}\t#{status}'`.
|
|
54
|
-
* Status bar on top adds one row when status is not off.
|
|
55
|
-
*/
|
|
56
|
-
export function parseTmuxOffsetMessage(stdout: string): TmuxOffset {
|
|
57
|
-
const [left, top, statusPosition, status] = stdout.trim().split("\t")
|
|
58
|
-
return {
|
|
59
|
-
x: Number.parseInt(left ?? "0", 10) || 0,
|
|
60
|
-
y:
|
|
61
|
-
(Number.parseInt(top ?? "0", 10) || 0) +
|
|
62
|
-
(statusPosition === "top" && status !== "off" ? 1 : 0),
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function queryTmuxOffset(): TmuxOffsetQueryResult {
|
|
67
|
-
const result = Bun.spawnSync([
|
|
68
|
-
"tmux",
|
|
69
|
-
"display-message",
|
|
70
|
-
"-p",
|
|
71
|
-
"#{pane_left}\t#{pane_top}\t#{status-position}\t#{status}",
|
|
72
|
-
])
|
|
73
|
-
return {
|
|
74
|
-
ok: result.exitCode === 0,
|
|
75
|
-
stdout: result.stdout.toString(),
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Resolve pane origin for absolute Kitty placement.
|
|
81
|
-
*
|
|
82
|
-
* - No TMUX / HERDR_ENV → zeros (no spawn).
|
|
83
|
-
* - Slot geometry changed or first paint / empty cache → re-query.
|
|
84
|
-
* - Identical slot + prior successful cache → reuse (no spawn).
|
|
85
|
-
* - Failed query → zeros and clear cache so the next paint retries.
|
|
86
|
-
*/
|
|
87
|
-
export function resolveTerminalOffset(options: {
|
|
88
|
-
env?: NodeJS.ProcessEnv
|
|
89
|
-
slot: SlotGeometry | null
|
|
90
|
-
cache: TmuxOffsetCache
|
|
91
|
-
query?: () => TmuxOffsetQueryResult
|
|
92
|
-
}): TmuxOffset {
|
|
93
|
-
const env = options.env ?? process.env
|
|
94
|
-
if (!shouldUseTmuxOffset(env)) {
|
|
95
|
-
options.cache.offset = null
|
|
96
|
-
options.cache.slot = null
|
|
97
|
-
return ZERO
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
options.cache.offset !== null &&
|
|
102
|
-
options.slot !== null &&
|
|
103
|
-
sameSlotGeometry(options.cache.slot, options.slot)
|
|
104
|
-
) {
|
|
105
|
-
return options.cache.offset
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const query = options.query ?? queryTmuxOffset
|
|
109
|
-
const result = query()
|
|
110
|
-
if (!result.ok) {
|
|
111
|
-
options.cache.offset = null
|
|
112
|
-
options.cache.slot = null
|
|
113
|
-
return ZERO
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const offset = parseTmuxOffsetMessage(result.stdout)
|
|
117
|
-
options.cache.offset = offset
|
|
118
|
-
options.cache.slot = options.slot ? { ...options.slot } : null
|
|
119
|
-
return offset
|
|
120
|
-
}
|
package/tui.tsx
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from "./index.tsx"
|
package/types.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PlayerState as CorePlayerState,
|
|
3
|
-
Track as CoreTrack,
|
|
4
|
-
} from "@naxodev/music-core"
|
|
5
|
-
import {
|
|
6
|
-
emptyPlayer as emptyCorePlayer,
|
|
7
|
-
formatMs,
|
|
8
|
-
isMac,
|
|
9
|
-
} from "@naxodev/music-core"
|
|
10
|
-
|
|
11
|
-
export type { Device, MusicError } from "@naxodev/music-core"
|
|
12
|
-
export { formatMs, isMac }
|
|
13
|
-
|
|
14
|
-
export type Artwork = {
|
|
15
|
-
id: string
|
|
16
|
-
legacy_id?: string
|
|
17
|
-
png_base64: string
|
|
18
|
-
accent: string
|
|
19
|
-
cells: Array<Array<{ upper: string; lower: string }>>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Native IDs validate artwork samples; recording metadata keys artwork work. */
|
|
23
|
-
export type ArtworkIdentity = {
|
|
24
|
-
uid: string
|
|
25
|
-
title: string
|
|
26
|
-
artist: string
|
|
27
|
-
album: string
|
|
28
|
-
duration_ms: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type ArtworkCompletionEvent = {
|
|
32
|
-
type: "artwork-completion"
|
|
33
|
-
identity: ArtworkIdentity
|
|
34
|
-
artwork: Artwork | null
|
|
35
|
-
duration_ms: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type ArtworkPresentationListener = (
|
|
39
|
-
event: ArtworkCompletionEvent,
|
|
40
|
-
) => void
|
|
41
|
-
|
|
42
|
-
export type Track = CoreTrack & {
|
|
43
|
-
artwork: Artwork | null
|
|
44
|
-
artwork_loading?: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type PlayerState = Omit<CorePlayerState, "track"> & {
|
|
48
|
-
track: Track | null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export type SessionMediaSnapshotEvent = {
|
|
52
|
-
type: "snapshot"
|
|
53
|
-
state: PlayerState
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Host-local projection of session provider/connection lifecycle. */
|
|
57
|
-
export type SessionMediaLifecycleEvent = {
|
|
58
|
-
type: "lifecycle"
|
|
59
|
-
message: string | null
|
|
60
|
-
source: "connection" | "provider" | "acquisition"
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type SessionMediaEvent =
|
|
64
|
-
SessionMediaSnapshotEvent | SessionMediaLifecycleEvent
|
|
65
|
-
export type SessionMediaListener = (event: SessionMediaEvent) => void
|
|
66
|
-
export type SessionMediaDisposer = () => void
|
|
67
|
-
|
|
68
|
-
/** The OpenCode controller's session-only media contract. */
|
|
69
|
-
export type SessionMedia = {
|
|
70
|
-
player: () => Promise<PlayerState | null>
|
|
71
|
-
play: () => Promise<unknown>
|
|
72
|
-
pause: () => Promise<unknown>
|
|
73
|
-
next: () => Promise<unknown>
|
|
74
|
-
previous: () => Promise<unknown>
|
|
75
|
-
seek: (positionMs: number) => Promise<unknown>
|
|
76
|
-
subscribe: (listener: SessionMediaListener) => SessionMediaDisposer
|
|
77
|
-
subscribePresentation: (
|
|
78
|
-
listener: ArtworkPresentationListener,
|
|
79
|
-
) => SessionMediaDisposer
|
|
80
|
-
dispose: () => Promise<void>
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function emptyPlayer(): PlayerState {
|
|
84
|
-
return {
|
|
85
|
-
...emptyCorePlayer(),
|
|
86
|
-
track: null,
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Merge an independent artwork result without accepting stale playback data. */
|
|
91
|
-
export function mergeArtworkCompletion(
|
|
92
|
-
player: PlayerState | null,
|
|
93
|
-
event: ArtworkCompletionEvent,
|
|
94
|
-
): PlayerState | null {
|
|
95
|
-
const track = player?.track
|
|
96
|
-
if (
|
|
97
|
-
!track ||
|
|
98
|
-
track.name !== event.identity.title ||
|
|
99
|
-
track.artists !== event.identity.artist ||
|
|
100
|
-
track.album !== event.identity.album ||
|
|
101
|
-
(track.duration_ms > 0 && track.duration_ms !== event.identity.duration_ms)
|
|
102
|
-
) {
|
|
103
|
-
return player
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
...player,
|
|
107
|
-
track: {
|
|
108
|
-
...track,
|
|
109
|
-
artwork: event.artwork,
|
|
110
|
-
artwork_loading: false,
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Keep host-local artwork presentation across playback-only snapshots. */
|
|
116
|
-
export function mergePlayerSnapshot(
|
|
117
|
-
player: PlayerState | null,
|
|
118
|
-
snapshot: PlayerState,
|
|
119
|
-
): PlayerState {
|
|
120
|
-
const current = player?.track
|
|
121
|
-
const next = snapshot.track
|
|
122
|
-
if (
|
|
123
|
-
!current ||
|
|
124
|
-
!next ||
|
|
125
|
-
current.name !== next.name ||
|
|
126
|
-
current.artists !== next.artists ||
|
|
127
|
-
current.album !== next.album ||
|
|
128
|
-
(current.duration_ms > 0 &&
|
|
129
|
-
next.duration_ms > 0 &&
|
|
130
|
-
current.duration_ms !== next.duration_ms) ||
|
|
131
|
-
(current.artwork === null && current.artwork_loading !== false)
|
|
132
|
-
) {
|
|
133
|
-
return snapshot
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
...snapshot,
|
|
137
|
-
track: {
|
|
138
|
-
...next,
|
|
139
|
-
artwork: next.artwork ?? current.artwork,
|
|
140
|
-
artwork_loading: false,
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
}
|