pi-ark-usage 0.1.3 → 0.1.4
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/README.md +2 -4
- package/extensions/index.ts +21 -107
- package/extensions/lib/types.ts +0 -7
- package/extensions/lib/widget.ts +5 -248
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,10 +10,8 @@ Volc Usage: sess 30%(reset after 4h18m) / wk 37%(reset after 4d) / mo 19%(reset
|
|
|
10
10
|
|
|
11
11
|
- **AK/SK 直连 OpenAPI**:不走子进程、不依赖 SSO 登录态;凭据只从环境变量或配置文件读取,本扩展不保存、不上传任何 Key
|
|
12
12
|
- **三个周期同屏**:session(会话)/ weekly(周)/ monthly(月),并显示各周期重置倒计时
|
|
13
|
-
- **Powerline
|
|
13
|
+
- **Powerline 状态段(唯一展示位)**:通过 `ctx.ui.setStatus("ark-usage", …)` 发布单行摘要(`1% 4h38m | 15% 5d4h | 54% 23d4h`,固定按 session/weekly/monthly 顺序),显示在 [pi-powerline-footer](https://www.npmjs.com/package/pi-powerline-footer) 上;百分比按阈值绿/黄/红着色,时间部分紫色,分隔符跟随主题 `thinkingHigh`(编辑器边框色)。不安装 powerline 时也可通过内置 footer 的 extension statuses 区域或 `/arkusage` 查看
|
|
14
14
|
- **阈值着色**:低用量绿色(<50%)、接近上限黄色(50–79%)、超过红线红色(≥80%,默认值,可用 `/arkset` 调整)
|
|
15
|
-
- **单轮消耗**:以每轮对话开始前的快照为基线,结算时标注各周期增量(如 `(+2)`)
|
|
16
|
-
- **产品切换感知**:在个人版/团队版、Coding Plan/Agent Plan 之间切换时标注 `(变更)`;套餐周期重置时标注 `(已重置)`
|
|
17
15
|
- **持久化缓存**:重启 pi 后立即显示上次快照(`~/.pi/agent/pi-ark-usage/cache.json`,0600 权限,临时文件原子写入)
|
|
18
16
|
- 可选空闲自动刷新(默认关闭)
|
|
19
17
|
|
|
@@ -99,7 +97,7 @@ pi install npm:pi-ark-usage
|
|
|
99
97
|
1. 用官方 `@volcengine/openapi` 的 `Signer` 对 `POST open.volcengineapi.com?Action=GetCodingPlanUsage&Version=2024-01-01`(service `ark`,region `cn-beijing`)做 V4 签名后 fetch。
|
|
100
98
|
2. 解析 `Result.QuotaUsage[]`(`Level/Percent/ResetTimestamp`,秒级时间戳转 `+08:00` ISO 字符串),做结构与取值校验。
|
|
101
99
|
3. 快照存入内存 Map,并以原子写(tmp 文件 + rename)持久化到 `~/.pi/agent/pi-ark-usage/`。
|
|
102
|
-
4.
|
|
100
|
+
4. 摘要通过 `ctx.ui.setStatus("ark-usage", …)` 发布到 extension status,由 pi-powerline-footer 渲染到 powerline 行;每分钟 ticker 在空闲时重发,保持重置倒计时刷新。
|
|
103
101
|
|
|
104
102
|
## 开发
|
|
105
103
|
|
package/extensions/index.ts
CHANGED
|
@@ -6,14 +6,11 @@
|
|
|
6
6
|
* the official `@volcengine/openapi` Signer. This extension never stores keys:
|
|
7
7
|
* credentials come from VOLC_ACCESSKEY/VOLC_SECRETKEY or ~/.volc/config.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Powerline status (via `ctx.ui.setStatus`): 1% 4h38m | 15% 5d4h | 54% 23d4h
|
|
10
10
|
* Commands:
|
|
11
11
|
* /arkcheck force refresh
|
|
12
12
|
* /arkusage show snapshot in a notification
|
|
13
13
|
* /arkset ... view/change settings
|
|
14
|
-
*
|
|
15
|
-
* Design modeled after pi-check-agent-quota: baseline snapshot taken at
|
|
16
|
-
* agent_start, compared at agent_settled to annotate per-round consumption.
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
16
|
import type {
|
|
@@ -23,11 +20,9 @@ import type {
|
|
|
23
20
|
} from "@earendil-works/pi-coding-agent";
|
|
24
21
|
import { fetchPlan } from "./lib/arkcli.js";
|
|
25
22
|
import {
|
|
26
|
-
ArkUsageComponent,
|
|
27
23
|
AUTO_REFRESH_MAX_MINUTES,
|
|
28
24
|
LOCALES,
|
|
29
25
|
buildCompactStatus,
|
|
30
|
-
buildItems,
|
|
31
26
|
formatRemaining,
|
|
32
27
|
getSettings,
|
|
33
28
|
patchSettings,
|
|
@@ -45,10 +40,8 @@ import type {
|
|
|
45
40
|
ArkSettings,
|
|
46
41
|
Language,
|
|
47
42
|
PlanSnapshot,
|
|
48
|
-
RenderItem,
|
|
49
43
|
} from "./lib/types.js";
|
|
50
44
|
|
|
51
|
-
const WIDGET_KEY = "pi-ark-usage";
|
|
52
45
|
/** Extension-status key consumed by pi-powerline-footer (powerline slot). */
|
|
53
46
|
const STATUS_KEY = "ark-usage";
|
|
54
47
|
const TICK_MS = 60_000;
|
|
@@ -87,10 +80,6 @@ let language: Language = "en";
|
|
|
87
80
|
const snapshots = new Map<string, PlanSnapshot>();
|
|
88
81
|
let activeProduct: ProductId | null = null;
|
|
89
82
|
let status: Status = "ok";
|
|
90
|
-
let deltas: Record<string, number> | null = null;
|
|
91
|
-
let note: "changed" | "reset" | null = null;
|
|
92
|
-
let baseline: PlanSnapshot | null = null;
|
|
93
|
-
let activeWidget: ArkUsageComponent | null = null;
|
|
94
83
|
let tickTimer: ReturnType<typeof setInterval> | null = null;
|
|
95
84
|
let lastCtx: ExtensionContext | null = null;
|
|
96
85
|
let lastAutoAt = 0;
|
|
@@ -128,20 +117,12 @@ function activeSnapshot(): PlanSnapshot | null {
|
|
|
128
117
|
|
|
129
118
|
/* -------------------------------- rendering -------------------------------- */
|
|
130
119
|
|
|
131
|
-
function
|
|
120
|
+
function renderStatus(ctx: ExtensionContext): void {
|
|
132
121
|
lastCtx = ctx;
|
|
133
122
|
const snap = activeSnapshot();
|
|
134
123
|
const widgetStatus = status === "ok" && !snap ? "none" : status;
|
|
135
|
-
const items: RenderItem[] = buildItems(
|
|
136
|
-
snap,
|
|
137
|
-
language,
|
|
138
|
-
widgetStatus,
|
|
139
|
-
ctx.isIdle(),
|
|
140
|
-
deltas,
|
|
141
|
-
note,
|
|
142
|
-
);
|
|
143
124
|
// Publish the one-line summary to the extension-status slot (powerline).
|
|
144
|
-
// The ticker calls
|
|
125
|
+
// The ticker calls renderStatus every minute while idle, keeping the
|
|
145
126
|
// reset countdown fresh without refetching.
|
|
146
127
|
const compact = buildCompactStatus(snap, widgetStatus, ctx.ui.theme);
|
|
147
128
|
if (compact) {
|
|
@@ -149,23 +130,6 @@ function renderWidget(ctx: ExtensionContext): void {
|
|
|
149
130
|
} else {
|
|
150
131
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
151
132
|
}
|
|
152
|
-
if (activeWidget) {
|
|
153
|
-
activeWidget.update(items);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
ctx.ui.setWidget(
|
|
157
|
-
WIDGET_KEY,
|
|
158
|
-
(tui, theme) => {
|
|
159
|
-
const component = new ArkUsageComponent(
|
|
160
|
-
items,
|
|
161
|
-
() => theme,
|
|
162
|
-
() => tui.requestRender?.(),
|
|
163
|
-
);
|
|
164
|
-
activeWidget = component;
|
|
165
|
-
return component;
|
|
166
|
-
},
|
|
167
|
-
{ placement: "belowEditor" },
|
|
168
|
-
);
|
|
169
133
|
}
|
|
170
134
|
|
|
171
135
|
/* --------------------------------- fetching -------------------------------- */
|
|
@@ -193,7 +157,7 @@ async function refresh(
|
|
|
193
157
|
activeProduct = product;
|
|
194
158
|
if (!product) {
|
|
195
159
|
status = "none";
|
|
196
|
-
|
|
160
|
+
renderStatus(ctx);
|
|
197
161
|
return false;
|
|
198
162
|
}
|
|
199
163
|
|
|
@@ -203,8 +167,7 @@ async function refresh(
|
|
|
203
167
|
}
|
|
204
168
|
|
|
205
169
|
status = "fetching";
|
|
206
|
-
|
|
207
|
-
renderWidget(ctx);
|
|
170
|
+
renderStatus(ctx);
|
|
208
171
|
|
|
209
172
|
const controller = new AbortController();
|
|
210
173
|
inFlight = controller;
|
|
@@ -242,7 +205,7 @@ async function refresh(
|
|
|
242
205
|
return false;
|
|
243
206
|
} finally {
|
|
244
207
|
if (inFlight === controller) inFlight = null;
|
|
245
|
-
|
|
208
|
+
renderStatus(ctx);
|
|
246
209
|
}
|
|
247
210
|
}
|
|
248
211
|
|
|
@@ -252,66 +215,21 @@ function persist(): void {
|
|
|
252
215
|
saveCache(language, out);
|
|
253
216
|
}
|
|
254
217
|
|
|
255
|
-
/*
|
|
256
|
-
|
|
257
|
-
function computeDeltas(
|
|
258
|
-
before: PlanSnapshot,
|
|
259
|
-
after: PlanSnapshot,
|
|
260
|
-
): Record<string, number> | null {
|
|
261
|
-
const out: Record<string, number> = {};
|
|
262
|
-
for (const p of after.periods) {
|
|
263
|
-
const old = before.periods.find((q) => q.label === p.label);
|
|
264
|
-
if (old) out[p.label] = Math.round(old.percent - p.percent);
|
|
265
|
-
// note: stored as consumed-positive; renderer negates for display
|
|
266
|
-
}
|
|
267
|
-
// flip sign convention for renderer: it wants negative=consumed
|
|
268
|
-
const render: Record<string, number> = {};
|
|
269
|
-
for (const [k, v] of Object.entries(out)) render[k] = -v;
|
|
270
|
-
return render;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function looksReset(a: PlanSnapshot, b: PlanSnapshot): boolean {
|
|
274
|
-
// all periods dropped sharply at once => plan period reset
|
|
275
|
-
return b.periods.every((p) => {
|
|
276
|
-
const old = a.periods.find((q) => q.label === p.label);
|
|
277
|
-
return old !== undefined && p.percent - old.percent < -30;
|
|
278
|
-
});
|
|
279
|
-
}
|
|
218
|
+
/* ----------------------- session-start / settled --------------------------- */
|
|
280
219
|
|
|
281
220
|
async function handleStart(ctx: ExtensionContext): Promise<void> {
|
|
282
221
|
const product = resolveProduct(ctx);
|
|
283
222
|
if (!product) return;
|
|
284
223
|
const cached = snapshots.get(product) ?? null;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const win = await timeoutResult(refresh(ctx, "stale"), AGENT_START_TIMEOUT_MS);
|
|
289
|
-
baseline = win ? snapshots.get(product) ?? null : cached;
|
|
224
|
+
// Only refetch at agent_start when the cached snapshot went stale.
|
|
225
|
+
if (!cached || Date.now() - cached.fetchedAt > BASELINE_FRESH_MS) {
|
|
226
|
+
await timeoutResult(refresh(ctx, "stale"), AGENT_START_TIMEOUT_MS);
|
|
290
227
|
}
|
|
291
|
-
|
|
292
|
-
renderWidget(ctx);
|
|
228
|
+
renderStatus(ctx);
|
|
293
229
|
}
|
|
294
230
|
|
|
295
231
|
async function handleSettled(ctx: ExtensionContext): Promise<void> {
|
|
296
|
-
|
|
297
|
-
if (!product) return;
|
|
298
|
-
const ok = await refresh(ctx, "settled");
|
|
299
|
-
if (!ok) return;
|
|
300
|
-
const snap = snapshots.get(product);
|
|
301
|
-
if (!snap) return;
|
|
302
|
-
|
|
303
|
-
if (baseline && baseline.product !== product) {
|
|
304
|
-
note = "changed";
|
|
305
|
-
deltas = null;
|
|
306
|
-
} else if (baseline && looksReset(baseline, snap)) {
|
|
307
|
-
note = "reset";
|
|
308
|
-
deltas = null;
|
|
309
|
-
} else if (baseline) {
|
|
310
|
-
deltas = computeDeltas(baseline, snap);
|
|
311
|
-
note = null;
|
|
312
|
-
}
|
|
313
|
-
baseline = null;
|
|
314
|
-
renderWidget(ctx);
|
|
232
|
+
await refresh(ctx, "settled");
|
|
315
233
|
}
|
|
316
234
|
|
|
317
235
|
async function timeoutResult<T>(p: Promise<T>, ms: number): Promise<T | null> {
|
|
@@ -342,7 +260,7 @@ function ensureTicker(): void {
|
|
|
342
260
|
return;
|
|
343
261
|
}
|
|
344
262
|
// age text ticks every minute even without refetch
|
|
345
|
-
if (
|
|
263
|
+
if (lastCtx && lastCtx.isIdle()) renderStatus(lastCtx);
|
|
346
264
|
}, TICK_MS);
|
|
347
265
|
tickTimer.unref?.();
|
|
348
266
|
}
|
|
@@ -408,7 +326,7 @@ async function cmdSet(
|
|
|
408
326
|
const notifySaved = (): void => {
|
|
409
327
|
persist();
|
|
410
328
|
ctx.ui.notify(locale.settingsApplied, "info");
|
|
411
|
-
if (lastCtx)
|
|
329
|
+
if (lastCtx) renderStatus(lastCtx);
|
|
412
330
|
};
|
|
413
331
|
|
|
414
332
|
if (parts.length === 0) {
|
|
@@ -422,7 +340,7 @@ async function cmdSet(
|
|
|
422
340
|
resetSettings();
|
|
423
341
|
persist();
|
|
424
342
|
ctx.ui.notify(locale.settingsReset, "info");
|
|
425
|
-
if (lastCtx)
|
|
343
|
+
if (lastCtx) renderStatus(lastCtx);
|
|
426
344
|
return;
|
|
427
345
|
}
|
|
428
346
|
case "yellow":
|
|
@@ -499,7 +417,7 @@ async function cmdSet(
|
|
|
499
417
|
language = raw;
|
|
500
418
|
persist();
|
|
501
419
|
ctx.ui.notify(locale.languageChanged(language), "info");
|
|
502
|
-
if (lastCtx)
|
|
420
|
+
if (lastCtx) renderStatus(lastCtx);
|
|
503
421
|
return;
|
|
504
422
|
}
|
|
505
423
|
default:
|
|
@@ -532,25 +450,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
532
450
|
});
|
|
533
451
|
|
|
534
452
|
pi.on("session_start", (_event, ctx) => {
|
|
535
|
-
// Session switches reuse this module instance in-process: pi has
|
|
536
|
-
//
|
|
537
|
-
//
|
|
453
|
+
// Session switches reuse this module instance in-process: pi has torn
|
|
454
|
+
// down the previous runtime, so revive the shutdown flags before doing
|
|
455
|
+
// anything else.
|
|
538
456
|
shuttingDown = false;
|
|
539
457
|
resetShutdownState();
|
|
540
|
-
activeWidget = null;
|
|
541
458
|
activeProduct = resolveProduct(ctx);
|
|
542
|
-
|
|
543
|
-
note = null;
|
|
544
|
-
renderWidget(ctx);
|
|
459
|
+
renderStatus(ctx);
|
|
545
460
|
ensureTicker();
|
|
546
461
|
void refresh(ctx, "session_start");
|
|
547
462
|
});
|
|
548
463
|
|
|
549
464
|
pi.on("model_select", (_event, ctx) => {
|
|
550
465
|
const next = resolveProduct(ctx);
|
|
551
|
-
if (baseline && next && baseline.product !== next) note = "changed";
|
|
552
466
|
activeProduct = next;
|
|
553
|
-
|
|
467
|
+
renderStatus(ctx);
|
|
554
468
|
void refresh(ctx, "model_select");
|
|
555
469
|
});
|
|
556
470
|
|
package/extensions/lib/types.ts
CHANGED
|
@@ -4,13 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
export type Language = "zh" | "en";
|
|
6
6
|
|
|
7
|
-
/** Render model, modeled after pi-check-agent-quota. */
|
|
8
|
-
export type RenderItem =
|
|
9
|
-
| { kind: "text"; text: string }
|
|
10
|
-
| { kind: "pct"; pct: number; metric: string }
|
|
11
|
-
| { kind: "annotation"; text: string }
|
|
12
|
-
| { kind: "age"; text: string };
|
|
13
|
-
|
|
14
7
|
/** User-tunable settings (persisted in the disk cache). */
|
|
15
8
|
export type ArkSettings = {
|
|
16
9
|
/** yellow threshold, percent */
|
package/extensions/lib/widget.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rendering
|
|
3
|
-
*
|
|
2
|
+
* Rendering for the powerline extension-status slot: locales, threshold
|
|
3
|
+
* settings, percent coloring, remaining-time formatting, and the compact
|
|
4
|
+
* colored one-line summary published via `ctx.ui.setStatus`.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import type { ArkSettings, Language, PlanSnapshot
|
|
8
|
+
import type { ArkSettings, Language, PlanSnapshot } from "./types.js";
|
|
8
9
|
|
|
9
10
|
/* ---------------------------------- i18n ---------------------------------- */
|
|
10
11
|
|
|
@@ -20,10 +21,6 @@ type Locale = {
|
|
|
20
21
|
changed: string;
|
|
21
22
|
reset: string;
|
|
22
23
|
resetsIn: (t: string) => string;
|
|
23
|
-
ageNow: string;
|
|
24
|
-
ageMinutesAgo: (m: number) => string;
|
|
25
|
-
ageHoursAgo: (h: number, m: number) => string;
|
|
26
|
-
ageDaysAgo: (d: number, h: number) => string;
|
|
27
24
|
noActiveProduct: string;
|
|
28
25
|
settingsShow: (s: ArkSettings) => string;
|
|
29
26
|
settingsApplied: string;
|
|
@@ -56,10 +53,6 @@ const ZH: Locale = {
|
|
|
56
53
|
changed: "变更",
|
|
57
54
|
reset: "已重置",
|
|
58
55
|
resetsIn: (t) => `${t}后重置`,
|
|
59
|
-
ageNow: "刚刚",
|
|
60
|
-
ageMinutesAgo: (m) => `${m}分钟前`,
|
|
61
|
-
ageHoursAgo: (h, m) => (m > 0 ? `${h}小时${m}分前` : `${h}小时前`),
|
|
62
|
-
ageDaysAgo: (d, h) => (h > 0 ? `${d}天${h}小时前` : `${d}天前`),
|
|
63
56
|
noActiveProduct: "当前没有可查询的火山套餐",
|
|
64
57
|
settingsShow: (s) =>
|
|
65
58
|
`当前设置:产品=${s.product} 黄线=${s.pctYellow} 红线=${s.pctRed} 自动刷新=${s.autoRefreshMinutes}分钟 seat=${s.seat || "无"}`,
|
|
@@ -94,10 +87,6 @@ const EN: Locale = {
|
|
|
94
87
|
changed: "changed",
|
|
95
88
|
reset: "reset",
|
|
96
89
|
resetsIn: (t) => `reset after ${t}`,
|
|
97
|
-
ageNow: "now",
|
|
98
|
-
ageMinutesAgo: (m) => `${m}m ago`,
|
|
99
|
-
ageHoursAgo: (h, m) => (m > 0 ? `${h}h${m}m ago` : `${h}h ago`),
|
|
100
|
-
ageDaysAgo: (d, h) => (h > 0 ? `${d}d${h}h ago` : `${d}d ago`),
|
|
101
90
|
noActiveProduct: "No active Volcengine plan",
|
|
102
91
|
settingsShow: (s) =>
|
|
103
92
|
`Settings: product=${s.product} yellow=${s.pctYellow} red=${s.pctRed} autoRefresh=${s.autoRefreshMinutes}m seat=${s.seat || "none"}`,
|
|
@@ -196,19 +185,6 @@ export function formatRemaining(ms: number, lang: Language): string {
|
|
|
196
185
|
return `${m}m`;
|
|
197
186
|
}
|
|
198
187
|
|
|
199
|
-
export function formatAge(fetchedAt: number, lang: Language): string {
|
|
200
|
-
const age = Date.now() - fetchedAt;
|
|
201
|
-
if (!Number.isFinite(age) || age < 0) return "";
|
|
202
|
-
if (age < 60_000) return LOCALES[lang].ageNow;
|
|
203
|
-
const totalMin = Math.floor(age / 60_000);
|
|
204
|
-
const h = Math.floor(totalMin / 60);
|
|
205
|
-
const m = totalMin % 60;
|
|
206
|
-
const d = Math.floor(h / 24);
|
|
207
|
-
if (d > 0) return LOCALES[lang].ageDaysAgo(d, h % 24);
|
|
208
|
-
if (h > 0) return LOCALES[lang].ageHoursAgo(h, m);
|
|
209
|
-
return LOCALES[lang].ageMinutesAgo(m);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
188
|
/* ---------------------- compact status (powerline slot) -------------------- */
|
|
213
189
|
|
|
214
190
|
/**
|
|
@@ -260,224 +236,5 @@ export function buildCompactStatus(
|
|
|
260
236
|
return null;
|
|
261
237
|
}
|
|
262
238
|
|
|
263
|
-
|
|
264
|
-
|
|
239
|
+
/** Lifecycle states mirrored in the compact status. */
|
|
265
240
|
export type WidgetStatus = "ok" | "fetching" | "failed" | "no-binary" | "none";
|
|
266
|
-
|
|
267
|
-
export function buildItems(
|
|
268
|
-
snapshot: PlanSnapshot | null,
|
|
269
|
-
lang: Language,
|
|
270
|
-
status: WidgetStatus,
|
|
271
|
-
isIdle: boolean,
|
|
272
|
-
/** delta annotations keyed by period label; negative = consumed */
|
|
273
|
-
deltas: Record<string, number> | null,
|
|
274
|
-
note?: "changed" | "reset" | null,
|
|
275
|
-
): RenderItem[] {
|
|
276
|
-
const locale = LOCALES[lang];
|
|
277
|
-
const items: RenderItem[] = [];
|
|
278
|
-
|
|
279
|
-
if (snapshot && snapshot.periods.length > 0) {
|
|
280
|
-
items.push({ kind: "text", text: `${locale.title}: ` });
|
|
281
|
-
snapshot.periods.forEach((p, i) => {
|
|
282
|
-
if (i > 0) items.push({ kind: "text", text: " / " });
|
|
283
|
-
const label = locale.labels[p.label] ?? p.label;
|
|
284
|
-
items.push({ kind: "text", text: `${label} ` });
|
|
285
|
-
items.push({ kind: "pct", pct: clampPct(p.percent), metric: p.label });
|
|
286
|
-
const delta = deltas?.[p.label];
|
|
287
|
-
if (typeof delta === "number" && delta !== 0) {
|
|
288
|
-
const sign = delta > 0 ? "+" : "-";
|
|
289
|
-
items.push({ kind: "annotation", text: `(${sign}${Math.abs(Math.round(delta))})` });
|
|
290
|
-
}
|
|
291
|
-
if (p.reset_at) {
|
|
292
|
-
const remaining = new Date(p.reset_at).getTime() - Date.now();
|
|
293
|
-
const t = formatRemaining(remaining, lang);
|
|
294
|
-
if (t) items.push({ kind: "annotation", text: `(${locale.resetsIn(t)})` });
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
} else if (status === "fetching") {
|
|
298
|
-
items.push({ kind: "text", text: `${locale.title}: ` });
|
|
299
|
-
items.push({ kind: "annotation", text: locale.fetching });
|
|
300
|
-
} else if (status === "failed") {
|
|
301
|
-
items.push({ kind: "text", text: `${locale.title}: ` });
|
|
302
|
-
items.push({ kind: "annotation", text: locale.failed });
|
|
303
|
-
} else if (status === "no-binary") {
|
|
304
|
-
items.push({ kind: "annotation", text: locale.noBinary });
|
|
305
|
-
} else {
|
|
306
|
-
items.push({ kind: "annotation", text: locale.noData });
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (!isIdle) {
|
|
310
|
-
items.push({ kind: "annotation", text: ` (${locale.using})` });
|
|
311
|
-
} else if (note) {
|
|
312
|
-
items.push({ kind: "annotation", text: ` (${note === "reset" ? locale.reset : locale.changed})` });
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (snapshot) {
|
|
316
|
-
items.push({ kind: "age", text: formatAge(snapshot.fetchedAt, lang) });
|
|
317
|
-
}
|
|
318
|
-
return items;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function formatItems(items: RenderItem[], theme: Theme): string {
|
|
322
|
-
return items
|
|
323
|
-
.map((it) => {
|
|
324
|
-
switch (it.kind) {
|
|
325
|
-
case "text":
|
|
326
|
-
return theme.fg("dim", it.text);
|
|
327
|
-
case "pct":
|
|
328
|
-
return coloredPct(it.pct);
|
|
329
|
-
case "age":
|
|
330
|
-
return theme.fg("dim", it.text);
|
|
331
|
-
case "annotation":
|
|
332
|
-
default:
|
|
333
|
-
return hexFg(HEX_COLORS.purple, it.text);
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
|
-
.join("");
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/* ------------------------------ ansi helpers ------------------------------ */
|
|
340
|
-
|
|
341
|
-
export const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
342
|
-
|
|
343
|
-
export function visibleWidth(s: string): number {
|
|
344
|
-
let w = 0;
|
|
345
|
-
for (const ch of s.replace(ANSI_RE, "")) {
|
|
346
|
-
const cp = ch.codePointAt(0) ?? 0;
|
|
347
|
-
if (cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f)) continue;
|
|
348
|
-
if (cp >= 0x0300 && cp <= 0x036f) continue;
|
|
349
|
-
w +=
|
|
350
|
-
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
351
|
-
(cp >= 0x2e80 && cp <= 0xa4cf) ||
|
|
352
|
-
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
353
|
-
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
354
|
-
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
355
|
-
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
356
|
-
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
357
|
-
(cp >= 0x1f300 && cp <= 0x1f9ff)
|
|
358
|
-
? 2
|
|
359
|
-
: 1;
|
|
360
|
-
}
|
|
361
|
-
return w;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function truncateAnsi(s: string, maxWidth: number): string {
|
|
365
|
-
if (maxWidth <= 0) return "";
|
|
366
|
-
if (visibleWidth(s) <= maxWidth) return s;
|
|
367
|
-
const budget = maxWidth - 1;
|
|
368
|
-
let out = "";
|
|
369
|
-
let w = 0;
|
|
370
|
-
let i = 0;
|
|
371
|
-
while (i < s.length) {
|
|
372
|
-
ANSI_RE.lastIndex = i;
|
|
373
|
-
const m = ANSI_RE.exec(s);
|
|
374
|
-
if (m && m.index === i) {
|
|
375
|
-
out += m[0];
|
|
376
|
-
i = ANSI_RE.lastIndex;
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
const ch = String.fromCodePoint(s.codePointAt(i) ?? 0);
|
|
380
|
-
const cw = visibleWidth(ch);
|
|
381
|
-
if (w + cw > budget) break;
|
|
382
|
-
out += ch;
|
|
383
|
-
w += cw;
|
|
384
|
-
i += ch.length;
|
|
385
|
-
}
|
|
386
|
-
return `${out}…\x1b[0m`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export function itemsEqual(a: RenderItem[], b: RenderItem[]): boolean {
|
|
390
|
-
if (a.length !== b.length) return false;
|
|
391
|
-
return a.every((it, i) => {
|
|
392
|
-
const o = b[i];
|
|
393
|
-
if (it.kind !== o.kind) return false;
|
|
394
|
-
switch (it.kind) {
|
|
395
|
-
case "pct":
|
|
396
|
-
return (
|
|
397
|
-
o.kind === "pct" && it.pct === o.pct && it.metric === o.metric
|
|
398
|
-
);
|
|
399
|
-
case "text":
|
|
400
|
-
return o.kind === "text" && it.text === o.text;
|
|
401
|
-
case "age":
|
|
402
|
-
return o.kind === "age" && it.text === o.text;
|
|
403
|
-
case "annotation":
|
|
404
|
-
return o.kind === "annotation" && it.text === o.text;
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/* -------------------------------- component -------------------------------- */
|
|
410
|
-
|
|
411
|
-
export interface Component {
|
|
412
|
-
render(width: number): string[];
|
|
413
|
-
invalidate(): void;
|
|
414
|
-
dispose?(): void;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Widget rendered below the editor. The age segment is right-aligned and the
|
|
419
|
-
* content wraps to two lines when the terminal is too narrow.
|
|
420
|
-
*/
|
|
421
|
-
export class ArkUsageComponent implements Component {
|
|
422
|
-
private items: RenderItem[];
|
|
423
|
-
private readonly themeRef: () => Theme;
|
|
424
|
-
private readonly requestRender: () => void;
|
|
425
|
-
private cache: { width: number; lines: string[] } | null = null;
|
|
426
|
-
private disposed = false;
|
|
427
|
-
|
|
428
|
-
constructor(
|
|
429
|
-
items: RenderItem[],
|
|
430
|
-
themeRef: () => Theme,
|
|
431
|
-
requestRender: () => void,
|
|
432
|
-
) {
|
|
433
|
-
this.items = items;
|
|
434
|
-
this.themeRef = themeRef;
|
|
435
|
-
this.requestRender = requestRender;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
update(items: RenderItem[]): void {
|
|
439
|
-
if (this.disposed || itemsEqual(this.items, items)) return;
|
|
440
|
-
this.items = items;
|
|
441
|
-
this.invalidate();
|
|
442
|
-
this.requestRender();
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
invalidate(): void {
|
|
446
|
-
this.cache = null;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
render(width: number): string[] {
|
|
450
|
-
if (this.cache && this.cache.width === width) return this.cache.lines;
|
|
451
|
-
const theme = this.themeRef();
|
|
452
|
-
|
|
453
|
-
const ageIdx = this.items.findIndex((it) => it.kind === "age");
|
|
454
|
-
if (ageIdx === -1) {
|
|
455
|
-
const text = formatItems(this.items, theme);
|
|
456
|
-
const lines = text ? [truncateAnsi(text, width)] : [];
|
|
457
|
-
this.cache = { width, lines };
|
|
458
|
-
return lines;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const leftText = formatItems(this.items.slice(0, ageIdx), theme);
|
|
462
|
-
const rightText = formatItems(this.items.slice(ageIdx), theme);
|
|
463
|
-
const lw = visibleWidth(leftText);
|
|
464
|
-
const rw = visibleWidth(rightText);
|
|
465
|
-
|
|
466
|
-
let lines: string[];
|
|
467
|
-
if (lw + rw + 1 <= width) {
|
|
468
|
-
lines = [leftText + " ".repeat(width - lw - rw) + rightText];
|
|
469
|
-
} else {
|
|
470
|
-
lines = [
|
|
471
|
-
leftText ? truncateAnsi(leftText, width) : "",
|
|
472
|
-
rw <= width ? " ".repeat(Math.max(0, width - rw)) + rightText : truncateAnsi(rightText, width),
|
|
473
|
-
].filter((l) => l !== "");
|
|
474
|
-
}
|
|
475
|
-
this.cache = { width, lines };
|
|
476
|
-
return lines;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
dispose(): void {
|
|
480
|
-
this.disposed = true;
|
|
481
|
-
this.cache = null;
|
|
482
|
-
}
|
|
483
|
-
}
|