@naxodev/opencode-music-player 0.5.1 → 0.5.3

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 +138 -67
  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}`;
@@ -840,25 +895,14 @@ function kittyImageId(key) {
840
895
  }
841
896
  function writeGraphics(renderer, data) {
842
897
  const target = renderer;
843
- if (!target.stdout || typeof target.realStdoutWrite !== "function")
898
+ if (typeof target.writeOut !== "function")
844
899
  return false;
900
+ const commands = typeof data === "string" ? [data] : data;
845
901
  const output = (value) => process.env.TMUX && !process.env.HERDR_ENV ? tmuxPassthrough(value) : value;
846
- let cursorSaved = false;
847
902
  try {
848
- for (const command of typeof data === "string" ? [data] : data) {
849
- if (command.includes("\x1B7"))
850
- cursorSaved = true;
851
- target.realStdoutWrite.call(target.stdout, output(command));
852
- if (command.includes("\x1B8"))
853
- cursorSaved = false;
854
- }
903
+ target.writeOut.call(renderer, commands.map(output).join(""));
855
904
  return true;
856
905
  } catch {
857
- if (cursorSaved) {
858
- try {
859
- target.realStdoutWrite.call(target.stdout, output("\x1B8"));
860
- } catch {}
861
- }
862
906
  return false;
863
907
  }
864
908
  }
@@ -986,6 +1030,8 @@ function AlbumArtwork(props) {
986
1030
  let container;
987
1031
  let paintPending = false;
988
1032
  let disposed = false;
1033
+ const [nativeVisible, setNativeVisible] = createSignal(false);
1034
+ const imageDimensions = createMemo(() => pngDimensions(props.artwork.png_base64));
989
1035
  const applyAction = (action) => {
990
1036
  const renderer2 = props.context.renderer;
991
1037
  switch (action.type) {
@@ -1034,23 +1080,39 @@ function AlbumArtwork(props) {
1034
1080
  const imageId = imageIdForArtwork(props.artwork.id);
1035
1081
  const x = slot ? slot.screenX + offset.x : 0;
1036
1082
  const y = slot ? slot.screenY + offset.y : 0;
1037
- const width = slot?.width ?? 0;
1038
- const height = slot?.height ?? 0;
1083
+ const image = imageDimensions();
1084
+ const resolution = renderer.resolution;
1085
+ const geometry = slot && image && resolution ? fitNativeArtworkGeometry({
1086
+ x,
1087
+ y,
1088
+ width: slot.width,
1089
+ height: slot.height
1090
+ }, image, {
1091
+ width: Math.floor(resolution.width / renderer.terminalWidth),
1092
+ height: Math.floor(resolution.height / renderer.terminalHeight)
1093
+ }) : null;
1039
1094
  const plan = planNativeArtworkPlacement({
1040
1095
  state: runtime.state,
1041
1096
  imageId,
1042
- x,
1043
- y,
1044
- width,
1045
- height,
1097
+ ...geometry ?? {
1098
+ x,
1099
+ y,
1100
+ width: 0,
1101
+ height: 0
1102
+ },
1046
1103
  kittySupported,
1047
- slotValid,
1104
+ slotValid: slotValid && geometry !== null,
1048
1105
  disposed
1049
1106
  });
1050
- if (plan.actions.length === 0)
1107
+ if (plan.actions.length === 0) {
1108
+ setNativeVisible(runtime.state.placement !== null);
1051
1109
  return;
1052
- if (applyPlan(plan))
1110
+ }
1111
+ if (applyPlan(plan)) {
1053
1112
  commitPlan(plan);
1113
+ setNativeVisible(plan.nextState.placement !== null);
1114
+ } else
1115
+ setNativeVisible(false);
1054
1116
  };
1055
1117
  const scheduleNativeImage = () => {
1056
1118
  if (paintPending || disposed)
@@ -1088,27 +1150,36 @@ function AlbumArtwork(props) {
1088
1150
  _$setProp(_el$, "width", 24);
1089
1151
  _$setProp(_el$, "height", 12);
1090
1152
  _$setProp(_el$, "flexDirection", "column");
1153
+ _$setProp(_el$, "alignItems", "center");
1154
+ _$setProp(_el$, "justifyContent", "center");
1091
1155
  _$setProp(_el$, "overflow", "hidden");
1092
- _$insert(_el$, _$createComponent(For, {
1093
- get each() {
1094
- return props.artwork.cells;
1156
+ _$insert(_el$, _$createComponent(Show, {
1157
+ get when() {
1158
+ return !nativeVisible();
1095
1159
  },
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;
1160
+ get children() {
1161
+ return _$createComponent(For, {
1162
+ get each() {
1163
+ return props.artwork.cells;
1164
+ },
1165
+ children: (row) => (() => {
1166
+ var _el$2 = _$createElement("text");
1167
+ _$insert(_el$2, _$createComponent(For, {
1168
+ each: row,
1169
+ children: (cell) => (() => {
1170
+ var _el$3 = _$createElement("span");
1171
+ _$insertNode(_el$3, _$createTextNode(`\u2580`));
1172
+ _$effect((_$p) => _$setProp(_el$3, "style", {
1173
+ fg: cell.upper,
1174
+ bg: cell.lower
1175
+ }, _$p));
1176
+ return _el$3;
1177
+ })()
1178
+ }));
1179
+ return _el$2;
1108
1180
  })()
1109
- }));
1110
- return _el$2;
1111
- })()
1181
+ });
1182
+ }
1112
1183
  }));
1113
1184
  return _el$;
1114
1185
  })();
@@ -1376,7 +1447,7 @@ function CompactPlayer(props) {
1376
1447
  let row;
1377
1448
  let content;
1378
1449
  let allocatedWidth = Number.MAX_SAFE_INTEGER;
1379
- const track = createMemo(() => props.state.player?.track);
1450
+ const track = createMemo2(() => props.state.player?.track);
1380
1451
  const renderLine = () => {
1381
1452
  const current = track();
1382
1453
  if (!current)
@@ -1399,7 +1470,7 @@ function CompactPlayer(props) {
1399
1470
  if (row?.onSizeChange === updateWidth)
1400
1471
  row.onSizeChange = undefined;
1401
1472
  });
1402
- return _$createComponent2(Show, {
1473
+ return _$createComponent2(Show2, {
1403
1474
  get when() {
1404
1475
  return track();
1405
1476
  },
@@ -1580,8 +1651,8 @@ function LiveElapsed(props) {
1580
1651
  })();
1581
1652
  }
1582
1653
  function IconBtn(props) {
1583
- const [hover, setHover] = createSignal(false);
1584
- const [press, setPress] = createSignal(false);
1654
+ const [hover, setHover] = createSignal2(false);
1655
+ const [press, setPress] = createSignal2(false);
1585
1656
  const bg = () => {
1586
1657
  if (press() || props.active) {
1587
1658
  return props.theme.background.action.primary.base;
@@ -1659,10 +1730,10 @@ function SidebarPlayer(props) {
1659
1730
  unsubscribeSession?.();
1660
1731
  presentationListeners.clear();
1661
1732
  });
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()));
1733
+ const player = createMemo2(() => props.state.player);
1734
+ const track = createMemo2(() => player()?.track);
1735
+ const playing = createMemo2(() => !!player()?.is_playing);
1736
+ const duration = createMemo2(() => presentationDuration(track()));
1666
1737
  return (() => {
1667
1738
  var _el$1 = _$createElement3("box"), _el$10 = _$createElement3("box"), _el$11 = _$createElement3("box"), _el$12 = _$createElement3("box"), _el$16 = _$createElement3("box");
1668
1739
  _$insertNode2(_el$1, _el$10);
@@ -1676,7 +1747,7 @@ function SidebarPlayer(props) {
1676
1747
  _$setProp2(_el$1, "paddingBottom", 1);
1677
1748
  _$setProp2(_el$1, "gap", 1);
1678
1749
  _$setProp2(_el$1, "flexShrink", 0);
1679
- _$insert3(_el$1, _$createComponent2(Show, {
1750
+ _$insert3(_el$1, _$createComponent2(Show2, {
1680
1751
  get when() {
1681
1752
  return props.state.error;
1682
1753
  },
@@ -1693,7 +1764,7 @@ function SidebarPlayer(props) {
1693
1764
  _$setProp2(_el$11, "width", 24);
1694
1765
  _$setProp2(_el$11, "height", 12);
1695
1766
  _$setProp2(_el$11, "overflow", "hidden");
1696
- _$insert3(_el$11, _$createComponent2(Show, {
1767
+ _$insert3(_el$11, _$createComponent2(Show2, {
1697
1768
  get when() {
1698
1769
  return track()?.artwork;
1699
1770
  },
@@ -1722,7 +1793,7 @@ function SidebarPlayer(props) {
1722
1793
  }
1723
1794
  })
1724
1795
  }));
1725
- _$insert3(_el$1, _$createComponent2(Show, {
1796
+ _$insert3(_el$1, _$createComponent2(Show2, {
1726
1797
  get when() {
1727
1798
  return track();
1728
1799
  },
@@ -1741,7 +1812,7 @@ function SidebarPlayer(props) {
1741
1812
  _$setProp2(_el$21, "gap", 0);
1742
1813
  _$insertNode2(_el$22, _el$23);
1743
1814
  _$insert3(_el$23, () => sanitizeTerminalText(t2().name, "Unknown track"));
1744
- _$insert3(_el$21, _$createComponent2(Show, {
1815
+ _$insert3(_el$21, _$createComponent2(Show2, {
1745
1816
  get when() {
1746
1817
  return t2().artists;
1747
1818
  },
@@ -1752,7 +1823,7 @@ function SidebarPlayer(props) {
1752
1823
  return _el$24;
1753
1824
  }
1754
1825
  }), null);
1755
- _$insert3(_el$21, _$createComponent2(Show, {
1826
+ _$insert3(_el$21, _$createComponent2(Show2, {
1756
1827
  get when() {
1757
1828
  return t2().album;
1758
1829
  },
@@ -1778,7 +1849,7 @@ function SidebarPlayer(props) {
1778
1849
  bars: 24,
1779
1850
  variant: "hero"
1780
1851
  }));
1781
- _$insert3(_el$1, _$createComponent2(Show, {
1852
+ _$insert3(_el$1, _$createComponent2(Show2, {
1782
1853
  get when() {
1783
1854
  return _$memo(() => !!track())() && duration() > 0;
1784
1855
  },
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.3",
4
4
  "description": "macOS system Now Playing controls for the OpenCode 2 TUI",
5
5
  "license": "MIT",
6
6
  "author": {