Files
pi-statusline/modules/usage.ts

153 lines
5.7 KiB
TypeScript

import type { ModuleContext, ModuleSpec, RenderedModule, UsageTextPart, UsageWindows } from "../types";
import type { UsageLimit, UsageReport } from "../usage";
function usageFraction(limit: UsageLimit): number | undefined {
if (limit.amount.usedFraction !== undefined) return Math.min(Math.max(limit.amount.usedFraction, 0), 1);
if (limit.amount.used !== undefined && limit.amount.limit !== undefined && limit.amount.limit > 0) {
return Math.min(Math.max(limit.amount.used / limit.amount.limit, 0), 1);
}
return undefined;
}
function duration(valueMs: number | undefined): string | undefined {
if (valueMs === undefined || !Number.isFinite(valueMs)) return undefined;
const ms = Math.max(0, valueMs);
const days = Math.floor(ms / 86_400_000);
const hours = Math.floor((ms % 86_400_000) / 3_600_000);
const minutes = Math.round((ms % 3_600_000) / 60_000);
if (days > 0) return `${days}d${hours}h`;
if (hours > 0) return `${hours}h${minutes}m`;
return `${minutes}m`;
}
function compactNumber(value: number | undefined): string | undefined {
if (value === undefined) return undefined;
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
return `${Math.round(value)}`;
}
function lerp(start: number, end: number, amount: number): number {
return Math.round(start + (end - start) * amount);
}
function fgRgb(text: string, r: number, g: number, b: number): string {
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
}
function usageColor(fraction: number): { r: number; g: number; b: number } {
const green = { r: 34, g: 197, b: 94 };
const yellow = { r: 234, g: 179, b: 8 };
const red = { r: 239, g: 68, b: 68 };
// Continuous Green → Yellow → Red Gradient
if (fraction <= 0.5) {
const amount = fraction / 0.5;
return {
r: lerp(green.r, yellow.r, amount),
g: lerp(green.g, yellow.g, amount),
b: lerp(green.b, yellow.b, amount),
};
}
const amount = (fraction - 0.5) / 0.5;
return {
r: lerp(yellow.r, red.r, amount),
g: lerp(yellow.g, red.g, amount),
b: lerp(yellow.b, red.b, amount),
};
}
function findWindowLimits(report: UsageReport): UsageWindows {
const windowId = (limit: UsageLimit) => (limit.window?.id ?? limit.scope.windowId ?? "").toLowerCase();
const current = report.limits.find(limit => ["5h", "primary"].includes(windowId(limit))) ?? report.limits[0];
const week = report.limits.find(limit => ["7d", "secondary"].includes(windowId(limit))) ?? report.limits.find(limit => limit !== current);
return { current, week };
}
function lineBar(limit: UsageLimit | undefined, width: number, theme?: any, color = true): string {
if (width <= 0) return "";
const fraction = limit ? usageFraction(limit) ?? 0 : 0;
const filled = Math.round(fraction * width);
const empty = width - filled;
const filledText = "━".repeat(filled);
const emptyText = "─".repeat(empty);
if (!color) return `${filledText}${emptyText}`;
const { r, g, b } = usageColor(fraction);
const coloredFilled = filledText ? fgRgb(filledText, r, g, b) : "";
const dimEmpty = emptyText && theme?.fg ? theme.fg("dim", emptyText) : emptyText;
return `${coloredFilled}${dimEmpty}`;
}
function selectedLimit(moduleCtx: ModuleContext, spec: ModuleSpec): UsageLimit | undefined {
if (!moduleCtx.state.report || moduleCtx.state.error) return undefined;
const windows = findWindowLimits(moduleCtx.state.report);
const window = typeof spec === "string" ? "both" : spec.window ?? "both";
if (window === "week") return windows.week;
return windows.current ?? windows.week;
}
function textPart(limit: UsageLimit, part: UsageTextPart): string | undefined {
const fraction = usageFraction(limit);
switch (part) {
case "percent":
return fraction === undefined ? undefined : `${Math.round(fraction * 100)}%`;
case "time":
return duration(limit.window?.resetsAt !== undefined ? limit.window.resetsAt - Date.now() : undefined);
case "used":
return compactNumber(limit.amount.used);
case "limit":
return compactNumber(limit.amount.limit);
case "remaining":
return compactNumber(limit.amount.remaining);
}
}
function fallbackTextPart(part: UsageTextPart): string | undefined {
switch (part) {
case "percent":
return "0%";
case "time":
return "∞";
case "used":
return "0";
case "remaining":
return "∞";
case "limit":
return undefined;
}
}
function textUsage(moduleCtx: ModuleContext, spec: ModuleSpec): string {
// Usage Text Parts
const parts = typeof spec === "string"
? moduleCtx.config.modules.usage?.parts ?? ["percent", "time"]
: spec.parts ?? moduleCtx.config.modules.usage?.parts ?? ["percent", "time"];
const separator = typeof spec === "string"
? moduleCtx.config.modules.usage?.separator ?? " | "
: spec.separator ?? moduleCtx.config.modules.usage?.separator ?? " | ";
const limit = !moduleCtx.state.report || moduleCtx.state.error ? undefined : selectedLimit(moduleCtx, spec);
return parts
.map(part => limit ? textPart(limit, part) : fallbackTextPart(part))
.filter((part): part is string => Boolean(part))
.join(separator);
}
export function usageModule(moduleCtx: ModuleContext, spec: ModuleSpec): RenderedModule {
const style = typeof spec === "string" ? "line" : spec.style ?? "line";
const grow = typeof spec === "string" ? 1 : spec.grow ?? 1;
if (style === "text") return { text: textUsage(moduleCtx, spec) };
return {
grow,
render(width: number, theme?: any): string {
const limit = !moduleCtx.state.report || moduleCtx.state.error ? undefined : selectedLimit(moduleCtx, spec);
return lineBar(limit, width, theme, moduleCtx.config.modules.usage?.colorBars !== false);
},
};
}