@naxodev/opencode-music-player 0.5.1 → 0.5.2

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.
Files changed (2) hide show
  1. package/dist/index.js +135 -53
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32,6 +32,14 @@ var CONVERSION_TIMEOUT_MS = 3000;
32
32
  function imageDimensionsAreSafe(width, height) {
33
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
34
  }
35
+ function pngDimensions(base64) {
36
+ const header = Buffer.from(base64.slice(0, 44), "base64");
37
+ if (header.length < 24 || header.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a" || header.readUInt32BE(8) !== 13 || header.toString("ascii", 12, 16) !== "IHDR")
38
+ return null;
39
+ const width = header.readUInt32BE(16);
40
+ const height = header.readUInt32BE(20);
41
+ return imageDimensionsAreSafe(width, height) ? { width, height } : null;
42
+ }
35
43
  async function runCommandWithTimeout(command, timeoutMs) {
36
44
  const proc = Bun.spawn(command, { stdout: "pipe", stderr: "ignore" });
37
45
  let timedOut = false;
@@ -54,7 +62,7 @@ function dimensionsFromSips(output) {
54
62
  const height = Number(output.match(/pixelHeight:\s*(\d+)/)?.[1]);
55
63
  return imageDimensionsAreSafe(width, height) ? { width, height } : null;
56
64
  }
57
- async function squarePng(bytes, size) {
65
+ async function fitPng(bytes, size) {
58
66
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_ARTWORK_BYTES)
59
67
  return null;
60
68
  const dir = await mkdtemp(join(tmpdir(), "opencode-music-art-"));
@@ -63,14 +71,18 @@ async function squarePng(bytes, size) {
63
71
  try {
64
72
  await Bun.write(input, bytes);
65
73
  const inspected = await runCommandWithTimeout(["sips", "-g", "pixelWidth", "-g", "pixelHeight", input], CONVERSION_TIMEOUT_MS);
66
- if (inspected.timed_out || inspected.code !== 0 || !dimensionsFromSips(inspected.out)) {
74
+ const dimensions = dimensionsFromSips(inspected.out);
75
+ if (inspected.timed_out || inspected.code !== 0 || !dimensions) {
67
76
  return null;
68
77
  }
78
+ const scale = size / Math.max(dimensions.width, dimensions.height);
79
+ const width = Math.max(1, Math.round(dimensions.width * scale));
80
+ const height = Math.max(1, Math.round(dimensions.height * scale));
69
81
  const converted = await runCommandWithTimeout([
70
82
  "sips",
71
83
  "-z",
72
- String(size),
73
- String(size),
84
+ String(height),
85
+ String(width),
74
86
  "-s",
75
87
  "format",
76
88
  "png",
@@ -134,8 +146,8 @@ async function resolveArtworkDetails(id, target, nativeBase64, legacyId = id, fe
134
146
  }
135
147
  for (const bytes of candidates) {
136
148
  try {
137
- const nativePng = await squarePng(bytes, 300);
138
- const thumbnail = nativePng ? await squarePng(nativePng, 24) : null;
149
+ const nativePng = await fitPng(bytes, 300);
150
+ const thumbnail = nativePng ? await fitPng(nativePng, 24) : null;
139
151
  if (nativePng && thumbnail) {
140
152
  return {
141
153
  artwork: {
@@ -162,8 +174,8 @@ async function resolveArtworkDetails(id, target, nativeBase64, legacyId = id, fe
162
174
  return { artwork: null, duration_ms: catalog.duration_ms };
163
175
  }
164
176
  try {
165
- const nativePng = await squarePng(catalog.bytes, 300);
166
- const thumbnail = nativePng ? await squarePng(nativePng, 24) : null;
177
+ const nativePng = await fitPng(catalog.bytes, 300);
178
+ const thumbnail = nativePng ? await fitPng(nativePng, 24) : null;
167
179
  if (!nativePng || !thumbnail) {
168
180
  return { artwork: null, duration_ms: catalog.duration_ms };
169
181
  }
@@ -709,7 +721,7 @@ import { use as _$use3 } from "@opentui/solid";
709
721
  import { setProp as _$setProp2 } from "@opentui/solid";
710
722
  import { createElement as _$createElement3 } from "@opentui/solid";
711
723
  import { createComponent as _$createComponent2 } from "@opentui/solid";
712
- import { Show, createEffect, createMemo, createSignal, onCleanup as onCleanup3, onMount as onMount3 } from "solid-js";
724
+ import { Show as Show2, createEffect, createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup3, onMount as onMount3 } from "solid-js";
713
725
  import { MouseButton } from "@opentui/core";
714
726
 
715
727
  // artwork.tsx
@@ -721,9 +733,49 @@ import { createComponent as _$createComponent } from "@opentui/solid";
721
733
  import { setProp as _$setProp } from "@opentui/solid";
722
734
  import { use as _$use } from "@opentui/solid";
723
735
  import { createElement as _$createElement } from "@opentui/solid";
724
- import { For, onCleanup, onMount } from "solid-js";
736
+ import { For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js";
725
737
 
726
738
  // artwork-placement.ts
739
+ function fitNativeArtworkGeometry(slot, image, cell) {
740
+ if ([
741
+ slot.width,
742
+ slot.height,
743
+ image.width,
744
+ image.height,
745
+ cell.width,
746
+ cell.height
747
+ ].some((value) => !Number.isFinite(value) || value <= 0))
748
+ return null;
749
+ const width = Math.floor(slot.width);
750
+ const height = Math.floor(slot.height);
751
+ if (width < 1 || height < 1)
752
+ return null;
753
+ const pixelWidth = width * cell.width;
754
+ const pixelHeight = height * cell.height;
755
+ const aspect = image.width / image.height;
756
+ if (pixelWidth / aspect <= pixelHeight) {
757
+ const displayedHeight = Math.round(pixelWidth / aspect);
758
+ if (displayedHeight < 1)
759
+ return null;
760
+ const occupiedRows = Math.ceil(displayedHeight / cell.height);
761
+ return {
762
+ x: slot.x,
763
+ y: slot.y + Math.floor((height - occupiedRows) / 2),
764
+ width,
765
+ height: 0
766
+ };
767
+ }
768
+ const displayedWidth = Math.round(pixelHeight * aspect);
769
+ if (displayedWidth < 1)
770
+ return null;
771
+ const occupiedColumns = Math.ceil(displayedWidth / cell.width);
772
+ return {
773
+ x: slot.x + Math.floor((width - occupiedColumns) / 2),
774
+ y: slot.y,
775
+ width: 0,
776
+ height
777
+ };
778
+ }
727
779
  var CLEARED_STATE = {
728
780
  transmitted: 0,
729
781
  placement: null
@@ -798,6 +850,9 @@ function planNativeArtworkPlacement(input) {
798
850
  var APC = "\x1B_G";
799
851
  var ST = "\x1B\\";
800
852
  var CHUNK_SIZE = 4096;
853
+ function scalingDimensions(width, height) {
854
+ return [width > 0 ? `c=${width}` : "", height > 0 ? `r=${height}` : ""].filter(Boolean).join(",");
855
+ }
801
856
  function tmuxPassthrough(data) {
802
857
  return `\x1BPtmux;${data.replaceAll("\x1B", "\x1B\x1B")}\x1B\\`;
803
858
  }
@@ -816,13 +871,13 @@ function kittyDisplayPng(pngBase64, imageId, x, y, width, height) {
816
871
  return chunks.map((command, index) => {
817
872
  let next = command;
818
873
  if (index === 0) {
819
- 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,`);
874
+ 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,${scalingDimensions(width, height)},z=1,`);
820
875
  }
821
876
  return index === chunks.length - 1 ? `${next}\x1B8` : next;
822
877
  });
823
878
  }
824
879
  function kittyPlace(imageId, placementId, x, y, width, height) {
825
- 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`;
880
+ return `\x1B7\x1B[${y + 1};${x + 1}H${APC}a=p,i=${imageId},p=${placementId},q=2,${scalingDimensions(width, height)},z=1;${ST}\x1B8`;
826
881
  }
827
882
  function kittyDelete(imageId) {
828
883
  return `${APC}a=d,d=I,i=${imageId},q=2;${ST}`;
@@ -986,6 +1041,8 @@ function AlbumArtwork(props) {
986
1041
  let container;
987
1042
  let paintPending = false;
988
1043
  let disposed = false;
1044
+ const [nativeVisible, setNativeVisible] = createSignal(false);
1045
+ const imageDimensions = createMemo(() => pngDimensions(props.artwork.png_base64));
989
1046
  const applyAction = (action) => {
990
1047
  const renderer2 = props.context.renderer;
991
1048
  switch (action.type) {
@@ -1034,23 +1091,39 @@ function AlbumArtwork(props) {
1034
1091
  const imageId = imageIdForArtwork(props.artwork.id);
1035
1092
  const x = slot ? slot.screenX + offset.x : 0;
1036
1093
  const y = slot ? slot.screenY + offset.y : 0;
1037
- const width = slot?.width ?? 0;
1038
- const height = slot?.height ?? 0;
1094
+ const image = imageDimensions();
1095
+ const resolution = renderer.resolution;
1096
+ const geometry = slot && image && resolution ? fitNativeArtworkGeometry({
1097
+ x,
1098
+ y,
1099
+ width: slot.width,
1100
+ height: slot.height
1101
+ }, image, {
1102
+ width: Math.floor(resolution.width / renderer.terminalWidth),
1103
+ height: Math.floor(resolution.height / renderer.terminalHeight)
1104
+ }) : null;
1039
1105
  const plan = planNativeArtworkPlacement({
1040
1106
  state: runtime.state,
1041
1107
  imageId,
1042
- x,
1043
- y,
1044
- width,
1045
- height,
1108
+ ...geometry ?? {
1109
+ x,
1110
+ y,
1111
+ width: 0,
1112
+ height: 0
1113
+ },
1046
1114
  kittySupported,
1047
- slotValid,
1115
+ slotValid: slotValid && geometry !== null,
1048
1116
  disposed
1049
1117
  });
1050
- if (plan.actions.length === 0)
1118
+ if (plan.actions.length === 0) {
1119
+ setNativeVisible(runtime.state.placement !== null);
1051
1120
  return;
1052
- if (applyPlan(plan))
1121
+ }
1122
+ if (applyPlan(plan)) {
1053
1123
  commitPlan(plan);
1124
+ setNativeVisible(plan.nextState.placement !== null);
1125
+ } else
1126
+ setNativeVisible(false);
1054
1127
  };
1055
1128
  const scheduleNativeImage = () => {
1056
1129
  if (paintPending || disposed)
@@ -1088,27 +1161,36 @@ function AlbumArtwork(props) {
1088
1161
  _$setProp(_el$, "width", 24);
1089
1162
  _$setProp(_el$, "height", 12);
1090
1163
  _$setProp(_el$, "flexDirection", "column");
1164
+ _$setProp(_el$, "alignItems", "center");
1165
+ _$setProp(_el$, "justifyContent", "center");
1091
1166
  _$setProp(_el$, "overflow", "hidden");
1092
- _$insert(_el$, _$createComponent(For, {
1093
- get each() {
1094
- return props.artwork.cells;
1167
+ _$insert(_el$, _$createComponent(Show, {
1168
+ get when() {
1169
+ return !nativeVisible();
1095
1170
  },
1096
- children: (row) => (() => {
1097
- var _el$2 = _$createElement("text");
1098
- _$insert(_el$2, _$createComponent(For, {
1099
- each: row,
1100
- children: (cell) => (() => {
1101
- var _el$3 = _$createElement("span");
1102
- _$insertNode(_el$3, _$createTextNode(`\u2580`));
1103
- _$effect((_$p) => _$setProp(_el$3, "style", {
1104
- fg: cell.upper,
1105
- bg: cell.lower
1106
- }, _$p));
1107
- return _el$3;
1171
+ get children() {
1172
+ return _$createComponent(For, {
1173
+ get each() {
1174
+ return props.artwork.cells;
1175
+ },
1176
+ children: (row) => (() => {
1177
+ var _el$2 = _$createElement("text");
1178
+ _$insert(_el$2, _$createComponent(For, {
1179
+ each: row,
1180
+ children: (cell) => (() => {
1181
+ var _el$3 = _$createElement("span");
1182
+ _$insertNode(_el$3, _$createTextNode(`\u2580`));
1183
+ _$effect((_$p) => _$setProp(_el$3, "style", {
1184
+ fg: cell.upper,
1185
+ bg: cell.lower
1186
+ }, _$p));
1187
+ return _el$3;
1188
+ })()
1189
+ }));
1190
+ return _el$2;
1108
1191
  })()
1109
- }));
1110
- return _el$2;
1111
- })()
1192
+ });
1193
+ }
1112
1194
  }));
1113
1195
  return _el$;
1114
1196
  })();
@@ -1376,7 +1458,7 @@ function CompactPlayer(props) {
1376
1458
  let row;
1377
1459
  let content;
1378
1460
  let allocatedWidth = Number.MAX_SAFE_INTEGER;
1379
- const track = createMemo(() => props.state.player?.track);
1461
+ const track = createMemo2(() => props.state.player?.track);
1380
1462
  const renderLine = () => {
1381
1463
  const current = track();
1382
1464
  if (!current)
@@ -1399,7 +1481,7 @@ function CompactPlayer(props) {
1399
1481
  if (row?.onSizeChange === updateWidth)
1400
1482
  row.onSizeChange = undefined;
1401
1483
  });
1402
- return _$createComponent2(Show, {
1484
+ return _$createComponent2(Show2, {
1403
1485
  get when() {
1404
1486
  return track();
1405
1487
  },
@@ -1580,8 +1662,8 @@ function LiveElapsed(props) {
1580
1662
  })();
1581
1663
  }
1582
1664
  function IconBtn(props) {
1583
- const [hover, setHover] = createSignal(false);
1584
- const [press, setPress] = createSignal(false);
1665
+ const [hover, setHover] = createSignal2(false);
1666
+ const [press, setPress] = createSignal2(false);
1585
1667
  const bg = () => {
1586
1668
  if (press() || props.active) {
1587
1669
  return props.theme.background.action.primary.base;
@@ -1659,10 +1741,10 @@ function SidebarPlayer(props) {
1659
1741
  unsubscribeSession?.();
1660
1742
  presentationListeners.clear();
1661
1743
  });
1662
- const player = createMemo(() => props.state.player);
1663
- const track = createMemo(() => player()?.track);
1664
- const playing = createMemo(() => !!player()?.is_playing);
1665
- const duration = createMemo(() => presentationDuration(track()));
1744
+ const player = createMemo2(() => props.state.player);
1745
+ const track = createMemo2(() => player()?.track);
1746
+ const playing = createMemo2(() => !!player()?.is_playing);
1747
+ const duration = createMemo2(() => presentationDuration(track()));
1666
1748
  return (() => {
1667
1749
  var _el$1 = _$createElement3("box"), _el$10 = _$createElement3("box"), _el$11 = _$createElement3("box"), _el$12 = _$createElement3("box"), _el$16 = _$createElement3("box");
1668
1750
  _$insertNode2(_el$1, _el$10);
@@ -1676,7 +1758,7 @@ function SidebarPlayer(props) {
1676
1758
  _$setProp2(_el$1, "paddingBottom", 1);
1677
1759
  _$setProp2(_el$1, "gap", 1);
1678
1760
  _$setProp2(_el$1, "flexShrink", 0);
1679
- _$insert3(_el$1, _$createComponent2(Show, {
1761
+ _$insert3(_el$1, _$createComponent2(Show2, {
1680
1762
  get when() {
1681
1763
  return props.state.error;
1682
1764
  },
@@ -1693,7 +1775,7 @@ function SidebarPlayer(props) {
1693
1775
  _$setProp2(_el$11, "width", 24);
1694
1776
  _$setProp2(_el$11, "height", 12);
1695
1777
  _$setProp2(_el$11, "overflow", "hidden");
1696
- _$insert3(_el$11, _$createComponent2(Show, {
1778
+ _$insert3(_el$11, _$createComponent2(Show2, {
1697
1779
  get when() {
1698
1780
  return track()?.artwork;
1699
1781
  },
@@ -1722,7 +1804,7 @@ function SidebarPlayer(props) {
1722
1804
  }
1723
1805
  })
1724
1806
  }));
1725
- _$insert3(_el$1, _$createComponent2(Show, {
1807
+ _$insert3(_el$1, _$createComponent2(Show2, {
1726
1808
  get when() {
1727
1809
  return track();
1728
1810
  },
@@ -1741,7 +1823,7 @@ function SidebarPlayer(props) {
1741
1823
  _$setProp2(_el$21, "gap", 0);
1742
1824
  _$insertNode2(_el$22, _el$23);
1743
1825
  _$insert3(_el$23, () => sanitizeTerminalText(t2().name, "Unknown track"));
1744
- _$insert3(_el$21, _$createComponent2(Show, {
1826
+ _$insert3(_el$21, _$createComponent2(Show2, {
1745
1827
  get when() {
1746
1828
  return t2().artists;
1747
1829
  },
@@ -1752,7 +1834,7 @@ function SidebarPlayer(props) {
1752
1834
  return _el$24;
1753
1835
  }
1754
1836
  }), null);
1755
- _$insert3(_el$21, _$createComponent2(Show, {
1837
+ _$insert3(_el$21, _$createComponent2(Show2, {
1756
1838
  get when() {
1757
1839
  return t2().album;
1758
1840
  },
@@ -1778,7 +1860,7 @@ function SidebarPlayer(props) {
1778
1860
  bars: 24,
1779
1861
  variant: "hero"
1780
1862
  }));
1781
- _$insert3(_el$1, _$createComponent2(Show, {
1863
+ _$insert3(_el$1, _$createComponent2(Show2, {
1782
1864
  get when() {
1783
1865
  return _$memo(() => !!track())() && duration() > 0;
1784
1866
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naxodev/opencode-music-player",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "macOS system Now Playing controls for the OpenCode 2 TUI",
5
5
  "license": "MIT",
6
6
  "author": {