coding-usage-bar 0.2.2 → 0.2.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.
@@ -0,0 +1,67 @@
1
+ export interface TitleSegment {
2
+ provider: string;
3
+ bars: Array<{
4
+ pct: number;
5
+ lightColor: string;
6
+ darkColor: string;
7
+ }>;
8
+ iconPath: string | null;
9
+ iconPathDark: string | null;
10
+ }
11
+ export interface TitleVariant {
12
+ dividerColor: string;
13
+ dividerAlpha: number;
14
+ barBgColor: string;
15
+ barBgAlpha: number;
16
+ }
17
+ export interface TitlePayload {
18
+ segments: TitleSegment[];
19
+ scale: number;
20
+ height: number;
21
+ minWidth: number;
22
+ paddingX: number;
23
+ iconSize: number;
24
+ iconBarGap: number;
25
+ segmentGap: number;
26
+ barWidth: number;
27
+ barHeight: number;
28
+ barGap: number;
29
+ barRadius: number;
30
+ barMinFill: number;
31
+ light: TitleVariant;
32
+ dark: TitleVariant;
33
+ }
34
+ interface RenderedImage {
35
+ image: string;
36
+ width: number;
37
+ height: number;
38
+ }
39
+ export declare function renderTitleImage(payload: TitlePayload): {
40
+ light: RenderedImage;
41
+ dark: RenderedImage;
42
+ };
43
+ export interface DropdownPayload {
44
+ bars: Array<{
45
+ pct: number;
46
+ lightColor: string;
47
+ darkColor: string;
48
+ }>;
49
+ scale: number;
50
+ barWidth: number;
51
+ barHeight: number;
52
+ barRadius: number;
53
+ light: {
54
+ barBgColor: string;
55
+ barBgAlpha: number;
56
+ };
57
+ dark: {
58
+ barBgColor: string;
59
+ barBgAlpha: number;
60
+ };
61
+ }
62
+ export declare function renderDropdownBars(payload: DropdownPayload): {
63
+ images: string[];
64
+ width: number;
65
+ height: number;
66
+ };
67
+ export {};
@@ -0,0 +1,255 @@
1
+ import fs from "node:fs";
2
+ import { encodePNG, decodePNG } from "./png.js";
3
+ // Pure-JS reimplementation of the menu bar image rendering that previously ran
4
+ // through `osascript -l JavaScript` + AppKit. Spawning AppKit-linked processes
5
+ // connects to the WindowServer and stalls the whole display for 1-2s on every
6
+ // SwiftBar refresh; drawing the pixels in-process here never touches the
7
+ // WindowServer, so there is zero UI freeze. Geometry, colors and the y-up
8
+ // (bottom-left origin) coordinate system mirror the old AppKit code exactly so
9
+ // the rendered images stay visually identical.
10
+ const SUPERSAMPLE = 4; // 4x4 coverage sampling for anti-aliased rounded corners
11
+ function parseHex(hex) {
12
+ const v = hex.replace("#", "");
13
+ return {
14
+ r: parseInt(v.slice(0, 2), 16) / 255,
15
+ g: parseInt(v.slice(2, 4), 16) / 255,
16
+ b: parseInt(v.slice(4, 6), 16) / 255,
17
+ };
18
+ }
19
+ class Canvas {
20
+ width;
21
+ height;
22
+ data; // straight-alpha RGBA, 0..1
23
+ constructor(width, height) {
24
+ this.width = width;
25
+ this.height = height;
26
+ this.data = new Float32Array(width * height * 4);
27
+ }
28
+ // Source-over compositing of a single pixel using straight alpha.
29
+ composite(px, py, color, alpha) {
30
+ if (alpha <= 0 || px < 0 || py < 0 || px >= this.width || py >= this.height) {
31
+ return;
32
+ }
33
+ const idx = (py * this.width + px) * 4;
34
+ const dr = this.data[idx];
35
+ const dg = this.data[idx + 1];
36
+ const db = this.data[idx + 2];
37
+ const da = this.data[idx + 3];
38
+ const outA = alpha + da * (1 - alpha);
39
+ if (outA <= 0) {
40
+ this.data[idx] = this.data[idx + 1] = this.data[idx + 2] = this.data[idx + 3] = 0;
41
+ return;
42
+ }
43
+ this.data[idx] = (color.r * alpha + dr * da * (1 - alpha)) / outA;
44
+ this.data[idx + 1] = (color.g * alpha + dg * da * (1 - alpha)) / outA;
45
+ this.data[idx + 2] = (color.b * alpha + db * da * (1 - alpha)) / outA;
46
+ this.data[idx + 3] = outA;
47
+ }
48
+ // Fill a rounded rectangle expressed in AppKit coordinates (origin bottom-left,
49
+ // y increases upward). r is clamped to half the smaller side, matching
50
+ // CGPathCreateWithRoundedRect.
51
+ fillRoundedRect(ax, ay, aw, ah, radius, color, alpha) {
52
+ const r = Math.min(radius, aw / 2, ah / 2);
53
+ const cx = ax + aw / 2;
54
+ const cy = ay + ah / 2;
55
+ const halfW = aw / 2 - r;
56
+ const halfH = ah / 2 - r;
57
+ const H = this.height;
58
+ const pxMin = Math.max(0, Math.floor(ax));
59
+ const pxMax = Math.min(this.width, Math.ceil(ax + aw));
60
+ const pyMin = Math.max(0, Math.floor(H - (ay + ah)));
61
+ const pyMax = Math.min(H, Math.ceil(H - ay));
62
+ for (let py = pyMin; py < pyMax; py++) {
63
+ for (let px = pxMin; px < pxMax; px++) {
64
+ let hits = 0;
65
+ for (let j = 0; j < SUPERSAMPLE; j++) {
66
+ for (let i = 0; i < SUPERSAMPLE; i++) {
67
+ const sx = px + (i + 0.5) / SUPERSAMPLE;
68
+ const sPngY = py + (j + 0.5) / SUPERSAMPLE;
69
+ const sy = H - sPngY; // back to AppKit y-up
70
+ const qx = Math.abs(sx - cx) - halfW;
71
+ const qy = Math.abs(sy - cy) - halfH;
72
+ const dxo = Math.max(qx, 0);
73
+ const dyo = Math.max(qy, 0);
74
+ const dist = Math.sqrt(dxo * dxo + dyo * dyo) + Math.min(Math.max(qx, qy), 0) - r;
75
+ if (dist <= 0)
76
+ hits++;
77
+ }
78
+ }
79
+ if (hits > 0) {
80
+ this.composite(px, py, color, alpha * (hits / (SUPERSAMPLE * SUPERSAMPLE)));
81
+ }
82
+ }
83
+ }
84
+ }
85
+ // Draw an RGBA source image upright into an AppKit-space rect (bottom-left
86
+ // origin), bilinearly scaled to `size` x `size`. Mirrors NSImage.drawInRect.
87
+ drawIcon(icon, ax, ay, size) {
88
+ const H = this.height;
89
+ const pxMin = Math.max(0, Math.floor(ax));
90
+ const pxMax = Math.min(this.width, Math.ceil(ax + size));
91
+ const pyMin = Math.max(0, Math.floor(H - (ay + size)));
92
+ const pyMax = Math.min(H, Math.ceil(H - ay));
93
+ const iw = icon.width;
94
+ const ih = icon.height;
95
+ for (let py = pyMin; py < pyMax; py++) {
96
+ for (let px = pxMin; px < pxMax; px++) {
97
+ const cxA = px + 0.5;
98
+ const cyA = H - (py + 0.5);
99
+ const u = (cxA - ax) / size; // 0..1 left -> right
100
+ const v = (ay + size - cyA) / size; // 0..1 top -> bottom
101
+ if (u < 0 || u >= 1 || v < 0 || v >= 1)
102
+ continue;
103
+ const sample = bilinear(icon.data, iw, ih, u * iw - 0.5, v * ih - 0.5);
104
+ if (sample.a > 0) {
105
+ this.composite(px, py, { r: sample.r, g: sample.g, b: sample.b }, sample.a);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ toPNGBase64() {
111
+ const out = new Uint8Array(this.width * this.height * 4);
112
+ for (let i = 0; i < this.width * this.height; i++) {
113
+ out[i * 4] = Math.round(clamp01(this.data[i * 4]) * 255);
114
+ out[i * 4 + 1] = Math.round(clamp01(this.data[i * 4 + 1]) * 255);
115
+ out[i * 4 + 2] = Math.round(clamp01(this.data[i * 4 + 2]) * 255);
116
+ out[i * 4 + 3] = Math.round(clamp01(this.data[i * 4 + 3]) * 255);
117
+ }
118
+ return encodePNG(out, this.width, this.height).toString("base64");
119
+ }
120
+ }
121
+ function clamp01(v) {
122
+ return v < 0 ? 0 : v > 1 ? 1 : v;
123
+ }
124
+ function bilinear(data, w, h, fx, fy) {
125
+ const x0 = Math.floor(fx);
126
+ const y0 = Math.floor(fy);
127
+ const tx = fx - x0;
128
+ const ty = fy - y0;
129
+ const at = (x, y) => {
130
+ const cx = x < 0 ? 0 : x >= w ? w - 1 : x;
131
+ const cy = y < 0 ? 0 : y >= h ? h - 1 : y;
132
+ const idx = (cy * w + cx) * 4;
133
+ return {
134
+ r: data[idx] / 255,
135
+ g: data[idx + 1] / 255,
136
+ b: data[idx + 2] / 255,
137
+ a: data[idx + 3] / 255,
138
+ };
139
+ };
140
+ const c00 = at(x0, y0);
141
+ const c10 = at(x0 + 1, y0);
142
+ const c01 = at(x0, y0 + 1);
143
+ const c11 = at(x0 + 1, y0 + 1);
144
+ const lerp = (a, b, t) => a + (b - a) * t;
145
+ return {
146
+ r: lerp(lerp(c00.r, c10.r, tx), lerp(c01.r, c11.r, tx), ty),
147
+ g: lerp(lerp(c00.g, c10.g, tx), lerp(c01.g, c11.g, tx), ty),
148
+ b: lerp(lerp(c00.b, c10.b, tx), lerp(c01.b, c11.b, tx), ty),
149
+ a: lerp(lerp(c00.a, c10.a, tx), lerp(c01.a, c11.a, tx), ty),
150
+ };
151
+ }
152
+ const iconCache = new Map();
153
+ function loadIcon(iconPath) {
154
+ if (iconCache.has(iconPath)) {
155
+ return iconCache.get(iconPath) ?? null;
156
+ }
157
+ let decoded = null;
158
+ try {
159
+ decoded = decodePNG(fs.readFileSync(iconPath));
160
+ }
161
+ catch {
162
+ decoded = null;
163
+ }
164
+ iconCache.set(iconPath, decoded);
165
+ return decoded;
166
+ }
167
+ function drawTitle(payload, variant, mode) {
168
+ const scale = payload.scale || 1;
169
+ const height = payload.height * scale;
170
+ const iconSize = payload.iconSize * scale;
171
+ const paddingX = payload.paddingX * scale;
172
+ const iconBarGap = payload.iconBarGap * scale;
173
+ const segmentGap = payload.segmentGap * scale;
174
+ const barWidth = payload.barWidth * scale;
175
+ const barHeight = payload.barHeight * scale;
176
+ const barGap = payload.barGap * scale;
177
+ const barRadius = payload.barRadius * scale;
178
+ const barMinFill = (payload.barMinFill || barRadius * 2) * scale;
179
+ const dividerW = 1 * scale;
180
+ let width = paddingX * 2;
181
+ payload.segments.forEach((seg, idx) => {
182
+ if (idx > 0)
183
+ width += segmentGap + dividerW + segmentGap;
184
+ width += (seg.iconPath ? iconSize + iconBarGap : 0) + barWidth;
185
+ });
186
+ width = Math.max(payload.minWidth * scale, width);
187
+ const canvas = new Canvas(Math.round(width), Math.round(height));
188
+ const dividerColor = parseHex(variant.dividerColor);
189
+ let x = paddingX;
190
+ const iconY = Math.floor((height - iconSize) / 2);
191
+ payload.segments.forEach((seg, idx) => {
192
+ if (idx > 0) {
193
+ x += segmentGap;
194
+ canvas.fillRoundedRect(x, Math.floor(height * 0.2), dividerW, Math.floor(height * 0.6), 0, dividerColor, variant.dividerAlpha);
195
+ x += dividerW + segmentGap;
196
+ }
197
+ const iconFile = mode === "dark" && seg.iconPathDark ? seg.iconPathDark : seg.iconPath;
198
+ if (iconFile) {
199
+ const icon = loadIcon(iconFile);
200
+ if (icon) {
201
+ canvas.drawIcon(icon, x, iconY, iconSize);
202
+ }
203
+ x += iconSize + iconBarGap;
204
+ }
205
+ const bars = seg.bars || [];
206
+ const totalH = bars.length * barHeight + Math.max(0, bars.length - 1) * barGap;
207
+ let barY = Math.floor((height + totalH) / 2) - barHeight;
208
+ const barBgColor = parseHex(variant.barBgColor);
209
+ for (let i = 0; i < bars.length; i++) {
210
+ canvas.fillRoundedRect(x, barY, barWidth, barHeight, barRadius, barBgColor, variant.barBgAlpha);
211
+ if (bars[i].pct > 0) {
212
+ const pct = Math.min(bars[i].pct, 100);
213
+ const fw = Math.max(barMinFill, Math.round((barWidth * pct) / 100));
214
+ const fillColor = parseHex(mode === "dark" ? bars[i].darkColor : bars[i].lightColor);
215
+ canvas.fillRoundedRect(x, barY, fw, barHeight, barRadius, fillColor, 1);
216
+ }
217
+ barY -= barHeight + barGap;
218
+ }
219
+ x += barWidth;
220
+ });
221
+ return {
222
+ image: canvas.toPNGBase64(),
223
+ width: Math.ceil(width / scale),
224
+ height: payload.height,
225
+ };
226
+ }
227
+ export function renderTitleImage(payload) {
228
+ return {
229
+ light: drawTitle(payload, payload.light, "light"),
230
+ dark: drawTitle(payload, payload.dark, "dark"),
231
+ };
232
+ }
233
+ function makeBar(bw, bh, br, pct, fill, bgColor, bgAlpha) {
234
+ const canvas = new Canvas(bw, bh);
235
+ canvas.fillRoundedRect(0, 0, bw, bh, br, bgColor, bgAlpha);
236
+ if (pct > 0) {
237
+ const fw = Math.max(br * 2, Math.round((bw * Math.min(pct, 100)) / 100));
238
+ canvas.fillRoundedRect(0, 0, fw, bh, br, fill, 1);
239
+ }
240
+ return canvas.toPNGBase64();
241
+ }
242
+ export function renderDropdownBars(payload) {
243
+ const s = payload.scale || 2;
244
+ const bw = payload.barWidth * s;
245
+ const bh = payload.barHeight * s;
246
+ const br = payload.barRadius * s;
247
+ const lightBg = parseHex(payload.light.barBgColor);
248
+ const darkBg = parseHex(payload.dark.barBgColor);
249
+ const images = payload.bars.map((bar) => {
250
+ const light = makeBar(bw, bh, br, bar.pct, parseHex(bar.lightColor), lightBg, payload.light.barBgAlpha);
251
+ const dark = makeBar(bw, bh, br, bar.pct, parseHex(bar.darkColor), darkBg, payload.dark.barBgAlpha);
252
+ return `${light},${dark}`;
253
+ });
254
+ return { images, width: payload.barWidth, height: payload.barHeight };
255
+ }
package/dist/menubar.js CHANGED
@@ -7,6 +7,7 @@ import { formatDurationUntil, formatProviderLabel } from "./format.js";
7
7
  import { buildPaths } from "./paths.js";
8
8
  import { stableNodeExecutable } from "./node-runtime.js";
9
9
  import { loadDisplayStatusSnapshot } from "./runtime.js";
10
+ import { renderTitleImage, renderDropdownBars } from "./menubar-render.js";
10
11
  const MARKER = "coding-usage-bar managed SwiftBar plugin";
11
12
  const SWIFTBAR_APP_PATHS = ["/Applications/SwiftBar.app", path.join(process.env.HOME ?? "", "Applications", "SwiftBar.app")];
12
13
  const STATE_LABEL = {
@@ -65,154 +66,6 @@ const ASSET_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..
65
66
  const imageCache = new Map();
66
67
  const TITLE_IMAGE_SCALE = 2;
67
68
  const titleImageCache = new Map();
68
- const TITLE_IMAGE_SCRIPT = `
69
- ObjC.import('AppKit');
70
- ObjC.import('CoreGraphics');
71
-
72
- function drawImage(payload, variant, mode) {
73
- var scale = payload.scale || 1;
74
- var height = payload.height * scale;
75
- var iconSize = payload.iconSize * scale;
76
- var paddingX = payload.paddingX * scale;
77
- var iconBarGap = payload.iconBarGap * scale;
78
- var segmentGap = payload.segmentGap * scale;
79
- var barWidth = payload.barWidth * scale;
80
- var barHeight = payload.barHeight * scale;
81
- var barGap = payload.barGap * scale;
82
- var barRadius = payload.barRadius * scale;
83
- var barMinFill = (payload.barMinFill || barRadius * 2) * scale;
84
- var dividerW = 1 * scale;
85
-
86
- var width = paddingX * 2;
87
- payload.segments.forEach(function(seg, idx) {
88
- if (idx > 0) width += segmentGap + dividerW + segmentGap;
89
- width += (seg.iconPath ? iconSize + iconBarGap : 0) + barWidth;
90
- });
91
- width = Math.max(payload.minWidth * scale, width);
92
-
93
- var rep = $.NSBitmapImageRep.alloc.initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBitmapFormatBytesPerRowBitsPerPixel(
94
- null, width, height, 8, 4, true, false, $.NSDeviceRGBColorSpace, $.NSBitmapFormatAlphaPremultipliedLast, 0, 0
95
- );
96
- var nsctx = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(rep);
97
- $.NSGraphicsContext.setCurrentContext(nsctx);
98
- nsctx.setShouldAntialias(true);
99
- nsctx.setImageInterpolation($.NSImageInterpolationHigh);
100
- var cg = nsctx.CGContext;
101
-
102
- function setFill(hex, alpha) {
103
- var v = hex.replace('#', '');
104
- $.CGContextSetRGBFillColor(cg,
105
- parseInt(v.slice(0, 2), 16) / 255,
106
- parseInt(v.slice(2, 4), 16) / 255,
107
- parseInt(v.slice(4, 6), 16) / 255,
108
- alpha !== undefined ? alpha : 1);
109
- }
110
-
111
- function fillRoundedRect(rx, ry, rw, rh, rr) {
112
- var p = $.CGPathCreateWithRoundedRect($.CGRectMake(rx, ry, rw, rh), rr, rr, null);
113
- $.CGContextAddPath(cg, p);
114
- $.CGContextFillPath(cg);
115
- }
116
-
117
- var x = paddingX;
118
- var iconY = Math.floor((height - iconSize) / 2);
119
-
120
- payload.segments.forEach(function(seg, idx) {
121
- if (idx > 0) {
122
- x += segmentGap;
123
- setFill(variant.dividerColor, variant.dividerAlpha);
124
- $.CGContextFillRect(cg, $.CGRectMake(x, Math.floor(height * 0.2), dividerW, Math.floor(height * 0.6)));
125
- x += dividerW + segmentGap;
126
- }
127
- var iconFile = (mode === 'dark' && seg.iconPathDark) ? seg.iconPathDark : seg.iconPath;
128
- if (iconFile) {
129
- var icon = $.NSImage.alloc.initWithContentsOfFile($(iconFile));
130
- if (icon) {
131
- icon.drawInRectFromRectOperationFraction(
132
- $.NSMakeRect(x, iconY, iconSize, iconSize),
133
- $.NSZeroRect, $.NSCompositingOperationSourceOver, 1
134
- );
135
- }
136
- x += iconSize + iconBarGap;
137
- }
138
- var bars = seg.bars || [];
139
- var totalH = bars.length * barHeight + Math.max(0, bars.length - 1) * barGap;
140
- var barY = Math.floor((height + totalH) / 2) - barHeight;
141
- for (var i = 0; i < bars.length; i++) {
142
- setFill(variant.barBgColor, variant.barBgAlpha);
143
- fillRoundedRect(x, barY, barWidth, barHeight, barRadius);
144
- if (bars[i].pct > 0) {
145
- var pct = Math.min(bars[i].pct, 100);
146
- var fw = Math.max(barMinFill, Math.round(barWidth * pct / 100));
147
- setFill(bars[i][mode + 'Color'], 1);
148
- fillRoundedRect(x, barY, fw, barHeight, barRadius);
149
- }
150
- barY -= barHeight + barGap;
151
- }
152
- x += barWidth;
153
- });
154
-
155
- var png = rep.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $.NSDictionary.alloc.init);
156
- return {
157
- image: ObjC.unwrap(png.base64EncodedStringWithOptions(0)),
158
- width: Math.ceil(width / scale),
159
- height: payload.height,
160
- };
161
- }
162
-
163
- function run(argv) {
164
- var payload = JSON.parse(argv[0]);
165
- return JSON.stringify({
166
- light: drawImage(payload, payload.light, 'light'),
167
- dark: drawImage(payload, payload.dark, 'dark'),
168
- });
169
- }
170
- `;
171
- const DROPDOWN_BAR_SCRIPT = `
172
- ObjC.import('AppKit');
173
- ObjC.import('CoreGraphics');
174
-
175
- function parseHex(hex) {
176
- var v = hex.replace('#', '');
177
- return [parseInt(v.slice(0,2),16)/255, parseInt(v.slice(2,4),16)/255, parseInt(v.slice(4,6),16)/255];
178
- }
179
-
180
- function makeBar(bw, bh, br, pct, fc, bgHex, bgAlpha) {
181
- var rep = $.NSBitmapImageRep.alloc.initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBitmapFormatBytesPerRowBitsPerPixel(
182
- null, bw, bh, 8, 4, true, false, $.NSDeviceRGBColorSpace, $.NSBitmapFormatAlphaPremultipliedLast, 0, 0);
183
- var nsctx = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(rep);
184
- $.NSGraphicsContext.setCurrentContext(nsctx);
185
- nsctx.setShouldAntialias(true);
186
- var cg = nsctx.CGContext;
187
- var bg = parseHex(bgHex);
188
- $.CGContextSetRGBFillColor(cg, bg[0], bg[1], bg[2], bgAlpha);
189
- var bp = $.CGPathCreateWithRoundedRect($.CGRectMake(0,0,bw,bh), br, br, null);
190
- $.CGContextAddPath(cg, bp); $.CGContextFillPath(cg);
191
- if (pct > 0) {
192
- var fw = Math.max(br*2, Math.round(bw * Math.min(pct,100) / 100));
193
- $.CGContextSetRGBFillColor(cg, fc[0], fc[1], fc[2], 1);
194
- var fp = $.CGPathCreateWithRoundedRect($.CGRectMake(0,0,fw,bh), br, br, null);
195
- $.CGContextAddPath(cg, fp); $.CGContextFillPath(cg);
196
- }
197
- var png = rep.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $.NSDictionary.alloc.init);
198
- return ObjC.unwrap(png.base64EncodedStringWithOptions(0));
199
- }
200
-
201
- function run(argv) {
202
- var p = JSON.parse(argv[0]);
203
- var s = p.scale || 2;
204
- var bw = p.barWidth * s, bh = p.barHeight * s, br = p.barRadius * s;
205
- var results = [];
206
- for (var i = 0; i < p.bars.length; i++) {
207
- var bar = p.bars[i];
208
- var lc = parseHex(bar.lightColor), dc = parseHex(bar.darkColor);
209
- var li = makeBar(bw, bh, br, bar.pct, lc, p.light.barBgColor, p.light.barBgAlpha);
210
- var di = makeBar(bw, bh, br, bar.pct, dc, p.dark.barBgColor, p.dark.barBgAlpha);
211
- results.push(li + ',' + di);
212
- }
213
- return JSON.stringify({images: results, width: p.barWidth, height: p.barHeight});
214
- }
215
- `;
216
69
  const dropdownBarCache = new Map();
217
70
  function renderDropdownBarImages(bars) {
218
71
  if (bars.length === 0) {
@@ -233,11 +86,7 @@ function renderDropdownBarImages(bars) {
233
86
  return cached ? cached.images.map((img) => ({ image: img, width: cached.width, height: cached.height })) : [];
234
87
  }
235
88
  try {
236
- const output = execFileSync("/usr/bin/osascript", ["-l", "JavaScript", "-e", DROPDOWN_BAR_SCRIPT, cacheKey], {
237
- encoding: "utf8",
238
- timeout: 3000,
239
- }).trim();
240
- const result = JSON.parse(output);
89
+ const result = renderDropdownBars(payload);
241
90
  dropdownBarCache.set(cacheKey, result);
242
91
  return result.images.map((img) => ({ image: img, width: result.width, height: result.height }));
243
92
  }
@@ -559,11 +408,7 @@ function titleImageValue(providers) {
559
408
  return titleImageCache.get(cacheKey);
560
409
  }
561
410
  try {
562
- const output = execFileSync("/usr/bin/osascript", ["-l", "JavaScript", "-e", TITLE_IMAGE_SCRIPT, cacheKey], {
563
- encoding: "utf8",
564
- timeout: 2500,
565
- }).trim();
566
- const images = JSON.parse(output);
411
+ const images = renderTitleImage(payload);
567
412
  const value = images.light?.image && images.dark?.image && images.light.width && images.light.height
568
413
  ? {
569
414
  image: `${images.light.image},${images.dark.image}`,
package/dist/png.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export declare function encodePNG(rgba: Uint8Array, width: number, height: number): Buffer;
2
+ export interface DecodedPNG {
3
+ width: number;
4
+ height: number;
5
+ data: Uint8Array;
6
+ }
7
+ export declare function decodePNG(buffer: Buffer): DecodedPNG;
package/dist/png.js ADDED
@@ -0,0 +1,141 @@
1
+ import zlib from "node:zlib";
2
+ // Minimal zero-dependency PNG codec, scoped to 8-bit RGBA, non-interlaced images.
3
+ // That is the only format this tool renders (menu bar strips) and the only
4
+ // format its provider icon assets use (all 18x18 RGBA8). Keeping the codec tiny
5
+ // preserves the package's zero-runtime-dependency property.
6
+ const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
7
+ const CRC_TABLE = (() => {
8
+ const table = new Uint32Array(256);
9
+ for (let n = 0; n < 256; n++) {
10
+ let c = n;
11
+ for (let k = 0; k < 8; k++) {
12
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
13
+ }
14
+ table[n] = c >>> 0;
15
+ }
16
+ return table;
17
+ })();
18
+ function crc32(buf) {
19
+ let c = 0xffffffff;
20
+ for (let i = 0; i < buf.length; i++) {
21
+ c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
22
+ }
23
+ return (c ^ 0xffffffff) >>> 0;
24
+ }
25
+ function chunk(type, data) {
26
+ const typeBuf = Buffer.from(type, "ascii");
27
+ const len = Buffer.alloc(4);
28
+ len.writeUInt32BE(data.length, 0);
29
+ const crcBuf = Buffer.alloc(4);
30
+ crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
31
+ return Buffer.concat([len, typeBuf, data, crcBuf]);
32
+ }
33
+ export function encodePNG(rgba, width, height) {
34
+ const stride = width * 4;
35
+ // One filter byte (0 = None) per scanline, followed by the raw RGBA row.
36
+ const raw = Buffer.alloc(height * (stride + 1));
37
+ for (let y = 0; y < height; y++) {
38
+ const dst = y * (stride + 1);
39
+ raw[dst] = 0;
40
+ for (let i = 0; i < stride; i++) {
41
+ raw[dst + 1 + i] = rgba[y * stride + i];
42
+ }
43
+ }
44
+ const ihdr = Buffer.alloc(13);
45
+ ihdr.writeUInt32BE(width, 0);
46
+ ihdr.writeUInt32BE(height, 4);
47
+ ihdr[8] = 8; // bit depth
48
+ ihdr[9] = 6; // color type: RGBA
49
+ ihdr[10] = 0; // compression
50
+ ihdr[11] = 0; // filter
51
+ ihdr[12] = 0; // interlace
52
+ const idat = zlib.deflateSync(raw, { level: 9 });
53
+ return Buffer.concat([
54
+ PNG_SIGNATURE,
55
+ chunk("IHDR", ihdr),
56
+ chunk("IDAT", idat),
57
+ chunk("IEND", Buffer.alloc(0)),
58
+ ]);
59
+ }
60
+ function paeth(a, b, c) {
61
+ const p = a + b - c;
62
+ const pa = Math.abs(p - a);
63
+ const pb = Math.abs(p - b);
64
+ const pc = Math.abs(p - c);
65
+ if (pa <= pb && pa <= pc)
66
+ return a;
67
+ if (pb <= pc)
68
+ return b;
69
+ return c;
70
+ }
71
+ export function decodePNG(buffer) {
72
+ if (!buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
73
+ throw new Error("decodePNG: not a PNG file");
74
+ }
75
+ let offset = 8;
76
+ let width = 0;
77
+ let height = 0;
78
+ const idatParts = [];
79
+ while (offset < buffer.length) {
80
+ const len = buffer.readUInt32BE(offset);
81
+ const type = buffer.toString("ascii", offset + 4, offset + 8);
82
+ const dataStart = offset + 8;
83
+ const data = buffer.subarray(dataStart, dataStart + len);
84
+ if (type === "IHDR") {
85
+ width = data.readUInt32BE(0);
86
+ height = data.readUInt32BE(4);
87
+ const bitDepth = data[8];
88
+ const colorType = data[9];
89
+ const interlace = data[12];
90
+ if (bitDepth !== 8 || colorType !== 6 || interlace !== 0) {
91
+ throw new Error(`decodePNG: unsupported PNG (bitDepth=${bitDepth} colorType=${colorType} interlace=${interlace}); only 8-bit RGBA non-interlaced is supported`);
92
+ }
93
+ }
94
+ else if (type === "IDAT") {
95
+ idatParts.push(Buffer.from(data));
96
+ }
97
+ else if (type === "IEND") {
98
+ break;
99
+ }
100
+ offset = dataStart + len + 4; // skip data + CRC
101
+ }
102
+ const inflated = zlib.inflateSync(Buffer.concat(idatParts));
103
+ const bpp = 4;
104
+ const stride = width * bpp;
105
+ const out = new Uint8Array(width * height * bpp);
106
+ let prev = new Uint8Array(stride);
107
+ for (let y = 0; y < height; y++) {
108
+ const filterType = inflated[y * (stride + 1)];
109
+ const rowStart = y * (stride + 1) + 1;
110
+ const cur = new Uint8Array(stride);
111
+ for (let i = 0; i < stride; i++) {
112
+ const rawByte = inflated[rowStart + i];
113
+ const a = i >= bpp ? cur[i - bpp] : 0;
114
+ const b = prev[i];
115
+ const c = i >= bpp ? prev[i - bpp] : 0;
116
+ let value;
117
+ switch (filterType) {
118
+ case 0:
119
+ value = rawByte;
120
+ break;
121
+ case 1:
122
+ value = rawByte + a;
123
+ break;
124
+ case 2:
125
+ value = rawByte + b;
126
+ break;
127
+ case 3:
128
+ value = rawByte + ((a + b) >> 1);
129
+ break;
130
+ case 4:
131
+ value = rawByte + paeth(a, b, c);
132
+ break;
133
+ default: throw new Error(`decodePNG: unknown filter type ${filterType}`);
134
+ }
135
+ cur[i] = value & 0xff;
136
+ }
137
+ out.set(cur, y * stride);
138
+ prev = cur;
139
+ }
140
+ return { width, height, data: out };
141
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-usage-bar",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "macOS menu bar usage monitor for Claude Code, Codex, GLM, DeepSeek, and MiniMax.",
5
5
  "author": "hanzhangzzz",
6
6
  "license": "MIT",