@naxodev/opencode-music-player 0.2.6 → 0.3.1

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/artwork.tsx DELETED
@@ -1,246 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { BoxRenderable, CliRenderer } from "@opentui/core"
3
- import { For, onCleanup, onMount } from "solid-js"
4
- import type { Plugin } from "@opencode-ai/plugin/tui"
5
- import type { Artwork } from "./types.ts"
6
- import {
7
- planNativeArtworkPlacement,
8
- type NativeArtworkPlacementAction,
9
- type NativeArtworkPlacementPlan,
10
- type NativeArtworkState,
11
- } from "./artwork-placement.ts"
12
- import {
13
- kittyDelete,
14
- kittyDeletePlacement,
15
- kittyDisplayPng,
16
- kittyImageId,
17
- kittyPlace,
18
- writeGraphics,
19
- } from "./kitty-graphics.ts"
20
- import {
21
- createTmuxOffsetCache,
22
- resolveTerminalOffset,
23
- type SlotGeometry,
24
- } from "./tmux-offset.ts"
25
-
26
- type Context = Plugin.Context
27
- const tmuxOffsetCache = createTmuxOffsetCache()
28
- const nativeArtworkImageId = kittyImageId("opencode-music-player:artwork")
29
- type NativeArtworkRuntime = {
30
- latestOwner: number
31
- state: NativeArtworkState
32
- identity: string
33
- }
34
- const defaultOwnershipScope = { latestOwner: 0 }
35
- const nativeArtworkRuntimes = new WeakMap<CliRenderer, NativeArtworkRuntime>()
36
-
37
- function nativeArtworkRuntime(renderer: CliRenderer): NativeArtworkRuntime {
38
- const existing = nativeArtworkRuntimes.get(renderer)
39
- if (existing) return existing
40
- const created = {
41
- latestOwner: 0,
42
- state: { transmitted: 0, placement: null },
43
- identity: "",
44
- }
45
- nativeArtworkRuntimes.set(renderer, created)
46
- return created
47
- }
48
-
49
- export type NativeArtworkOwnership = { isCurrent: () => boolean }
50
-
51
- export function claimNativeArtworkOwnership(
52
- scope: { latestOwner: number } = defaultOwnershipScope,
53
- ): NativeArtworkOwnership {
54
- const owner = ++scope.latestOwner
55
- return { isCurrent: () => owner === scope.latestOwner }
56
- }
57
-
58
- export function cleanupNativeArtwork(
59
- ownership: NativeArtworkOwnership,
60
- remove: () => void,
61
- ): void {
62
- if (ownership.isCurrent()) remove()
63
- }
64
-
65
- export function imageIdForArtwork(_artworkId: string): number {
66
- return nativeArtworkImageId
67
- }
68
-
69
- export function legacyImageIdForArtwork(artworkId: string): number {
70
- return kittyImageId(artworkId)
71
- }
72
-
73
- export function legacyImageIdForResolvedArtwork(
74
- artwork: Pick<Artwork, "id" | "legacy_id">,
75
- ): number {
76
- return legacyImageIdForArtwork(artwork.legacy_id ?? artwork.id)
77
- }
78
-
79
- function terminalOffset(slot: SlotGeometry | null) {
80
- return resolveTerminalOffset({
81
- slot,
82
- cache: tmuxOffsetCache,
83
- })
84
- }
85
-
86
- function supportsKittyGraphics(context: Context): boolean {
87
- return (
88
- !!context.renderer.capabilities?.kitty_graphics ||
89
- process.env.TERM_PROGRAM?.toLowerCase() === "ghostty"
90
- )
91
- }
92
-
93
- function copyState(next: NativeArtworkState): NativeArtworkState {
94
- return {
95
- transmitted: next.transmitted,
96
- placement: next.placement ? { ...next.placement } : null,
97
- }
98
- }
99
-
100
- export function AlbumArtwork(props: { context: Context; artwork: Artwork }) {
101
- const renderer = props.context.renderer
102
- const runtime = nativeArtworkRuntime(renderer)
103
- const ownership = claimNativeArtworkOwnership(runtime)
104
- let container: BoxRenderable | undefined
105
- let paintPending = false
106
- let disposed = false
107
-
108
- const applyAction = (action: NativeArtworkPlacementAction): boolean => {
109
- const renderer = props.context.renderer
110
- switch (action.type) {
111
- case "delete-image":
112
- return writeGraphics(renderer, kittyDelete(action.imageId))
113
- case "delete-placement":
114
- return writeGraphics(renderer, kittyDeletePlacement(action.imageId))
115
- case "transmit-and-display": {
116
- const commands = kittyDisplayPng(
117
- props.artwork.png_base64,
118
- action.imageId,
119
- action.x,
120
- action.y,
121
- action.width,
122
- action.height,
123
- )
124
- return writeGraphics(renderer, commands)
125
- }
126
- case "place":
127
- return writeGraphics(
128
- renderer,
129
- kittyPlace(
130
- action.imageId,
131
- action.placementId,
132
- action.x,
133
- action.y,
134
- action.width,
135
- action.height,
136
- ),
137
- )
138
- }
139
- }
140
-
141
- // Partial success must not commit: stop on first failed write and keep prior state.
142
- const applyPlan = (plan: NativeArtworkPlacementPlan): boolean => {
143
- for (const action of plan.actions) {
144
- if (!applyAction(action)) return false
145
- }
146
- return true
147
- }
148
-
149
- const commitPlan = (plan: NativeArtworkPlacementPlan) => {
150
- runtime.state = copyState(plan.nextState)
151
- }
152
-
153
- const paintNativeImage = () => {
154
- if (!ownership.isCurrent()) return
155
- if (runtime.identity !== props.artwork.id) {
156
- runtime.identity = props.artwork.id
157
- writeGraphics(
158
- props.context.renderer,
159
- kittyDelete(legacyImageIdForResolvedArtwork(props.artwork)),
160
- )
161
- runtime.state = { transmitted: 0, placement: null }
162
- }
163
- const kittySupported = supportsKittyGraphics(props.context)
164
- const slotValid =
165
- !!container &&
166
- !container.isDestroyed &&
167
- container.width >= 1 &&
168
- container.height >= 1
169
-
170
- const slot: SlotGeometry | null = container
171
- ? {
172
- screenX: container.screenX,
173
- screenY: container.screenY,
174
- width: container.width,
175
- height: container.height,
176
- }
177
- : null
178
- const offset = terminalOffset(slot)
179
- const imageId = imageIdForArtwork(props.artwork.id)
180
- const x = slot ? slot.screenX + offset.x : 0
181
- const y = slot ? slot.screenY + offset.y : 0
182
- const width = slot?.width ?? 0
183
- const height = slot?.height ?? 0
184
-
185
- const plan = planNativeArtworkPlacement({
186
- state: runtime.state,
187
- imageId,
188
- x,
189
- y,
190
- width,
191
- height,
192
- kittySupported,
193
- slotValid,
194
- disposed,
195
- })
196
-
197
- if (plan.actions.length === 0) return
198
- if (applyPlan(plan)) commitPlan(plan)
199
- }
200
-
201
- const scheduleNativeImage = () => {
202
- if (paintPending || disposed) return
203
- paintPending = true
204
- void props.context.renderer.idle().then(() => {
205
- paintPending = false
206
- if (!disposed && ownership.isCurrent()) paintNativeImage()
207
- })
208
- }
209
-
210
- onMount(() => props.context.renderer.on("frame", scheduleNativeImage))
211
- onCleanup(() => {
212
- disposed = true
213
- props.context.renderer.off("frame", scheduleNativeImage)
214
- // A replacement mount claims ownership before the next task runs.
215
- setTimeout(() => {
216
- cleanupNativeArtwork(ownership, () => {
217
- if (!renderer.isDestroyed)
218
- writeGraphics(renderer, kittyDelete(nativeArtworkImageId))
219
- runtime.state = { transmitted: 0, placement: null }
220
- runtime.identity = ""
221
- })
222
- }, 0)
223
- })
224
-
225
- return (
226
- <box
227
- ref={(value) => (container = value)}
228
- width={24}
229
- height={12}
230
- flexDirection="column"
231
- overflow="hidden"
232
- >
233
- <For each={props.artwork.cells}>
234
- {(row) => (
235
- <text>
236
- <For each={row}>
237
- {(cell) => (
238
- <span style={{ fg: cell.upper, bg: cell.lower }}>▀</span>
239
- )}
240
- </For>
241
- </text>
242
- )}
243
- </For>
244
- </box>
245
- )
246
- }