@owloops/claude-powerline 1.24.4 → 1.25.1

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.
Files changed (46) hide show
  1. package/dist/browser.d.ts +676 -0
  2. package/dist/browser.js +3 -0
  3. package/dist/index.mjs +10 -10
  4. package/package.json +9 -1
  5. package/plugin/templates/config-tui-compact.json +4 -4
  6. package/plugin/templates/config-tui-full.json +5 -5
  7. package/plugin/templates/config-tui-standard.json +5 -5
  8. package/src/browser.ts +203 -0
  9. package/src/config/defaults.ts +79 -0
  10. package/src/config/loader.ts +462 -0
  11. package/src/index.ts +90 -0
  12. package/src/powerline.ts +904 -0
  13. package/src/segments/block.ts +31 -0
  14. package/src/segments/context.ts +221 -0
  15. package/src/segments/git.ts +492 -0
  16. package/src/segments/index.ts +25 -0
  17. package/src/segments/metrics.ts +175 -0
  18. package/src/segments/pricing.ts +454 -0
  19. package/src/segments/renderer.ts +796 -0
  20. package/src/segments/session.ts +207 -0
  21. package/src/segments/tmux.ts +35 -0
  22. package/src/segments/today.ts +191 -0
  23. package/src/themes/dark.ts +52 -0
  24. package/src/themes/gruvbox.ts +52 -0
  25. package/src/themes/index.ts +131 -0
  26. package/src/themes/light.ts +52 -0
  27. package/src/themes/nord.ts +52 -0
  28. package/src/themes/rose-pine.ts +52 -0
  29. package/src/themes/tokyo-night.ts +52 -0
  30. package/src/tui/grid.ts +712 -0
  31. package/src/tui/index.ts +4 -0
  32. package/src/tui/layouts.ts +285 -0
  33. package/src/tui/primitives.ts +175 -0
  34. package/src/tui/renderer.ts +206 -0
  35. package/src/tui/sections.ts +1080 -0
  36. package/src/tui/types.ts +181 -0
  37. package/src/utils/budget.ts +47 -0
  38. package/src/utils/cache.ts +247 -0
  39. package/src/utils/claude.ts +489 -0
  40. package/src/utils/color-support.ts +118 -0
  41. package/src/utils/colors.ts +120 -0
  42. package/src/utils/constants.ts +176 -0
  43. package/src/utils/formatters.ts +160 -0
  44. package/src/utils/logger.ts +5 -0
  45. package/src/utils/terminal-width.ts +117 -0
  46. package/src/utils/terminal.ts +11 -0
