termino
~/docs/custom/token-meter

TokenMetercustom

Context window usage bar with zone colors and io stats

~/custom/token-meter
live
$ termino token-meter — context window usage
────────────────────────────────
ctx 120k / 200k 60%
██████████████████
──────────────────────────────
61.0k↑ 8.4k↓ 16.8k☍
────────────────────────────────
live fills to 198k then resets — color zones at 60% / 85%

##API

Raised label row + recessed (inset) track: dark well with the fill as █ bars. Fill color crosses green → yellow at `warnAt` → red at `dangerAt`. Bottom stats row shows ↑input ↓output ☍cache.

properties
proptypedefaultdescription
used / totalnumber0 / 200000Window usage
input / output / cachednumber0Per-direction token stats
widthnumber34Track width
warnAt / dangerAtnumber0.6 / 0.85Zone thresholds
colorstring"#a5d98f"Fill color under warnAt
colorsPartial<NeoColors>NEOBevel palette

##Keymap

~/keymap
Display only

##Notes

  • Inset track is the recessed twin of the raised panels — light/dark edges flipped.
  • At 85% the fill goes red: context pressure at a glance, LSP-server style.

##Source

Real implementation — this is the code that runs in your terminal.

tsxtoken-meter.tsx
1import { createElement as h, useMemo } from "react";
2import { Canvas } from "./canvas";
3import { cellsWidth, strWidth, type CursorCell } from "./chart";
4import { NEO, neoFill, neoPanel, type NeoColors } from "./neo";
5
6export interface TokenMeterProps {
7 used?: number;
8 total?: number;
9 input?: number;
10 output?: number;
11 cached?: number;
12 label?: string;
13 width?: number;
14 color?: string;
15 warnAt?: number;
16 dangerAt?: number;
17 colors?: Partial<NeoColors>;
18}
19
20export function zoneColor(ratio: number, warnAt: number, dangerAt: number, base: string): string {
21 if (ratio >= dangerAt) return "#f0929e";
22 if (ratio >= warnAt) return "#f0c674";
23 return base;
24}
25
26export function TokenMeter({
27 used = 0,
28 total = 200000,
29 input = 0,
30 output = 0,
31 cached = 0,
32 label = "ctx",
33 width = 34,
34 color = "#a5d98f",
35 warnAt = 0.6,
36 dangerAt = 0.85,
37 colors,
38}: Readonly<TokenMeterProps>) {
39 const c = { ...NEO, ...colors };
40 const ratio = total > 0 ? used / total : 0;
41 const fillColor = zoneColor(ratio, warnAt, dangerAt, color);
42
43 const rows = useMemo(() => {
44 const fmt = (n: number) => `${(n / 1000).toFixed(n >= 100000 ? 0 : 1)}k`;
45 const labelRow: CursorCell[] = [
46 { ch: `${label} `, fg: c.dim },
47 { ch: `${fmt(used)}`, fg: fillColor },
48 { ch: ` / ${fmt(total)}`, fg: c.dim },
49 ];
50 const pct = `${Math.round(ratio * 100)}%`;
51 const pctCell = ` ${pct}`;
52 while (cellsWidth(labelRow) < width + 2 - strWidth(pctCell)) {
53 labelRow.push({ ch: " ", fg: c.text });
54 }
55 labelRow.push({ ch: pctCell, fg: fillColor });
56
57 const fill = neoFill(used, total, width, fillColor, c);
58 const body: CursorCell[][] = [labelRow];
59 body.push([
60 { ch: "╭", fg: c.dark },
61 ...fill,
62 { ch: "╮", fg: c.dark },
63 ]);
64 body.push([
65 { ch: "╰", fg: c.light },
66 { ch: "─".repeat(width), fg: c.light },
67 { ch: "╯", fg: c.light },
68 ]);
69 if (input || output || cached) {
70 const stats = [
71 input ? `${fmt(input)}↑` : "",
72 output ? `${fmt(output)}↓` : "",
73 cached ? `${fmt(cached)}☍` : "",
74 ]
75 .filter(Boolean)
76 .join(" ");
77 body.push([{ ch: ` ${stats}`, fg: c.dim }]);
78 }
79 return body;
80 // eslint-disable-next-line react-hooks/exhaustive-deps
81 }, [used, total, input, output, cached, label, width, fillColor, color, colors]);
82
83 const panel = neoPanel(rows, c);
84 const widthFix = cellsWidth(panel[0] ?? []);
85 return h("box", { flexDirection: "column", gap: 0, width: widthFix }, h(Canvas, { rows: panel, width: widthFix }));
86}
87