coding-usage-bar 0.1.0
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/LICENSE +21 -0
- package/README.md +220 -0
- package/README.zh-CN.md +53 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/coding-usage-bar-limit.png +0 -0
- package/assets/coding-usage-bar-ok.png +0 -0
- package/assets/coding-usage-bar-over.png +0 -0
- package/assets/coding-usage-bar-under.png +0 -0
- package/assets/provider-claude.png +0 -0
- package/assets/provider-codex.png +0 -0
- package/assets/provider-deepseek.png +0 -0
- package/assets/provider-glm-dark.png +0 -0
- package/assets/provider-glm.png +0 -0
- package/assets/provider-marks.png +0 -0
- package/assets/provider-minimax.png +0 -0
- package/dist/burn.d.ts +3 -0
- package/dist/burn.js +164 -0
- package/dist/card.d.ts +4 -0
- package/dist/card.js +79 -0
- package/dist/claude.d.ts +9 -0
- package/dist/claude.js +141 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +129 -0
- package/dist/codex.d.ts +6 -0
- package/dist/codex.js +128 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +64 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +16 -0
- package/dist/deepseek.d.ts +18 -0
- package/dist/deepseek.js +47 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +225 -0
- package/dist/format.d.ts +9 -0
- package/dist/format.js +92 -0
- package/dist/fs-util.d.ts +6 -0
- package/dist/fs-util.js +39 -0
- package/dist/glm.d.ts +22 -0
- package/dist/glm.js +62 -0
- package/dist/install.d.ts +25 -0
- package/dist/install.js +432 -0
- package/dist/menubar.d.ts +23 -0
- package/dist/menubar.js +880 -0
- package/dist/minimax.d.ts +47 -0
- package/dist/minimax.js +100 -0
- package/dist/node-runtime.d.ts +1 -0
- package/dist/node-runtime.js +29 -0
- package/dist/notifier.d.ts +7 -0
- package/dist/notifier.js +121 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +45 -0
- package/dist/runtime.d.ts +23 -0
- package/dist/runtime.js +244 -0
- package/dist/star.d.ts +15 -0
- package/dist/star.js +117 -0
- package/dist/status.d.ts +14 -0
- package/dist/status.js +65 -0
- package/dist/store.d.ts +4 -0
- package/dist/store.js +29 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +12 -0
- package/dist/usage.js +56 -0
- package/docs/assets/hero.png +0 -0
- package/fixtures/claude/latest.json +20 -0
- package/fixtures/claude/samples.jsonl +3 -0
- package/fixtures/codex/latest.json +20 -0
- package/fixtures/codex/samples.jsonl +3 -0
- package/fixtures/deepseek/latest.json +12 -0
- package/package.json +59 -0
package/dist/menubar.js
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { ensureDir, isFile } from "./fs-util.js";
|
|
6
|
+
import { formatDurationUntil, formatProviderLabel } from "./format.js";
|
|
7
|
+
import { buildPaths } from "./paths.js";
|
|
8
|
+
import { stableNodeExecutable } from "./node-runtime.js";
|
|
9
|
+
import { loadDisplayStatusSnapshot } from "./runtime.js";
|
|
10
|
+
const MARKER = "coding-usage-bar managed SwiftBar plugin";
|
|
11
|
+
const SWIFTBAR_APP_PATHS = ["/Applications/SwiftBar.app", path.join(process.env.HOME ?? "", "Applications", "SwiftBar.app")];
|
|
12
|
+
const STATE_LABEL = {
|
|
13
|
+
RAW: "Learning",
|
|
14
|
+
UNDER_BURN: "Low",
|
|
15
|
+
ON_TRACK: "On Track",
|
|
16
|
+
OVER_BURN: "Fast",
|
|
17
|
+
LIMIT_RISK: "Limit",
|
|
18
|
+
};
|
|
19
|
+
// Local provider marks keep SwiftBar rendering offline; see THIRD_PARTY_NOTICES.md.
|
|
20
|
+
const PROVIDER_ICON_ASSET = {
|
|
21
|
+
claude: "provider-claude.png",
|
|
22
|
+
codex: "provider-codex.png",
|
|
23
|
+
glm: "provider-glm.png",
|
|
24
|
+
deepseek: "provider-deepseek.png",
|
|
25
|
+
minimax: "provider-minimax.png",
|
|
26
|
+
};
|
|
27
|
+
const PROVIDER_ICON_DARK_ASSET = {
|
|
28
|
+
glm: "provider-glm-dark.png",
|
|
29
|
+
};
|
|
30
|
+
const PROVIDER_ICON_FALLBACK = {
|
|
31
|
+
claude: "sparkles",
|
|
32
|
+
codex: "curlybraces.square.fill",
|
|
33
|
+
glm: "cpu",
|
|
34
|
+
deepseek: "bubble.left.and.bubble.right",
|
|
35
|
+
minimax: "waveform.path.ecg",
|
|
36
|
+
};
|
|
37
|
+
const TITLE_ICON_ASSET = "provider-marks.png";
|
|
38
|
+
const ALERT_COLOR = "#D97757,#E8956E";
|
|
39
|
+
const WARNING_COLOR = "#FF9F0A,#FFD60A";
|
|
40
|
+
const OK_COLOR = "#248A3D,#30D158";
|
|
41
|
+
const RAW_COLOR = "#248A3D,#30D158";
|
|
42
|
+
const STATE_COLOR = {
|
|
43
|
+
RAW: RAW_COLOR,
|
|
44
|
+
UNDER_BURN: WARNING_COLOR,
|
|
45
|
+
ON_TRACK: OK_COLOR,
|
|
46
|
+
OVER_BURN: ALERT_COLOR,
|
|
47
|
+
LIMIT_RISK: ALERT_COLOR,
|
|
48
|
+
};
|
|
49
|
+
const STATE_PRIORITY = {
|
|
50
|
+
LIMIT_RISK: 0,
|
|
51
|
+
OVER_BURN: 1,
|
|
52
|
+
UNDER_BURN: 2,
|
|
53
|
+
RAW: 3,
|
|
54
|
+
ON_TRACK: 4,
|
|
55
|
+
};
|
|
56
|
+
const TEXT_COLOR = "#111827,#F9FAFB";
|
|
57
|
+
const MUTED_COLOR = "#6B7280,#A1A1AA";
|
|
58
|
+
const ROW_FONT = "Menlo";
|
|
59
|
+
const TITLE_PROVIDER_ORDER = ["codex", "claude", "glm", "deepseek", "minimax"];
|
|
60
|
+
const TITLE_SEPARATOR = "│";
|
|
61
|
+
const METER_WIDTH = 12;
|
|
62
|
+
const ASSET_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "assets");
|
|
63
|
+
const imageCache = new Map();
|
|
64
|
+
const TITLE_IMAGE_SCALE = 2;
|
|
65
|
+
const titleImageCache = new Map();
|
|
66
|
+
const TITLE_IMAGE_SCRIPT = `
|
|
67
|
+
ObjC.import('AppKit');
|
|
68
|
+
ObjC.import('CoreGraphics');
|
|
69
|
+
|
|
70
|
+
function drawImage(payload, variant, mode) {
|
|
71
|
+
var scale = payload.scale || 1;
|
|
72
|
+
var height = payload.height * scale;
|
|
73
|
+
var iconSize = payload.iconSize * scale;
|
|
74
|
+
var paddingX = payload.paddingX * scale;
|
|
75
|
+
var iconBarGap = payload.iconBarGap * scale;
|
|
76
|
+
var segmentGap = payload.segmentGap * scale;
|
|
77
|
+
var barWidth = payload.barWidth * scale;
|
|
78
|
+
var barHeight = payload.barHeight * scale;
|
|
79
|
+
var barGap = payload.barGap * scale;
|
|
80
|
+
var barRadius = payload.barRadius * scale;
|
|
81
|
+
var barMinFill = (payload.barMinFill || barRadius * 2) * scale;
|
|
82
|
+
var dividerW = 1 * scale;
|
|
83
|
+
|
|
84
|
+
var width = paddingX * 2;
|
|
85
|
+
payload.segments.forEach(function(seg, idx) {
|
|
86
|
+
if (idx > 0) width += segmentGap + dividerW + segmentGap;
|
|
87
|
+
width += (seg.iconPath ? iconSize + iconBarGap : 0) + barWidth;
|
|
88
|
+
});
|
|
89
|
+
width = Math.max(payload.minWidth * scale, width);
|
|
90
|
+
|
|
91
|
+
var rep = $.NSBitmapImageRep.alloc.initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBitmapFormatBytesPerRowBitsPerPixel(
|
|
92
|
+
null, width, height, 8, 4, true, false, $.NSDeviceRGBColorSpace, $.NSBitmapFormatAlphaPremultipliedLast, 0, 0
|
|
93
|
+
);
|
|
94
|
+
var nsctx = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(rep);
|
|
95
|
+
$.NSGraphicsContext.setCurrentContext(nsctx);
|
|
96
|
+
nsctx.setShouldAntialias(true);
|
|
97
|
+
nsctx.setImageInterpolation($.NSImageInterpolationHigh);
|
|
98
|
+
var cg = nsctx.CGContext;
|
|
99
|
+
|
|
100
|
+
function setFill(hex, alpha) {
|
|
101
|
+
var v = hex.replace('#', '');
|
|
102
|
+
$.CGContextSetRGBFillColor(cg,
|
|
103
|
+
parseInt(v.slice(0, 2), 16) / 255,
|
|
104
|
+
parseInt(v.slice(2, 4), 16) / 255,
|
|
105
|
+
parseInt(v.slice(4, 6), 16) / 255,
|
|
106
|
+
alpha !== undefined ? alpha : 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fillRoundedRect(rx, ry, rw, rh, rr) {
|
|
110
|
+
var p = $.CGPathCreateWithRoundedRect($.CGRectMake(rx, ry, rw, rh), rr, rr, null);
|
|
111
|
+
$.CGContextAddPath(cg, p);
|
|
112
|
+
$.CGContextFillPath(cg);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var x = paddingX;
|
|
116
|
+
var iconY = Math.floor((height - iconSize) / 2);
|
|
117
|
+
|
|
118
|
+
payload.segments.forEach(function(seg, idx) {
|
|
119
|
+
if (idx > 0) {
|
|
120
|
+
x += segmentGap;
|
|
121
|
+
setFill(variant.dividerColor, variant.dividerAlpha);
|
|
122
|
+
$.CGContextFillRect(cg, $.CGRectMake(x, Math.floor(height * 0.2), dividerW, Math.floor(height * 0.6)));
|
|
123
|
+
x += dividerW + segmentGap;
|
|
124
|
+
}
|
|
125
|
+
var iconFile = (mode === 'dark' && seg.iconPathDark) ? seg.iconPathDark : seg.iconPath;
|
|
126
|
+
if (iconFile) {
|
|
127
|
+
var icon = $.NSImage.alloc.initWithContentsOfFile($(iconFile));
|
|
128
|
+
if (icon) {
|
|
129
|
+
icon.drawInRectFromRectOperationFraction(
|
|
130
|
+
$.NSMakeRect(x, iconY, iconSize, iconSize),
|
|
131
|
+
$.NSZeroRect, $.NSCompositingOperationSourceOver, 1
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
x += iconSize + iconBarGap;
|
|
135
|
+
}
|
|
136
|
+
var bars = seg.bars || [];
|
|
137
|
+
var totalH = bars.length * barHeight + Math.max(0, bars.length - 1) * barGap;
|
|
138
|
+
var barY = Math.floor((height + totalH) / 2) - barHeight;
|
|
139
|
+
for (var i = 0; i < bars.length; i++) {
|
|
140
|
+
setFill(variant.barBgColor, variant.barBgAlpha);
|
|
141
|
+
fillRoundedRect(x, barY, barWidth, barHeight, barRadius);
|
|
142
|
+
if (bars[i].pct > 0) {
|
|
143
|
+
var pct = Math.min(bars[i].pct, 100);
|
|
144
|
+
var fw = Math.max(barMinFill, Math.round(barWidth * pct / 100));
|
|
145
|
+
setFill(bars[i][mode + 'Color'], 1);
|
|
146
|
+
fillRoundedRect(x, barY, fw, barHeight, barRadius);
|
|
147
|
+
}
|
|
148
|
+
barY -= barHeight + barGap;
|
|
149
|
+
}
|
|
150
|
+
x += barWidth;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
var png = rep.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $.NSDictionary.alloc.init);
|
|
154
|
+
return {
|
|
155
|
+
image: ObjC.unwrap(png.base64EncodedStringWithOptions(0)),
|
|
156
|
+
width: Math.ceil(width / scale),
|
|
157
|
+
height: payload.height,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function run(argv) {
|
|
162
|
+
var payload = JSON.parse(argv[0]);
|
|
163
|
+
return JSON.stringify({
|
|
164
|
+
light: drawImage(payload, payload.light, 'light'),
|
|
165
|
+
dark: drawImage(payload, payload.dark, 'dark'),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
const DROPDOWN_BAR_SCRIPT = `
|
|
170
|
+
ObjC.import('AppKit');
|
|
171
|
+
ObjC.import('CoreGraphics');
|
|
172
|
+
|
|
173
|
+
function parseHex(hex) {
|
|
174
|
+
var v = hex.replace('#', '');
|
|
175
|
+
return [parseInt(v.slice(0,2),16)/255, parseInt(v.slice(2,4),16)/255, parseInt(v.slice(4,6),16)/255];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function makeBar(bw, bh, br, pct, fc, bgHex, bgAlpha) {
|
|
179
|
+
var rep = $.NSBitmapImageRep.alloc.initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBitmapFormatBytesPerRowBitsPerPixel(
|
|
180
|
+
null, bw, bh, 8, 4, true, false, $.NSDeviceRGBColorSpace, $.NSBitmapFormatAlphaPremultipliedLast, 0, 0);
|
|
181
|
+
var nsctx = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(rep);
|
|
182
|
+
$.NSGraphicsContext.setCurrentContext(nsctx);
|
|
183
|
+
nsctx.setShouldAntialias(true);
|
|
184
|
+
var cg = nsctx.CGContext;
|
|
185
|
+
var bg = parseHex(bgHex);
|
|
186
|
+
$.CGContextSetRGBFillColor(cg, bg[0], bg[1], bg[2], bgAlpha);
|
|
187
|
+
var bp = $.CGPathCreateWithRoundedRect($.CGRectMake(0,0,bw,bh), br, br, null);
|
|
188
|
+
$.CGContextAddPath(cg, bp); $.CGContextFillPath(cg);
|
|
189
|
+
if (pct > 0) {
|
|
190
|
+
var fw = Math.max(br*2, Math.round(bw * Math.min(pct,100) / 100));
|
|
191
|
+
$.CGContextSetRGBFillColor(cg, fc[0], fc[1], fc[2], 1);
|
|
192
|
+
var fp = $.CGPathCreateWithRoundedRect($.CGRectMake(0,0,fw,bh), br, br, null);
|
|
193
|
+
$.CGContextAddPath(cg, fp); $.CGContextFillPath(cg);
|
|
194
|
+
}
|
|
195
|
+
var png = rep.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $.NSDictionary.alloc.init);
|
|
196
|
+
return ObjC.unwrap(png.base64EncodedStringWithOptions(0));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function run(argv) {
|
|
200
|
+
var p = JSON.parse(argv[0]);
|
|
201
|
+
var s = p.scale || 2;
|
|
202
|
+
var bw = p.barWidth * s, bh = p.barHeight * s, br = p.barRadius * s;
|
|
203
|
+
var results = [];
|
|
204
|
+
for (var i = 0; i < p.bars.length; i++) {
|
|
205
|
+
var bar = p.bars[i];
|
|
206
|
+
var lc = parseHex(bar.lightColor), dc = parseHex(bar.darkColor);
|
|
207
|
+
var li = makeBar(bw, bh, br, bar.pct, lc, p.light.barBgColor, p.light.barBgAlpha);
|
|
208
|
+
var di = makeBar(bw, bh, br, bar.pct, dc, p.dark.barBgColor, p.dark.barBgAlpha);
|
|
209
|
+
results.push(li + ',' + di);
|
|
210
|
+
}
|
|
211
|
+
return JSON.stringify({images: results, width: p.barWidth, height: p.barHeight});
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
const dropdownBarCache = new Map();
|
|
215
|
+
function renderDropdownBarImages(bars) {
|
|
216
|
+
if (bars.length === 0) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const payload = {
|
|
220
|
+
bars,
|
|
221
|
+
scale: TITLE_IMAGE_SCALE,
|
|
222
|
+
barWidth: 80,
|
|
223
|
+
barHeight: 10,
|
|
224
|
+
barRadius: 3,
|
|
225
|
+
light: { barBgColor: "000000", barBgAlpha: 0.15 },
|
|
226
|
+
dark: { barBgColor: "FFFFFF", barBgAlpha: 0.2 },
|
|
227
|
+
};
|
|
228
|
+
const cacheKey = JSON.stringify(payload);
|
|
229
|
+
if (dropdownBarCache.has(cacheKey)) {
|
|
230
|
+
const cached = dropdownBarCache.get(cacheKey);
|
|
231
|
+
return cached ? cached.images.map((img) => ({ image: img, width: cached.width, height: cached.height })) : [];
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const output = execFileSync("/usr/bin/osascript", ["-l", "JavaScript", "-e", DROPDOWN_BAR_SCRIPT, cacheKey], {
|
|
235
|
+
encoding: "utf8",
|
|
236
|
+
timeout: 3000,
|
|
237
|
+
}).trim();
|
|
238
|
+
const result = JSON.parse(output);
|
|
239
|
+
dropdownBarCache.set(cacheKey, result);
|
|
240
|
+
return result.images.map((img) => ({ image: img, width: result.width, height: result.height }));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
dropdownBarCache.set(cacheKey, null);
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function appCliPath(paths) {
|
|
248
|
+
return path.join(paths.stateDir, "app", "dist", "cli.js");
|
|
249
|
+
}
|
|
250
|
+
const COMPACT_MODE_FILE = "compact-mode";
|
|
251
|
+
function compactModeFilePath(paths) {
|
|
252
|
+
return path.join(paths.stateDir, COMPACT_MODE_FILE);
|
|
253
|
+
}
|
|
254
|
+
export function readCompactMode(paths = buildPaths()) {
|
|
255
|
+
try {
|
|
256
|
+
return fs.existsSync(compactModeFilePath(paths));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function toggleCompactMode(paths = buildPaths()) {
|
|
263
|
+
const filePath = compactModeFilePath(paths);
|
|
264
|
+
if (fs.existsSync(filePath)) {
|
|
265
|
+
fs.rmSync(filePath, { force: true });
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
ensureDir(paths.stateDir);
|
|
269
|
+
fs.writeFileSync(filePath, "");
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
function configuredSwiftBarPluginDir(paths) {
|
|
273
|
+
// Isolation escape hatch: an explicit CODING_USAGE_BAR_PLUGIN_DIR always wins and never
|
|
274
|
+
// reads the global SwiftBar config. Tests set this to a temp dir so running the
|
|
275
|
+
// suite (which exercises uninstall) can never delete the developer's live plugin.
|
|
276
|
+
const override = process.env.CODING_USAGE_BAR_PLUGIN_DIR?.trim();
|
|
277
|
+
if (override) {
|
|
278
|
+
return override;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const configured = execFileSync("defaults", ["read", "com.ameba.SwiftBar", "PluginDirectory"], {
|
|
282
|
+
encoding: "utf8",
|
|
283
|
+
}).trim();
|
|
284
|
+
return configured || paths.swiftBarPluginDir;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return paths.swiftBarPluginDir;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export function swiftBarPluginPath(paths = buildPaths()) {
|
|
291
|
+
const pluginDir = configuredSwiftBarPluginDir(paths);
|
|
292
|
+
return {
|
|
293
|
+
dir: pluginDir,
|
|
294
|
+
file: path.join(pluginDir, "coding-usage-bar.1m.js"),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export function isSwiftBarInstalled() {
|
|
298
|
+
return SWIFTBAR_APP_PATHS.some((appPath) => appPath && fs.existsSync(appPath));
|
|
299
|
+
}
|
|
300
|
+
function swiftBarAppPath() {
|
|
301
|
+
return SWIFTBAR_APP_PATHS.find((appPath) => appPath && fs.existsSync(appPath)) ?? null;
|
|
302
|
+
}
|
|
303
|
+
function brewPath() {
|
|
304
|
+
const candidates = ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"];
|
|
305
|
+
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
|
306
|
+
if (found) {
|
|
307
|
+
return found;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
return execFileSync("which", ["brew"], { encoding: "utf8" }).trim();
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
export function ensureSwiftBarInstalled(options = {}) {
|
|
317
|
+
const dryRun = options.dryRun ?? false;
|
|
318
|
+
if (isSwiftBarInstalled()) {
|
|
319
|
+
return ["SwiftBar already installed."];
|
|
320
|
+
}
|
|
321
|
+
const brew = brewPath();
|
|
322
|
+
if (!brew) {
|
|
323
|
+
return ["SwiftBar is missing and Homebrew is unavailable; install SwiftBar manually, then run coding-usage-bar menubar install."];
|
|
324
|
+
}
|
|
325
|
+
if (dryRun) {
|
|
326
|
+
return [`[dry-run] would install SwiftBar with ${brew} install --cask swiftbar`];
|
|
327
|
+
}
|
|
328
|
+
execFileSync(brew, ["install", "--cask", "swiftbar"], {
|
|
329
|
+
env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" },
|
|
330
|
+
stdio: "inherit",
|
|
331
|
+
});
|
|
332
|
+
return ["Installed SwiftBar with Homebrew cask."];
|
|
333
|
+
}
|
|
334
|
+
export function openSwiftBar() {
|
|
335
|
+
const appPath = swiftBarAppPath();
|
|
336
|
+
if (!appPath) {
|
|
337
|
+
return ["SwiftBar is not installed; skipping launch."];
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
execFileSync("osascript", ["-e", 'quit app "SwiftBar"'], { stdio: "ignore" });
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// SwiftBar may not be running yet.
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
execFileSync("open", [appPath], { stdio: "ignore" });
|
|
347
|
+
execFileSync("sleep", ["1"], { stdio: "ignore" });
|
|
348
|
+
return ["Opened SwiftBar.", ...clearSwiftBarStatusItemVisibility()];
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return [`SwiftBar installed, but launch failed: ${error instanceof Error ? error.message : String(error)}`];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
export function addSwiftBarToLoginItems(options = {}) {
|
|
355
|
+
const dryRun = options.dryRun ?? false;
|
|
356
|
+
const appPath = swiftBarAppPath();
|
|
357
|
+
if (!appPath) {
|
|
358
|
+
return ["SwiftBar is not installed; skipping login item."];
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const result = execFileSync("osascript", [
|
|
362
|
+
"-e", 'tell application "System Events" to get the name of every login item',
|
|
363
|
+
], { encoding: "utf8" });
|
|
364
|
+
if (result.includes("SwiftBar")) {
|
|
365
|
+
return ["SwiftBar already in login items."];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Permission or access issue; try to add anyway.
|
|
370
|
+
}
|
|
371
|
+
if (dryRun) {
|
|
372
|
+
return [`[dry-run] would add SwiftBar to login items: ${appPath}`];
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
execFileSync("osascript", [
|
|
376
|
+
"-e", `tell application "System Events" to make login item at end with properties {name:"SwiftBar", path:${JSON.stringify(appPath)}, hidden:false}`,
|
|
377
|
+
]);
|
|
378
|
+
return ["Added SwiftBar to login items (auto-starts on login)."];
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
return [`Failed to add SwiftBar to login items: ${error instanceof Error ? error.message : String(error)}. Add manually in System Settings → General → Login Items.`];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function swiftBarEscape(value) {
|
|
385
|
+
return value.replaceAll("|", "\\|");
|
|
386
|
+
}
|
|
387
|
+
function swiftBarParamValue(value) {
|
|
388
|
+
return String(value).replaceAll(" ", "\\ ");
|
|
389
|
+
}
|
|
390
|
+
function line(title, params = {}) {
|
|
391
|
+
const renderedParams = Object.entries(params)
|
|
392
|
+
.filter((entry) => entry[1] !== undefined)
|
|
393
|
+
.map(([key, value]) => `${key}=${swiftBarParamValue(value)}`);
|
|
394
|
+
if (renderedParams.length === 0) {
|
|
395
|
+
return swiftBarEscape(title);
|
|
396
|
+
}
|
|
397
|
+
return `${swiftBarEscape(title)} | ${renderedParams.join(" ")}`;
|
|
398
|
+
}
|
|
399
|
+
function imageAssetBase64(name) {
|
|
400
|
+
if (imageCache.has(name)) {
|
|
401
|
+
return imageCache.get(name);
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const encoded = fs.readFileSync(path.join(ASSET_DIR, name)).toString("base64");
|
|
405
|
+
imageCache.set(name, encoded);
|
|
406
|
+
return encoded;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
imageCache.set(name, null);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function titleProviders(snapshot) {
|
|
414
|
+
return [...snapshot.providers].sort((left, right) => {
|
|
415
|
+
const leftIndex = TITLE_PROVIDER_ORDER.indexOf(left.usage.provider);
|
|
416
|
+
const rightIndex = TITLE_PROVIDER_ORDER.indexOf(right.usage.provider);
|
|
417
|
+
const leftRank = leftIndex === -1 ? TITLE_PROVIDER_ORDER.length : leftIndex;
|
|
418
|
+
const rightRank = rightIndex === -1 ? TITLE_PROVIDER_ORDER.length : rightIndex;
|
|
419
|
+
return leftRank - rightRank;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function titleSegment(provider) {
|
|
423
|
+
const usage = provider.usage;
|
|
424
|
+
if (usage.balance && usage.windows.length === 0) {
|
|
425
|
+
const currency = usage.balance.currency === "CNY" ? "¥" : "$";
|
|
426
|
+
return `${currency}${usage.balance.total}`;
|
|
427
|
+
}
|
|
428
|
+
const fiveHour = provider.analysis.fiveHour;
|
|
429
|
+
const sevenDay = provider.analysis.sevenDay;
|
|
430
|
+
const parts = [];
|
|
431
|
+
if (fiveHour) {
|
|
432
|
+
parts.push(`5H:${Math.round(fiveHour.usedPercent)}%`);
|
|
433
|
+
}
|
|
434
|
+
if (sevenDay) {
|
|
435
|
+
parts.push(`7D:${Math.round(sevenDay.usedPercent)}%`);
|
|
436
|
+
}
|
|
437
|
+
return parts.join(",");
|
|
438
|
+
}
|
|
439
|
+
function titleBars(provider) {
|
|
440
|
+
if (provider.usage.balance && provider.usage.windows.length === 0) {
|
|
441
|
+
const isAvailable = provider.usage.balance.isAvailable;
|
|
442
|
+
const [lightColor, darkColor] = (isAvailable ? OK_COLOR : ALERT_COLOR).split(",");
|
|
443
|
+
return [{ pct: isAvailable ? 100 : 0, lightColor, darkColor }];
|
|
444
|
+
}
|
|
445
|
+
const state = provider.analysis.state;
|
|
446
|
+
const [lightColor, darkColor] = STATE_COLOR[state].split(",");
|
|
447
|
+
const fiveHour = provider.analysis.fiveHour;
|
|
448
|
+
const sevenDay = provider.analysis.sevenDay;
|
|
449
|
+
const bars = [];
|
|
450
|
+
if (fiveHour) {
|
|
451
|
+
bars.push({ pct: Math.round(fiveHour.usedPercent), lightColor, darkColor });
|
|
452
|
+
}
|
|
453
|
+
if (sevenDay) {
|
|
454
|
+
bars.push({ pct: Math.round(sevenDay.usedPercent), lightColor, darkColor });
|
|
455
|
+
}
|
|
456
|
+
if (bars.length === 0) {
|
|
457
|
+
bars.push({ pct: 0, lightColor, darkColor });
|
|
458
|
+
}
|
|
459
|
+
return bars;
|
|
460
|
+
}
|
|
461
|
+
function providerIconPath(provider) {
|
|
462
|
+
const asset = PROVIDER_ICON_ASSET[provider];
|
|
463
|
+
if (!asset) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
const iconPath = path.join(ASSET_DIR, asset);
|
|
467
|
+
return fs.existsSync(iconPath) ? iconPath : null;
|
|
468
|
+
}
|
|
469
|
+
function titleImageValue(providers) {
|
|
470
|
+
const segments = providers.map((provider) => {
|
|
471
|
+
const p = provider.usage.provider;
|
|
472
|
+
const darkAsset = PROVIDER_ICON_DARK_ASSET[p];
|
|
473
|
+
const darkPath = darkAsset ? path.join(ASSET_DIR, darkAsset) : null;
|
|
474
|
+
return {
|
|
475
|
+
provider: p,
|
|
476
|
+
bars: titleBars(provider),
|
|
477
|
+
iconPath: providerIconPath(p),
|
|
478
|
+
iconPathDark: darkPath && fs.existsSync(darkPath) ? darkPath : null,
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
if (segments.length === 0) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
const payload = {
|
|
485
|
+
segments,
|
|
486
|
+
scale: TITLE_IMAGE_SCALE,
|
|
487
|
+
height: 22,
|
|
488
|
+
minWidth: 1,
|
|
489
|
+
paddingX: 0,
|
|
490
|
+
iconSize: 16,
|
|
491
|
+
iconBarGap: 4,
|
|
492
|
+
segmentGap: 8,
|
|
493
|
+
barWidth: 48,
|
|
494
|
+
barHeight: 4,
|
|
495
|
+
barGap: 3,
|
|
496
|
+
barRadius: 2,
|
|
497
|
+
barMinFill: 6,
|
|
498
|
+
light: {
|
|
499
|
+
dividerColor: "000000",
|
|
500
|
+
dividerAlpha: 0.25,
|
|
501
|
+
barBgColor: "000000",
|
|
502
|
+
barBgAlpha: 0.3,
|
|
503
|
+
},
|
|
504
|
+
dark: {
|
|
505
|
+
dividerColor: "FFFFFF",
|
|
506
|
+
dividerAlpha: 0.3,
|
|
507
|
+
barBgColor: "FFFFFF",
|
|
508
|
+
barBgAlpha: 0.25,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
const cacheKey = JSON.stringify(payload);
|
|
512
|
+
if (titleImageCache.has(cacheKey)) {
|
|
513
|
+
return titleImageCache.get(cacheKey);
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const output = execFileSync("/usr/bin/osascript", ["-l", "JavaScript", "-e", TITLE_IMAGE_SCRIPT, cacheKey], {
|
|
517
|
+
encoding: "utf8",
|
|
518
|
+
timeout: 2500,
|
|
519
|
+
}).trim();
|
|
520
|
+
const images = JSON.parse(output);
|
|
521
|
+
const value = images.light?.image && images.dark?.image && images.light.width && images.light.height
|
|
522
|
+
? {
|
|
523
|
+
image: `${images.light.image},${images.dark.image}`,
|
|
524
|
+
width: images.light.width,
|
|
525
|
+
height: images.light.height,
|
|
526
|
+
}
|
|
527
|
+
: null;
|
|
528
|
+
titleImageCache.set(cacheKey, value);
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
titleImageCache.set(cacheKey, null);
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function targetLabel(provider) {
|
|
537
|
+
const target = provider.analysis.target;
|
|
538
|
+
if (!target) {
|
|
539
|
+
return "Target learning baseline";
|
|
540
|
+
}
|
|
541
|
+
return `Target ${target.minPercent.toFixed(1)}-${target.maxPercent.toFixed(1)}%`;
|
|
542
|
+
}
|
|
543
|
+
function windowByName(provider, name) {
|
|
544
|
+
return provider.usage.windows.find((window) => window.name === name);
|
|
545
|
+
}
|
|
546
|
+
function muted(text) {
|
|
547
|
+
return line(text, { color: MUTED_COLOR, size: 12 });
|
|
548
|
+
}
|
|
549
|
+
function ansiRGB(hexPair, text) {
|
|
550
|
+
const hex = (hexPair.split(",")[1] || hexPair).replace("#", "");
|
|
551
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
552
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
553
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
554
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
555
|
+
}
|
|
556
|
+
function coloredMeter(usedPercent, fillColor, width = METER_WIDTH) {
|
|
557
|
+
const filled = Math.max(0, Math.min(width, Math.round((usedPercent / 100) * width)));
|
|
558
|
+
const empty = width - filled;
|
|
559
|
+
return (filled > 0 ? ansiRGB(fillColor, "█".repeat(filled)) : "")
|
|
560
|
+
+ (empty > 0 ? ansiRGB(MUTED_COLOR, "░".repeat(empty)) : "");
|
|
561
|
+
}
|
|
562
|
+
function usageLine(label, usedPercent, resetsAt, color, barImage) {
|
|
563
|
+
const percent = `${Math.round(usedPercent).toString().padStart(3)}%`;
|
|
564
|
+
if (barImage) {
|
|
565
|
+
return line(`${label} ${percent} reset ${formatDurationUntil(resetsAt)}`, {
|
|
566
|
+
image: barImage.image,
|
|
567
|
+
width: barImage.width,
|
|
568
|
+
height: barImage.height,
|
|
569
|
+
color,
|
|
570
|
+
font: ROW_FONT,
|
|
571
|
+
size: 12,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
const bar = coloredMeter(usedPercent, color);
|
|
575
|
+
return line(`${label} ${bar} ${ansiRGB(color, percent)} reset ${formatDurationUntil(resetsAt)}`, {
|
|
576
|
+
ansi: true,
|
|
577
|
+
font: ROW_FONT,
|
|
578
|
+
size: 12,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function providerBadge(provider) {
|
|
582
|
+
const fiveHour = provider.analysis.fiveHour;
|
|
583
|
+
if (!fiveHour) {
|
|
584
|
+
return STATE_LABEL[provider.analysis.state];
|
|
585
|
+
}
|
|
586
|
+
return `${Math.round(fiveHour.usedPercent)}%`;
|
|
587
|
+
}
|
|
588
|
+
function providerIconParams(provider) {
|
|
589
|
+
const image = imageAssetBase64(PROVIDER_ICON_ASSET[provider] ?? "");
|
|
590
|
+
if (image) {
|
|
591
|
+
return { image };
|
|
592
|
+
}
|
|
593
|
+
return { sfimage: PROVIDER_ICON_FALLBACK[provider] ?? "terminal.fill" };
|
|
594
|
+
}
|
|
595
|
+
function titleIconParams() {
|
|
596
|
+
const image = imageAssetBase64(TITLE_ICON_ASSET);
|
|
597
|
+
if (image) {
|
|
598
|
+
return { image };
|
|
599
|
+
}
|
|
600
|
+
return { sfimage: "flame.fill", sfcolor: RAW_COLOR };
|
|
601
|
+
}
|
|
602
|
+
function maxProviderAge(snapshot) {
|
|
603
|
+
return Math.max(0, ...snapshot.providers.map((item) => item.meta.ageSeconds));
|
|
604
|
+
}
|
|
605
|
+
function issueLabel(code) {
|
|
606
|
+
if (code === "CLAUDE_INGEST_MISSING") {
|
|
607
|
+
return "Claude not connected";
|
|
608
|
+
}
|
|
609
|
+
if (code === "GLM_API_KEY_MISSING") {
|
|
610
|
+
return "GLM API key not set";
|
|
611
|
+
}
|
|
612
|
+
if (code === "DEEPSEEK_API_KEY_MISSING") {
|
|
613
|
+
return "DeepSeek API key not set";
|
|
614
|
+
}
|
|
615
|
+
if (code === "MINIMAX_API_KEY_MISSING") {
|
|
616
|
+
return "MiniMax API key not set";
|
|
617
|
+
}
|
|
618
|
+
if (code === "USAGE_STALE") {
|
|
619
|
+
return "Usage data is stale";
|
|
620
|
+
}
|
|
621
|
+
if (code === "STATUS_MISSING") {
|
|
622
|
+
return "Status not ready";
|
|
623
|
+
}
|
|
624
|
+
return code.replaceAll("_", " ").toLowerCase();
|
|
625
|
+
}
|
|
626
|
+
export function renderMenuBar(snapshot = loadDisplayStatusSnapshot(), paths = buildPaths()) {
|
|
627
|
+
const compact = readCompactMode(paths);
|
|
628
|
+
const providers = titleProviders(snapshot);
|
|
629
|
+
const title = providers.map(titleSegment).join(` ${TITLE_SEPARATOR} `);
|
|
630
|
+
const toggleParams = {
|
|
631
|
+
bash: stableNodeExecutable(),
|
|
632
|
+
param1: appCliPath(paths),
|
|
633
|
+
param2: "menubar",
|
|
634
|
+
param3: "toggle-compact",
|
|
635
|
+
terminal: false,
|
|
636
|
+
refresh: true,
|
|
637
|
+
color: TEXT_COLOR,
|
|
638
|
+
sfimage: compact ? "rectangle.expand.vertical" : "rectangle.compress.vertical",
|
|
639
|
+
};
|
|
640
|
+
if (!title) {
|
|
641
|
+
return [
|
|
642
|
+
line("Coding Usage Bar No Usage", { sfimage: "flame.fill", sfcolor: RAW_COLOR }),
|
|
643
|
+
"---",
|
|
644
|
+
line("No provider usage available", { color: MUTED_COLOR }),
|
|
645
|
+
line(compact ? "Expand" : "Collapse", toggleParams),
|
|
646
|
+
line("Refresh now", { refresh: true, color: TEXT_COLOR, sfimage: "arrow.clockwise" }),
|
|
647
|
+
].join("\n");
|
|
648
|
+
}
|
|
649
|
+
const topState = [...providers].sort((left, right) => (STATE_PRIORITY[left.analysis.state] - STATE_PRIORITY[right.analysis.state]))[0]?.analysis.state
|
|
650
|
+
?? "RAW";
|
|
651
|
+
const titleImage = titleImageValue(providers);
|
|
652
|
+
const lines = [
|
|
653
|
+
compact
|
|
654
|
+
? line("", {
|
|
655
|
+
sfimage: "flame.fill",
|
|
656
|
+
sfcolor: STATE_COLOR[topState],
|
|
657
|
+
dropdown: false,
|
|
658
|
+
tooltip: title,
|
|
659
|
+
})
|
|
660
|
+
: titleImage
|
|
661
|
+
? line("", {
|
|
662
|
+
image: titleImage.image,
|
|
663
|
+
width: titleImage.width,
|
|
664
|
+
height: titleImage.height,
|
|
665
|
+
dropdown: false,
|
|
666
|
+
tooltip: title,
|
|
667
|
+
})
|
|
668
|
+
: line(title, {
|
|
669
|
+
...titleIconParams(),
|
|
670
|
+
dropdown: false,
|
|
671
|
+
}),
|
|
672
|
+
"---",
|
|
673
|
+
line("Coding Usage Bar", {
|
|
674
|
+
color: TEXT_COLOR,
|
|
675
|
+
size: 15,
|
|
676
|
+
sfimage: "flame.fill",
|
|
677
|
+
sfcolor: STATE_COLOR[topState],
|
|
678
|
+
badge: snapshot.profile.toUpperCase(),
|
|
679
|
+
}),
|
|
680
|
+
muted(`Data age ${maxProviderAge(snapshot)}s`),
|
|
681
|
+
"---",
|
|
682
|
+
];
|
|
683
|
+
const barDataList = [];
|
|
684
|
+
for (const item of providers) {
|
|
685
|
+
if (item.usage.balance && item.usage.windows.length === 0) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const [lc, dc] = STATE_COLOR[item.analysis.state].split(",");
|
|
689
|
+
const f = windowByName(item, "five_hour");
|
|
690
|
+
const s = windowByName(item, "seven_day");
|
|
691
|
+
if (f) {
|
|
692
|
+
barDataList.push({ pct: Math.round(f.usedPercent), lightColor: lc, darkColor: dc });
|
|
693
|
+
}
|
|
694
|
+
if (s) {
|
|
695
|
+
barDataList.push({ pct: Math.round(s.usedPercent), lightColor: lc, darkColor: dc });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const dropdownBars = renderDropdownBarImages(barDataList);
|
|
699
|
+
let barIdx = 0;
|
|
700
|
+
for (const item of providers) {
|
|
701
|
+
const color = STATE_COLOR[item.analysis.state];
|
|
702
|
+
const five = windowByName(item, "five_hour");
|
|
703
|
+
const seven = windowByName(item, "seven_day");
|
|
704
|
+
if (item.usage.balance && item.usage.windows.length === 0) {
|
|
705
|
+
const currency = item.usage.balance.currency === "CNY" ? "¥" : "$";
|
|
706
|
+
const balanceText = `${currency}${item.usage.balance.total}`;
|
|
707
|
+
const availableLabel = item.usage.balance.isAvailable ? "Available" : "Depleted";
|
|
708
|
+
lines.push(line(`${formatProviderLabel(item.usage.provider)} ${availableLabel}`, {
|
|
709
|
+
...providerIconParams(item.usage.provider),
|
|
710
|
+
color: TEXT_COLOR,
|
|
711
|
+
size: 14,
|
|
712
|
+
badge: balanceText,
|
|
713
|
+
}));
|
|
714
|
+
lines.push(line(`Balance ${balanceText}`, {
|
|
715
|
+
color: TEXT_COLOR,
|
|
716
|
+
font: ROW_FONT,
|
|
717
|
+
size: 12,
|
|
718
|
+
}));
|
|
719
|
+
lines.push(line(item.analysis.message, { color: MUTED_COLOR, size: 12, length: 84 }));
|
|
720
|
+
lines.push("---");
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
lines.push(line(`${formatProviderLabel(item.usage.provider)} ${STATE_LABEL[item.analysis.state]}`, {
|
|
724
|
+
...providerIconParams(item.usage.provider),
|
|
725
|
+
color: TEXT_COLOR,
|
|
726
|
+
size: 14,
|
|
727
|
+
badge: providerBadge(item),
|
|
728
|
+
}));
|
|
729
|
+
if (five) {
|
|
730
|
+
lines.push(usageLine("5h", five.usedPercent, five.resetsAt, TEXT_COLOR, dropdownBars[barIdx++]));
|
|
731
|
+
}
|
|
732
|
+
if (seven) {
|
|
733
|
+
lines.push(usageLine("7d", seven.usedPercent, seven.resetsAt, TEXT_COLOR, dropdownBars[barIdx++]));
|
|
734
|
+
}
|
|
735
|
+
lines.push(muted(targetLabel(item)));
|
|
736
|
+
lines.push(line(item.analysis.message, { color: MUTED_COLOR, size: 12, length: 84 }));
|
|
737
|
+
lines.push("---");
|
|
738
|
+
}
|
|
739
|
+
for (const issue of snapshot.issues) {
|
|
740
|
+
const color = issue.severity === "error" ? ALERT_COLOR : WARNING_COLOR;
|
|
741
|
+
lines.push(line(`${issue.severity.toUpperCase()} ${issueLabel(issue.code)}`, {
|
|
742
|
+
color,
|
|
743
|
+
size: 13,
|
|
744
|
+
sfimage: "exclamationmark.triangle.fill",
|
|
745
|
+
sfcolor: color,
|
|
746
|
+
}));
|
|
747
|
+
lines.push(muted(issue.message));
|
|
748
|
+
}
|
|
749
|
+
if (snapshot.issues.length > 0) {
|
|
750
|
+
lines.push("---");
|
|
751
|
+
}
|
|
752
|
+
lines.push(line(compact ? "Expand" : "Collapse", toggleParams));
|
|
753
|
+
lines.push(line("Refresh now", {
|
|
754
|
+
refresh: true,
|
|
755
|
+
color: TEXT_COLOR,
|
|
756
|
+
sfimage: "arrow.clockwise",
|
|
757
|
+
}));
|
|
758
|
+
lines.push(line("Exit Coding Usage Bar", {
|
|
759
|
+
bash: stableNodeExecutable(),
|
|
760
|
+
param1: appCliPath(buildPaths()),
|
|
761
|
+
param2: "menubar",
|
|
762
|
+
param3: "uninstall",
|
|
763
|
+
terminal: false,
|
|
764
|
+
color: MUTED_COLOR,
|
|
765
|
+
sfimage: "xmark.circle",
|
|
766
|
+
}));
|
|
767
|
+
return lines.join("\n");
|
|
768
|
+
}
|
|
769
|
+
function pluginScript(paths) {
|
|
770
|
+
const node = stableNodeExecutable();
|
|
771
|
+
return `#!${node}
|
|
772
|
+
// ${MARKER}
|
|
773
|
+
// <swiftbar.title>Coding Usage Bar</swiftbar.title>
|
|
774
|
+
// <swiftbar.version>v0.1.0</swiftbar.version>
|
|
775
|
+
// <swiftbar.author>Coding Usage Bar</swiftbar.author>
|
|
776
|
+
// <swiftbar.desc>Local Claude Code and Codex burn-rate monitor.</swiftbar.desc>
|
|
777
|
+
// <swiftbar.hideSwiftBar>true</swiftbar.hideSwiftBar>
|
|
778
|
+
// <swiftbar.hideAbout>true</swiftbar.hideAbout>
|
|
779
|
+
// <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>
|
|
780
|
+
// <swiftbar.hideLastUpdated>true</swiftbar.hideLastUpdated>
|
|
781
|
+
// <swiftbar.hideDisablePlugin>true</swiftbar.hideDisablePlugin>
|
|
782
|
+
// <swiftbar.refreshOnOpen>false</swiftbar.refreshOnOpen>
|
|
783
|
+
const { spawnSync } = require("node:child_process");
|
|
784
|
+
const result = spawnSync(${JSON.stringify(node)}, [${JSON.stringify(appCliPath(paths))}, "menubar", "render"], {
|
|
785
|
+
encoding: "utf8",
|
|
786
|
+
});
|
|
787
|
+
if (result.error) {
|
|
788
|
+
console.log("Usage ERR");
|
|
789
|
+
console.log("---");
|
|
790
|
+
console.log(result.error.message);
|
|
791
|
+
process.exit(0);
|
|
792
|
+
}
|
|
793
|
+
if (result.status !== 0) {
|
|
794
|
+
console.log("Usage ERR");
|
|
795
|
+
console.log("---");
|
|
796
|
+
console.log(result.stderr || result.stdout || "coding-usage-bar menubar render failed");
|
|
797
|
+
process.exit(0);
|
|
798
|
+
}
|
|
799
|
+
process.stdout.write(result.stdout);
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
function isManagedPlugin(paths) {
|
|
803
|
+
const plugin = swiftBarPluginPath(paths);
|
|
804
|
+
try {
|
|
805
|
+
return fs.readFileSync(plugin.file, "utf8").includes(MARKER);
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
export function swiftBarStatusItemVisibilityKeys(defaultsOutput) {
|
|
812
|
+
return defaultsOutput
|
|
813
|
+
.split("\n")
|
|
814
|
+
.map((line) => line.match(/^\s*"?((?:NSStatusItem Visible)[^"=]*)"?\s*=/)?.[1]?.trim())
|
|
815
|
+
.filter((key) => Boolean(key));
|
|
816
|
+
}
|
|
817
|
+
function clearSwiftBarStatusItemVisibility(options = {}) {
|
|
818
|
+
let keys;
|
|
819
|
+
try {
|
|
820
|
+
keys = swiftBarStatusItemVisibilityKeys(execFileSync("defaults", ["read", "com.ameba.SwiftBar"], {
|
|
821
|
+
encoding: "utf8",
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
if (keys.length === 0) {
|
|
828
|
+
return [];
|
|
829
|
+
}
|
|
830
|
+
if (options.dryRun) {
|
|
831
|
+
return [`[dry-run] would clear SwiftBar hidden status item cache: ${keys.join(", ")}`];
|
|
832
|
+
}
|
|
833
|
+
for (const key of keys) {
|
|
834
|
+
try {
|
|
835
|
+
execFileSync("defaults", ["delete", "com.ameba.SwiftBar", key], { stdio: "ignore" });
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
// Ignore races with SwiftBar rewriting or deleting the same key.
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return [`Cleared SwiftBar hidden status item cache: ${keys.join(", ")}`];
|
|
842
|
+
}
|
|
843
|
+
export function installMenuBar(options = {}) {
|
|
844
|
+
const dryRun = options.dryRun ?? false;
|
|
845
|
+
const paths = buildPaths();
|
|
846
|
+
const plugin = swiftBarPluginPath(paths);
|
|
847
|
+
if (dryRun) {
|
|
848
|
+
return [
|
|
849
|
+
`[dry-run] would write SwiftBar plugin: ${plugin.file}`,
|
|
850
|
+
...clearSwiftBarStatusItemVisibility({ dryRun }),
|
|
851
|
+
];
|
|
852
|
+
}
|
|
853
|
+
if (isFile(plugin.file) && !isManagedPlugin(paths)) {
|
|
854
|
+
return [`SwiftBar plugin already exists and is not managed by coding-usage-bar: ${plugin.file}`];
|
|
855
|
+
}
|
|
856
|
+
ensureDir(plugin.dir);
|
|
857
|
+
fs.writeFileSync(plugin.file, pluginScript(paths), { mode: 0o755 });
|
|
858
|
+
return [
|
|
859
|
+
`Installed SwiftBar plugin: ${plugin.file}`,
|
|
860
|
+
...clearSwiftBarStatusItemVisibility(),
|
|
861
|
+
"Open SwiftBar and set its plugin folder to the Coding Usage Bar plugin directory if it is not already configured.",
|
|
862
|
+
`Plugin directory: ${plugin.dir}`,
|
|
863
|
+
];
|
|
864
|
+
}
|
|
865
|
+
export function uninstallMenuBar(options = {}) {
|
|
866
|
+
const dryRun = options.dryRun ?? false;
|
|
867
|
+
const paths = buildPaths();
|
|
868
|
+
const plugin = swiftBarPluginPath(paths);
|
|
869
|
+
if (!isFile(plugin.file)) {
|
|
870
|
+
return ["No Coding Usage Bar SwiftBar plugin installed."];
|
|
871
|
+
}
|
|
872
|
+
if (!isManagedPlugin(paths)) {
|
|
873
|
+
return [`SwiftBar plugin is user-managed; leaving it unchanged: ${plugin.file}`];
|
|
874
|
+
}
|
|
875
|
+
if (dryRun) {
|
|
876
|
+
return [`[dry-run] would remove SwiftBar plugin: ${plugin.file}`];
|
|
877
|
+
}
|
|
878
|
+
fs.rmSync(plugin.file, { force: true });
|
|
879
|
+
return [`Removed SwiftBar plugin: ${plugin.file}`];
|
|
880
|
+
}
|