coding-usage-bar 0.2.1 → 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.
package/dist/doctor.js CHANGED
@@ -7,7 +7,7 @@ import { isDir, isFile } from "./fs-util.js";
7
7
  import { resolveKimiConfig } from "./kimi.js";
8
8
  import { buildPaths, providerLatestPath } from "./paths.js";
9
9
  import { notificationBackend } from "./notifier.js";
10
- import { isSwiftBarInstalled, swiftBarPluginPath } from "./menubar.js";
10
+ import { isSwiftBarInstalled, isSwiftBarRunning, swiftBarPluginPath } from "./menubar.js";
11
11
  function stableClaudeIngestHint() {
12
12
  const paths = buildPaths();
13
13
  return `printf "%s" "$input" | node "${paths.stateDir}/app/dist/cli.js" ingest claude-statusline >/dev/null`;
@@ -236,12 +236,16 @@ export function runDoctor(options = {}) {
236
236
  });
237
237
  const swiftBarInstalled = isSwiftBarInstalled();
238
238
  const swiftBarPlugin = swiftBarPluginPath(paths);
239
+ const pluginPresent = swiftBarInstalled && isFile(swiftBarPlugin.file);
240
+ const swiftBarRunning = pluginPresent && isSwiftBarRunning();
239
241
  checks.push({
240
242
  name: "Menu bar",
241
- ok: swiftBarInstalled && isFile(swiftBarPlugin.file),
243
+ ok: pluginPresent,
242
244
  message: swiftBarInstalled
243
245
  ? isFile(swiftBarPlugin.file)
244
- ? `SwiftBar plugin installed: ${swiftBarPlugin.file}`
246
+ ? swiftBarRunning
247
+ ? `SwiftBar plugin installed and SwiftBar running: ${swiftBarPlugin.file}`
248
+ : `SwiftBar plugin installed but SwiftBar is not running (menu bar item hidden); run: open -a SwiftBar`
245
249
  : `SwiftBar found; run coding-usage-bar menubar install`
246
250
  : "SwiftBar not found; install SwiftBar, then run coding-usage-bar menubar install",
247
251
  });
@@ -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.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare function isSwiftBarInstalled(): boolean;
9
9
  export declare function ensureSwiftBarInstalled(options?: {
10
10
  dryRun?: boolean;
11
11
  }): string[];
12
+ export declare function isSwiftBarRunning(): boolean;
12
13
  export declare function openSwiftBar(): string[];
13
14
  export declare function addSwiftBarToLoginItems(options?: {
14
15
  dryRun?: boolean;
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
  }
@@ -333,25 +182,69 @@ export function ensureSwiftBarInstalled(options = {}) {
333
182
  });
334
183
  return ["Installed SwiftBar with Homebrew cask."];
335
184
  }
185
+ export function isSwiftBarRunning() {
186
+ try {
187
+ execFileSync("pgrep", ["-x", "SwiftBar"], { stdio: "ignore" });
188
+ return true;
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ // Poll until SwiftBar reaches the desired running state, or the timeout elapses.
195
+ // Synchronous on purpose: openSwiftBar runs inside the synchronous installer flow.
196
+ function waitForSwiftBar(wantRunning, timeoutMs) {
197
+ const deadline = Date.now() + timeoutMs;
198
+ while (Date.now() < deadline) {
199
+ if (isSwiftBarRunning() === wantRunning) {
200
+ return true;
201
+ }
202
+ try {
203
+ execFileSync("sleep", ["0.2"], { stdio: "ignore" });
204
+ }
205
+ catch {
206
+ // sleep should not fail; fall through to re-check.
207
+ }
208
+ }
209
+ return isSwiftBarRunning() === wantRunning;
210
+ }
336
211
  export function openSwiftBar() {
337
212
  const appPath = swiftBarAppPath();
338
213
  if (!appPath) {
339
214
  return ["SwiftBar is not installed; skipping launch."];
340
215
  }
341
- try {
342
- execFileSync("osascript", ["-e", 'quit app "SwiftBar"'], { stdio: "ignore" });
343
- }
344
- catch {
345
- // SwiftBar may not be running yet.
216
+ const messages = [];
217
+ // 1. Quit and wait until the old instance is actually gone. `quit app` is
218
+ // async; launching again before it finishes terminating can coalesce into
219
+ // a no-op, and the visibility cleanup below only sticks while SwiftBar is
220
+ // not running (cfprefsd lets a live SwiftBar rewrite the keys on quit).
221
+ if (isSwiftBarRunning()) {
222
+ try {
223
+ execFileSync("osascript", ["-e", 'quit app "SwiftBar"'], { stdio: "ignore" });
224
+ }
225
+ catch {
226
+ // Ignore; the wait below reports the real state.
227
+ }
228
+ waitForSwiftBar(false, 5000);
346
229
  }
230
+ // 2. Clear any stale "hidden" status-item cache while SwiftBar is stopped so
231
+ // the change survives the next launch instead of being clobbered.
232
+ messages.push(...clearSwiftBarStatusItemVisibility());
233
+ // 3. Launch and verify the process actually comes up before claiming success.
347
234
  try {
348
235
  execFileSync("open", [appPath], { stdio: "ignore" });
349
- execFileSync("sleep", ["1"], { stdio: "ignore" });
350
- return ["Opened SwiftBar.", ...clearSwiftBarStatusItemVisibility()];
351
236
  }
352
237
  catch (error) {
353
- return [`SwiftBar installed, but launch failed: ${error instanceof Error ? error.message : String(error)}`];
238
+ messages.push(`SwiftBar installed, but launch failed: ${error instanceof Error ? error.message : String(error)}`);
239
+ return messages;
240
+ }
241
+ if (waitForSwiftBar(true, 8000)) {
242
+ messages.push("Opened SwiftBar.");
354
243
  }
244
+ else {
245
+ messages.push("Launched SwiftBar, but it did not start within 8s. Open SwiftBar manually; if macOS prompts for Automation/Accessibility permission, allow it.");
246
+ }
247
+ return messages;
355
248
  }
356
249
  export function addSwiftBarToLoginItems(options = {}) {
357
250
  const dryRun = options.dryRun ?? false;
@@ -515,11 +408,7 @@ function titleImageValue(providers) {
515
408
  return titleImageCache.get(cacheKey);
516
409
  }
517
410
  try {
518
- const output = execFileSync("/usr/bin/osascript", ["-l", "JavaScript", "-e", TITLE_IMAGE_SCRIPT, cacheKey], {
519
- encoding: "utf8",
520
- timeout: 2500,
521
- }).trim();
522
- const images = JSON.parse(output);
411
+ const images = renderTitleImage(payload);
523
412
  const value = images.light?.image && images.dark?.image && images.light.width && images.light.height
524
413
  ? {
525
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.1",
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",