@@ -0,0 +1,285 @@
1
+ import type { RenderCtx } from "./types";
2
+
3
+ import { formatCost } from "../utils/formatters";
4
+ import {
5
+ contentRow,
6
+ divider,
7
+ spreadEven,
8
+ spreadTwo,
9
+ colorize,
10
+ } from "./primitives";
11
+ import {
12
+ collectMetricSegments,
13
+ collectActivityParts,
14
+ collectWorkspaceParts,
15
+ collectFooterParts,
16
+ formatBlockSegment,
17
+ formatWeeklySegment,
18
+ formatSessionSegment,
19
+ formatTodaySegment,
20
+ } from "./sections";
21
+
22
+ // --- Wide layout (80+ cols): metrics on 1 line, workspace+footer on 1 line ---
23
+
24
+ export function renderWideMetrics(ctx: RenderCtx): void {
25
+ const {
26
+ lines,
27
+ data,
28
+ box,
29
+ contentWidth,
30
+ innerWidth,
31
+ sym,
32
+ config,
33
+ reset,
34
+ colors,
35
+ } = ctx;
36
+ const segments = collectMetricSegments(data, sym, config, reset, colors);
37
+ if (segments.length > 0) {
38
+ lines.push(contentRow(box, spreadEven(segments, contentWidth), innerWidth));
39
+ }
40
+ }
41
+
42
+ export function renderWideBottom(ctx: RenderCtx): void {
43
+ const {
44
+ lines,
45
+ data,
46
+ box,
47
+ contentWidth,
48
+ innerWidth,
49
+ sym,
50
+ config,
51
+ reset,
52
+ colors,
53
+ } = ctx;
54
+ const leftParts = collectWorkspaceParts(data, sym, reset, colors);
55
+ const rightParts = collectFooterParts(data, sym, config, reset, colors);
56
+
57
+ const leftStr = leftParts.join(" ");
58
+ const rightStr = rightParts.join(" · ");
59
+
60
+ if (leftStr || rightStr) {
61
+ lines.push(divider(box, innerWidth));
62
+ lines.push(
63
+ contentRow(box, spreadTwo(leftStr, rightStr, contentWidth), innerWidth),
64
+ );
65
+ }
66
+ }
67
+
68
+ // --- Medium layout (55-79 cols): metrics on 2 lines, workspace and footer separate ---
69
+
70
+ export function renderMediumMetrics(ctx: RenderCtx): void {
71
+ const {
72
+ lines,
73
+ data,
74
+ box,
75
+ contentWidth,
76
+ innerWidth,
77
+ sym,
78
+ config,
79
+ reset,
80
+ colors,
81
+ } = ctx;
82
+ const line1Parts: string[] = [];
83
+ const line2Parts: string[] = [];
84
+
85
+ if (data.blockInfo) {
86
+ line1Parts.push(
87
+ colorize(
88
+ formatBlockSegment(data.blockInfo, sym, config),
89
+ colors.blockFg,
90
+ reset,
91
+ ),
92
+ );
93
+ }
94
+ const sevenDay = data.hookData.rate_limits?.seven_day;
95
+ if (sevenDay) {
96
+ line1Parts.push(
97
+ colorize(formatWeeklySegment(sevenDay, sym), colors.weeklyFg, reset),
98
+ );
99
+ }
100
+ if (data.todayInfo) {
101
+ line1Parts.push(
102
+ colorize(
103
+ formatTodaySegment(data.todayInfo, sym, config),
104
+ colors.todayFg,
105
+ reset,
106
+ ),
107
+ );
108
+ }
109
+
110
+ if (data.usageInfo) {
111
+ line2Parts.push(
112
+ colorize(
113
+ formatSessionSegment(data.usageInfo, sym, config),
114
+ colors.sessionFg,
115
+ reset,
116
+ ),
117
+ );
118
+ }
119
+ const activityParts = collectActivityParts(data, sym);
120
+ if (activityParts.length > 0) {
121
+ line2Parts.push(
122
+ colorize(activityParts.join(" · "), colors.metricsFg, reset),
123
+ );
124
+ }
125
+
126
+ if (line1Parts.length > 0) {
127
+ lines.push(
128
+ contentRow(box, spreadEven(line1Parts, contentWidth), innerWidth),
129
+ );
130
+ }
131
+ if (line2Parts.length > 0) {
132
+ lines.push(
133
+ contentRow(
134
+ box,
135
+ spreadTwo(line2Parts[0] ?? "", line2Parts[1] ?? "", contentWidth),
136
+ innerWidth,
137
+ ),
138
+ );
139
+ }
140
+ }
141
+
142
+ export function renderMediumBottom(ctx: RenderCtx): void {
143
+ const {
144
+ lines,
145
+ data,
146
+ box,
147
+ contentWidth,
148
+ innerWidth,
149
+ sym,
150
+ config,
151
+ reset,
152
+ colors,
153
+ } = ctx;
154
+ const workspaceParts = collectWorkspaceParts(data, sym, reset, colors);
155
+ if (workspaceParts.length > 0) {
156
+ lines.push(divider(box, innerWidth));
157
+ lines.push(
158
+ contentRow(
159
+ box,
160
+ spreadTwo(
161
+ workspaceParts[0] ?? "",
162
+ workspaceParts[1] ?? "",
163
+ contentWidth,
164
+ ),
165
+ innerWidth,
166
+ ),
167
+ );
168
+ }
169
+
170
+ const footerParts = collectFooterParts(data, sym, config, reset, colors);
171
+ if (footerParts.length > 0) {
172
+ lines.push(divider(box, innerWidth));
173
+ lines.push(contentRow(box, footerParts.join(" · "), innerWidth));
174
+ }
175
+ }
176
+
177
+ // --- Narrow layout (<55 cols): everything stacks ---
178
+
179
+ export function renderNarrowMetrics(ctx: RenderCtx): void {
180
+ const {
181
+ lines,
182
+ data,
183
+ box,
184
+ contentWidth,
185
+ innerWidth,
186
+ sym,
187
+ config,
188
+ reset,
189
+ colors,
190
+ } = ctx;
191
+ if (data.blockInfo) {
192
+ lines.push(
193
+ contentRow(
194
+ box,
195
+ colorize(
196
+ formatBlockSegment(data.blockInfo, sym, config),
197
+ colors.blockFg,
198
+ reset,
199
+ ),
200
+ innerWidth,
201
+ ),
202
+ );
203
+ }
204
+ const narrowSevenDay = data.hookData.rate_limits?.seven_day;
205
+ if (narrowSevenDay) {
206
+ lines.push(
207
+ contentRow(
208
+ box,
209
+ colorize(
210
+ formatWeeklySegment(narrowSevenDay, sym),
211
+ colors.weeklyFg,
212
+ reset,
213
+ ),
214
+ innerWidth,
215
+ ),
216
+ );
217
+ }
218
+
219
+ const sessionAndToday: string[] = [];
220
+ if (data.usageInfo) {
221
+ sessionAndToday.push(
222
+ colorize(
223
+ `${sym.session_cost} ${formatCost(data.usageInfo.session.cost)}`,
224
+ colors.sessionFg,
225
+ reset,
226
+ ),
227
+ );
228
+ }
229
+ if (data.todayInfo) {
230
+ sessionAndToday.push(
231
+ colorize(
232
+ `${sym.today_cost} ${formatCost(data.todayInfo.cost)} today`,
233
+ colors.todayFg,
234
+ reset,
235
+ ),
236
+ );
237
+ }
238
+ if (sessionAndToday.length > 0) {
239
+ lines.push(
240
+ contentRow(
241
+ box,
242
+ spreadTwo(
243
+ sessionAndToday[0] ?? "",
244
+ sessionAndToday[1] ?? "",
245
+ contentWidth,
246
+ ),
247
+ innerWidth,
248
+ ),
249
+ );
250
+ }
251
+ }
252
+
253
+ export function renderNarrowBottom(ctx: RenderCtx): void {
254
+ const {
255
+ lines,
256
+ data,
257
+ box,
258
+ contentWidth,
259
+ innerWidth,
260
+ sym,
261
+ config,
262
+ reset,
263
+ colors,
264
+ } = ctx;
265
+ const workspaceParts = collectWorkspaceParts(data, sym, reset, colors);
266
+ if (workspaceParts.length > 0) {
267
+ lines.push(divider(box, innerWidth));
268
+ lines.push(
269
+ contentRow(
270
+ box,
271
+ spreadTwo(
272
+ workspaceParts[0] ?? "",
273
+ workspaceParts[1] ?? "",
274
+ contentWidth,
275
+ ),
276
+ innerWidth,
277
+ ),
278
+ );
279
+ }
280
+
281
+ const footerParts = collectFooterParts(data, sym, config, reset, colors);
282
+ if (footerParts.length > 0) {
283
+ lines.push(contentRow(box, footerParts.join(" · "), innerWidth));
284
+ }
285
+ }
@@ -0,0 +1,175 @@
1
+ import type { BoxChars } from "./types";
2
+
3
+ import { visibleLength, stripAnsi, ESC, ANSI_SPLIT } from "../utils/terminal";
4
+
5
+ export function colorize(text: string, fgColor: string, reset: string): string {
6
+ if (!fgColor) {
7
+ return text;
8
+ }
9
+ return `${fgColor}${text}${reset}`;
10
+ }
11
+
12
+ export function padRight(text: string, width: number): string {
13
+ const visible = visibleLength(text);
14
+ if (visible >= width) {
15
+ return text;
16
+ }
17
+ return text + " ".repeat(width - visible);
18
+ }
19
+
20
+ export function padLeft(text: string, width: number): string {
21
+ const visible = visibleLength(text);
22
+ if (visible >= width) {
23
+ return text;
24
+ }
25
+ return " ".repeat(width - visible) + text;
26
+ }
27
+
28
+ export function padCenter(text: string, width: number): string {
29
+ const visible = visibleLength(text);
30
+ if (visible >= width) {
31
+ return text;
32
+ }
33
+ const totalPad = width - visible;
34
+ const leftPad = Math.floor(totalPad / 2);
35
+ const rightPad = totalPad - leftPad;
36
+ return " ".repeat(leftPad) + text + " ".repeat(rightPad);
37
+ }
38
+
39
+ export function truncateAnsi(text: string, maxWidth: number): string {
40
+ if (stripAnsi(text).length <= maxWidth) {
41
+ return text;
42
+ }
43
+
44
+ let width = 0;
45
+ let result = "";
46
+ const parts = text.split(ANSI_SPLIT);
47
+ for (const part of parts) {
48
+ if (part.startsWith(ESC)) {
49
+ result += part;
50
+ continue;
51
+ }
52
+ for (const char of part) {
53
+ if (width >= maxWidth - 1) {
54
+ result += "…\x1b[0m";
55
+ return result;
56
+ }
57
+ result += char;
58
+ width++;
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+
64
+ export function contentRow(
65
+ box: BoxChars,
66
+ content: string,
67
+ innerWidth: number,
68
+ ): string {
69
+ const maxContent = innerWidth - 2;
70
+ const truncated = truncateAnsi(content, maxContent);
71
+ const padded = padRight(truncated, maxContent);
72
+ return box.vertical + " " + padded + " " + box.vertical;
73
+ }
74
+
75
+ export function divider(box: BoxChars, innerWidth: number): string {
76
+ return box.teeLeft + box.horizontal.repeat(innerWidth) + box.teeRight;
77
+ }
78
+
79
+ export function bottomBorder(
80
+ box: BoxChars,
81
+ innerWidth: number,
82
+ leftText?: string,
83
+ rightText?: string,
84
+ ): string {
85
+ if (!leftText && !rightText) {
86
+ return box.bottomLeft + box.horizontal.repeat(innerWidth) + box.bottomRight;
87
+ }
88
+
89
+ let left = leftText ? ` ${leftText} ` : "";
90
+ let right = rightText ? ` ${rightText} ` : "";
91
+ let leftLen = visibleLength(left);
92
+ let rightLen = visibleLength(right);
93
+
94
+ // Truncate if combined text exceeds innerWidth
95
+ if (leftLen + rightLen > innerWidth) {
96
+ const maxLeft = Math.max(0, innerWidth - rightLen);
97
+ if (leftLen > maxLeft) {
98
+ left = truncateAnsi(left, maxLeft);
99
+ leftLen = visibleLength(left);
100
+ }
101
+ if (leftLen + rightLen > innerWidth) {
102
+ const maxRight = Math.max(0, innerWidth - leftLen);
103
+ right = truncateAnsi(right, maxRight);
104
+ rightLen = visibleLength(right);
105
+ }
106
+ }
107
+
108
+ const fillCount = innerWidth - leftLen - rightLen;
109
+
110
+ return (
111
+ box.bottomLeft +
112
+ left +
113
+ box.horizontal.repeat(Math.max(0, fillCount)) +
114
+ right +
115
+ box.bottomRight
116
+ );
117
+ }
118
+
119
+ export function spreadEven(parts: string[], totalWidth: number): string {
120
+ if (parts.length === 0) {
121
+ return "";
122
+ }
123
+ if (parts.length === 1) {
124
+ return parts[0] ?? "";
125
+ }
126
+
127
+ const widths = parts.map((p) => visibleLength(p));
128
+ const totalContentWidth = widths.reduce((sum, w) => sum + w, 0);
129
+ const totalGap = totalWidth - totalContentWidth;
130
+ const gapPerSlot = Math.max(2, Math.floor(totalGap / (parts.length - 1)));
131
+
132
+ const suffixWidths = Array.from<number>({ length: parts.length });
133
+ suffixWidths[parts.length - 1] = widths[parts.length - 1] ?? 0;
134
+ for (let i = parts.length - 2; i >= 0; i--) {
135
+ suffixWidths[i] = (suffixWidths[i + 1] ?? 0) + (widths[i] ?? 0);
136
+ }
137
+
138
+ let result = parts[0] ?? "";
139
+ let usedWidth = widths[0] ?? 0;
140
+ for (let i = 1; i < parts.length; i++) {
141
+ const remaining =
142
+ totalWidth -
143
+ usedWidth -
144
+ (suffixWidths[i] ?? 0) -
145
+ (parts.length - 1 - i) * 2;
146
+ const gap = Math.max(2, Math.min(gapPerSlot, remaining));
147
+ result += " ".repeat(gap) + (parts[i] ?? "");
148
+ usedWidth += gap + (widths[i] ?? 0);
149
+ }
150
+
151
+ return result;
152
+ }
153
+
154
+ export function spreadTwo(
155
+ left: string,
156
+ right: string,
157
+ totalWidth: number,
158
+ ): string {
159
+ if (!right) {
160
+ return left;
161
+ }
162
+ if (!left) {
163
+ return right;
164
+ }
165
+
166
+ const leftLen = visibleLength(left);
167
+ const rightLen = visibleLength(right);
168
+ const gap = totalWidth - leftLen - rightLen;
169
+
170
+ if (gap < 2) {
171
+ return `${left} ${right}`;
172
+ }
173
+
174
+ return left + " ".repeat(gap) + right;
175
+ }
@@ -0,0 +1,206 @@
1
+ import type { PowerlineConfig } from "../config/loader";
2
+ import type { TuiData, BoxChars, LayoutMode, RenderCtx } from "./types";
3
+
4
+ import { SYMBOLS, TEXT_SYMBOLS, BOX_PRESETS } from "../utils/constants";
5
+ import { contentRow, bottomBorder } from "./primitives";
6
+ import {
7
+ buildTitleBar,
8
+ buildContextLine,
9
+ buildContextBar,
10
+ buildBlockBar,
11
+ buildWeeklyBar,
12
+ resolveSegments,
13
+ composeTemplate,
14
+ resolveTitleToken,
15
+ } from "./sections";
16
+ import {
17
+ renderWideMetrics,
18
+ renderWideBottom,
19
+ renderMediumMetrics,
20
+ renderMediumBottom,
21
+ renderNarrowMetrics,
22
+ renderNarrowBottom,
23
+ } from "./layouts";
24
+ import { renderGrid } from "./grid";
25
+
26
+ // Synchronized Output (DEC mode 2026): prevents tearing on multi-line renders.
27
+ // Terminals that don't support it silently ignore these sequences.
28
+ const SYNC_START = "\x1b[?2026h";
29
+ const SYNC_END = "\x1b[?2026l";
30
+
31
+ // No-op ANSI reset prepended to each line to prevent leading whitespace stripping.
32
+ // Claude Code's status line renderer strips leading spaces, but ANSI sequences at the
33
+ // start of a line protect subsequent whitespace from being trimmed.
34
+ const WS_GUARD = "\x1b[0m";
35
+
36
+ const MIN_PANEL_WIDTH = 32;
37
+ const WIDE_THRESHOLD = 80;
38
+ const MEDIUM_THRESHOLD = 55;
39
+
40
+ function getLayoutMode(panelWidth: number): LayoutMode {
41
+ if (panelWidth >= WIDE_THRESHOLD) {
42
+ return "wide";
43
+ }
44
+ if (panelWidth >= MEDIUM_THRESHOLD) {
45
+ return "medium";
46
+ }
47
+ return "narrow";
48
+ }
49
+
50
+ function calculatePanelWidth(terminalWidth: number | null): number {
51
+ if (terminalWidth && terminalWidth > 0) {
52
+ return Math.max(MIN_PANEL_WIDTH, terminalWidth);
53
+ }
54
+ return 80;
55
+ }
56
+
57
+ export interface TuiPanelOptions {
58
+ rawTerminalWidth?: number | null;
59
+ }
60
+
61
+ export async function renderTuiPanel(
62
+ data: TuiData,
63
+ box: BoxChars,
64
+ reset: string,
65
+ terminalWidth: number | null,
66
+ config: PowerlineConfig,
67
+ options?: TuiPanelOptions,
68
+ ): Promise<string> {
69
+ const sym =
70
+ (config.display.charset || "unicode") === "text" ? TEXT_SYMBOLS : SYMBOLS;
71
+ const colors = data.colors;
72
+
73
+ // Grid path: when display.tui grid config is present
74
+ if (config.display.tui) {
75
+ const gridConfig = config.display.tui;
76
+ const rawWidth =
77
+ gridConfig.terminalWidth ?? options?.rawTerminalWidth ?? 120;
78
+
79
+ // Merge box character overrides with charset defaults
80
+ // Resolve box preset name or merge partial overrides with charset defaults
81
+ let mergedBox: BoxChars;
82
+ if (typeof gridConfig.box === "string") {
83
+ mergedBox = BOX_PRESETS[gridConfig.box] ?? box;
84
+ } else {
85
+ mergedBox = gridConfig.box ? { ...box, ...gridConfig.box } : box;
86
+ }
87
+
88
+ // Estimate content width for initial segment resolution (grid will compute final widths)
89
+ const estPanelWidth = Math.max(
90
+ gridConfig.minWidth ?? MIN_PANEL_WIDTH,
91
+ rawWidth - (gridConfig.widthReserve ?? 45),
92
+ );
93
+ const estInnerWidth = estPanelWidth - 2;
94
+ const estContentWidth = estInnerWidth - 2;
95
+
96
+ const ctx: RenderCtx = {
97
+ lines: [],
98
+ data,
99
+ box: mergedBox,
100
+ contentWidth: estContentWidth,
101
+ innerWidth: estInnerWidth,
102
+ sym,
103
+ config,
104
+ reset,
105
+ colors,
106
+ };
107
+ const resolved = resolveSegments(data, ctx);
108
+ const resolvedData = resolved.data;
109
+ const templates = resolved.templates;
110
+
111
+ const pf = colors.partFg;
112
+ const lateResolve = (
113
+ segment: string,
114
+ cellWidth: number,
115
+ ): string | undefined => {
116
+ if (segment === "context") {
117
+ return buildContextLine(data, cellWidth, sym, reset, colors) ?? "";
118
+ }
119
+ if (segment === "context.bar") {
120
+ return buildContextBar(data, cellWidth, sym, reset, colors, pf);
121
+ }
122
+ if (segment === "block.bar") {
123
+ return buildBlockBar(data, cellWidth, sym, reset, colors, config, pf);
124
+ }
125
+ if (segment === "weekly.bar") {
126
+ return buildWeeklyBar(data, cellWidth, sym, reset, colors, pf);
127
+ }
128
+ const tmpl = templates[segment];
129
+ if (tmpl) {
130
+ return composeTemplate(tmpl.items, tmpl.gap, tmpl.justify, cellWidth);
131
+ }
132
+ return undefined;
133
+ };
134
+
135
+ const gridResult = renderGrid(
136
+ gridConfig,
137
+ resolvedData,
138
+ mergedBox,
139
+ rawWidth,
140
+ lateResolve,
141
+ );
142
+ const innerWidth = gridResult.panelWidth - 2;
143
+
144
+ const footerLeft = gridConfig.footer?.left
145
+ ? resolveTitleToken(gridConfig.footer.left, data, resolvedData)
146
+ : undefined;
147
+ const footerRight = gridConfig.footer?.right
148
+ ? resolveTitleToken(gridConfig.footer.right, data, resolvedData)
149
+ : undefined;
150
+
151
+ const lines: string[] = [];
152
+ lines.push(
153
+ buildTitleBar(
154
+ data,
155
+ mergedBox,
156
+ innerWidth,
157
+ gridConfig.title,
158
+ resolvedData,
159
+ ),
160
+ );
161
+ lines.push(...gridResult.lines);
162
+ lines.push(bottomBorder(mergedBox, innerWidth, footerLeft, footerRight));
163
+ return SYNC_START + lines.map((l) => WS_GUARD + l).join("\n") + SYNC_END;
164
+ }
165
+
166
+ // Hardcoded path: existing layout system
167
+ const panelWidth = calculatePanelWidth(terminalWidth);
168
+ const innerWidth = panelWidth - 2;
169
+ const contentWidth = innerWidth - 2;
170
+ const mode = getLayoutMode(panelWidth);
171
+
172
+ const lines: string[] = [];
173
+
174
+ lines.push(buildTitleBar(data, box, innerWidth));
175
+
176
+ const contextLine = buildContextLine(data, contentWidth, sym, reset, colors);
177
+ if (contextLine) {
178
+ lines.push(contentRow(box, contextLine, innerWidth));
179
+ }
180
+
181
+ const ctx: RenderCtx = {
182
+ lines,
183
+ data,
184
+ box,
185
+ contentWidth,
186
+ innerWidth,
187
+ sym,
188
+ config,
189
+ reset,
190
+ colors,
191
+ };
192
+
193
+ if (mode === "wide") {
194
+ renderWideMetrics(ctx);
195
+ renderWideBottom(ctx);
196
+ } else if (mode === "medium") {
197
+ renderMediumMetrics(ctx);
198
+ renderMediumBottom(ctx);
199
+ } else {
200
+ renderNarrowMetrics(ctx);
201
+ renderNarrowBottom(ctx);
202
+ }
203
+
204
+ lines.push(bottomBorder(box, innerWidth));
205
+ return SYNC_START + lines.map((l) => WS_GUARD + l).join("\n") + SYNC_END;
206
+ }