@bastra-recall/statusline 0.7.0-beta.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/bin/claude-powerline +2 -0
  4. package/dist/browser.d.ts +796 -0
  5. package/dist/browser.js +3 -0
  6. package/dist/index.mjs +48 -0
  7. package/package.json +39 -0
  8. package/src/browser.ts +208 -0
  9. package/src/config/defaults.ts +88 -0
  10. package/src/config/loader.ts +473 -0
  11. package/src/index.ts +90 -0
  12. package/src/powerline.ts +1082 -0
  13. package/src/segments/bastra.ts +123 -0
  14. package/src/segments/block.ts +31 -0
  15. package/src/segments/cacheTimer.ts +108 -0
  16. package/src/segments/context.ts +221 -0
  17. package/src/segments/git.ts +498 -0
  18. package/src/segments/index.ts +33 -0
  19. package/src/segments/metrics.ts +175 -0
  20. package/src/segments/pricing.ts +454 -0
  21. package/src/segments/renderer.ts +1002 -0
  22. package/src/segments/session.ts +207 -0
  23. package/src/segments/tmux.ts +35 -0
  24. package/src/segments/today.ts +191 -0
  25. package/src/themes/dark.ts +61 -0
  26. package/src/themes/gruvbox.ts +61 -0
  27. package/src/themes/index.ts +161 -0
  28. package/src/themes/light.ts +61 -0
  29. package/src/themes/nord.ts +61 -0
  30. package/src/themes/rose-pine.ts +61 -0
  31. package/src/themes/tokyo-night.ts +61 -0
  32. package/src/tui/grid.ts +712 -0
  33. package/src/tui/index.ts +4 -0
  34. package/src/tui/layouts.ts +321 -0
  35. package/src/tui/primitives.ts +187 -0
  36. package/src/tui/renderer.ts +206 -0
  37. package/src/tui/sections.ts +1511 -0
  38. package/src/tui/types.ts +189 -0
  39. package/src/utils/budget.ts +116 -0
  40. package/src/utils/cache.ts +247 -0
  41. package/src/utils/claude.ts +518 -0
  42. package/src/utils/color-support.ts +118 -0
  43. package/src/utils/colors.ts +120 -0
  44. package/src/utils/constants.ts +184 -0
  45. package/src/utils/formatters.ts +175 -0
  46. package/src/utils/icon-visibility.ts +31 -0
  47. package/src/utils/logger.ts +5 -0
  48. package/src/utils/terminal-width.ts +117 -0
  49. package/src/utils/terminal.ts +54 -0
@@ -0,0 +1,1511 @@
1
+ import type { PowerlineConfig } from "../config/loader";
2
+ import type { PowerlineColors } from "../themes";
3
+ import type {
4
+ TuiData,
5
+ SymbolSet,
6
+ BoxChars,
7
+ RenderCtx,
8
+ SegmentTemplate,
9
+ JustifyValue,
10
+ TuiTitleConfig,
11
+ } from "./types";
12
+ import { visibleLength } from "../utils/terminal";
13
+
14
+ import {
15
+ formatCost,
16
+ formatTokenCount,
17
+ collapseHome,
18
+ formatDuration,
19
+ formatModelName,
20
+ formatResponseTime,
21
+ formatTimeRemaining,
22
+ formatLongTimeRemaining,
23
+ minutesUntilReset,
24
+ abbreviateFishStyle,
25
+ formatCacheTimerElapsed,
26
+ } from "../utils/formatters";
27
+ import { resolveBudgetDisplay } from "../utils/budget";
28
+ import { colorize, truncateAnsi } from "./primitives";
29
+ import { getEffortLevel, getThinkingEnabled } from "../utils/claude";
30
+ import { resolveIconVisibility } from "../utils/icon-visibility";
31
+
32
+ export function resolveTitleToken(
33
+ template: string,
34
+ data: TuiData,
35
+ resolvedData?: Record<string, string>,
36
+ ): string {
37
+ const rawName = data.hookData.model?.display_name || "Claude";
38
+ const modelName = formatModelName(rawName).toLowerCase();
39
+
40
+ return template.replace(/\{([^}]+)\}/g, (_match, token: string) => {
41
+ if (resolvedData) {
42
+ const value = resolvedData[token];
43
+ if (value !== undefined) return value;
44
+ }
45
+ if (token === "model") return modelName;
46
+ return "";
47
+ });
48
+ }
49
+
50
+ export function buildTitleBar(
51
+ data: TuiData,
52
+ box: BoxChars,
53
+ innerWidth: number,
54
+ titleConfig?: TuiTitleConfig,
55
+ resolvedData?: Record<string, string>,
56
+ ): string {
57
+ const leftTemplate = titleConfig?.left ?? "{model}";
58
+ const rightTemplate = titleConfig?.right;
59
+ const leftResolved = resolveTitleToken(leftTemplate, data, resolvedData);
60
+ const leftText = leftResolved ? ` ${leftResolved} ` : "";
61
+ const leftLen = visibleLength(leftText);
62
+
63
+ if (!rightTemplate) {
64
+ const simpleFill = innerWidth - leftLen;
65
+ return (
66
+ box.topLeft +
67
+ leftText +
68
+ box.horizontal.repeat(Math.max(0, simpleFill)) +
69
+ box.topRight
70
+ );
71
+ }
72
+
73
+ const rightResolved = resolveTitleToken(rightTemplate, data, resolvedData);
74
+ const rightText = rightResolved ? ` ${rightResolved} ` : "";
75
+ const rightLen = visibleLength(rightText);
76
+
77
+ // Truncate if combined text exceeds innerWidth
78
+ let finalLeft = leftText;
79
+ let finalLeftLen = leftLen;
80
+ let finalRight = rightText;
81
+ let finalRightLen = rightLen;
82
+
83
+ if (finalLeftLen + finalRightLen > innerWidth) {
84
+ const maxLeft = Math.max(0, innerWidth - finalRightLen);
85
+ if (finalLeftLen > maxLeft) {
86
+ finalLeft = truncateAnsi(finalLeft, maxLeft);
87
+ finalLeftLen = visibleLength(finalLeft);
88
+ }
89
+ if (finalLeftLen + finalRightLen > innerWidth) {
90
+ const maxRight = Math.max(0, innerWidth - finalLeftLen);
91
+ finalRight = truncateAnsi(finalRight, maxRight);
92
+ finalRightLen = visibleLength(finalRight);
93
+ }
94
+ }
95
+
96
+ const fillCount = innerWidth - finalLeftLen - finalRightLen;
97
+
98
+ if (fillCount < 2) {
99
+ const simpleFill = innerWidth - finalLeftLen;
100
+ return (
101
+ box.topLeft +
102
+ finalLeft +
103
+ box.horizontal.repeat(Math.max(0, simpleFill)) +
104
+ box.topRight
105
+ );
106
+ }
107
+
108
+ return (
109
+ box.topLeft +
110
+ finalLeft +
111
+ box.horizontal.repeat(fillCount) +
112
+ finalRight +
113
+ box.topRight
114
+ );
115
+ }
116
+
117
+ function resolveThresholdStyle(
118
+ pct: number,
119
+ defaultFg: string,
120
+ defaultBold: boolean,
121
+ colors: PowerlineColors,
122
+ warningAt = 60,
123
+ criticalAt = 80,
124
+ ): { fg: string; bold: boolean } {
125
+ if (pct >= criticalAt)
126
+ return { fg: colors.contextCriticalFg, bold: colors.contextCriticalBold };
127
+ if (pct >= warningAt)
128
+ return { fg: colors.contextWarningFg, bold: colors.contextWarningBold };
129
+ return { fg: defaultFg, bold: defaultBold };
130
+ }
131
+
132
+ function buildBarString(
133
+ pct: number,
134
+ barWidth: number,
135
+ sym: SymbolSet,
136
+ reset: string,
137
+ fgColor: string,
138
+ bold = false,
139
+ ): string {
140
+ barWidth = Math.max(5, barWidth);
141
+ const filledCount = Math.max(
142
+ 0,
143
+ Math.min(barWidth, Math.round((pct / 100) * barWidth)),
144
+ );
145
+ const emptyCount = barWidth - filledCount;
146
+ const bar =
147
+ sym.bar_filled.repeat(filledCount) + sym.bar_empty.repeat(emptyCount);
148
+ return colorize(bar, fgColor, reset, bold);
149
+ }
150
+
151
+ export function formatContextParts(
152
+ data: TuiData,
153
+ sym: SymbolSet,
154
+ iconVisible = true,
155
+ ): Record<string, string> {
156
+ if (!data.contextInfo)
157
+ return { icon: "", label: "context", bar: "", pct: "", tokens: "" };
158
+
159
+ const usedPct = data.contextInfo.usablePercentage;
160
+ const tokenStr = formatTokenCount(data.contextInfo.totalTokens);
161
+ const maxStr = formatTokenCount(data.contextInfo.maxTokens);
162
+
163
+ return {
164
+ icon: iconVisible ? sym.context_time : "",
165
+ label: "context",
166
+ bar: " ",
167
+ pct: `${usedPct}%`,
168
+ tokens: `${tokenStr}/${maxStr}`,
169
+ };
170
+ }
171
+
172
+ export function buildContextBar(
173
+ data: TuiData,
174
+ barWidth: number,
175
+ sym: SymbolSet,
176
+ reset: string,
177
+ colors: PowerlineColors,
178
+ partFg?: Record<string, string>,
179
+ ): string {
180
+ if (!data.contextInfo) return "";
181
+ const usedPct = data.contextInfo.usablePercentage;
182
+ const defaultFg =
183
+ partFg?.["context.bar"] ?? partFg?.["context"] ?? colors.contextFg;
184
+ const { fg, bold } = resolveThresholdStyle(
185
+ usedPct,
186
+ defaultFg,
187
+ colors.contextBold,
188
+ colors,
189
+ );
190
+ return buildBarString(usedPct, barWidth, sym, reset, fg, bold);
191
+ }
192
+
193
+ export function buildBlockBar(
194
+ data: TuiData,
195
+ barWidth: number,
196
+ sym: SymbolSet,
197
+ reset: string,
198
+ colors: PowerlineColors,
199
+ config: PowerlineConfig,
200
+ partFg?: Record<string, string>,
201
+ ): string {
202
+ if (!data.blockInfo) return "";
203
+
204
+ const pct = data.blockInfo.nativeUtilization;
205
+ const warningThreshold = config.budget?.block?.warningThreshold ?? 80;
206
+ const defaultFg =
207
+ partFg?.["block.bar"] ?? partFg?.["block"] ?? colors.blockFg;
208
+ const { fg, bold } = resolveThresholdStyle(
209
+ pct,
210
+ defaultFg,
211
+ colors.blockBold,
212
+ colors,
213
+ 50,
214
+ warningThreshold,
215
+ );
216
+ return buildBarString(pct, barWidth, sym, reset, fg, bold);
217
+ }
218
+
219
+ export function buildWeeklyBar(
220
+ data: TuiData,
221
+ barWidth: number,
222
+ sym: SymbolSet,
223
+ reset: string,
224
+ colors: PowerlineColors,
225
+ partFg?: Record<string, string>,
226
+ ): string {
227
+ const sevenDay = data.hookData.rate_limits?.seven_day;
228
+ if (!sevenDay) return "";
229
+
230
+ const pct = sevenDay.used_percentage;
231
+ const defaultFg =
232
+ partFg?.["weekly.bar"] ?? partFg?.["weekly"] ?? colors.weeklyFg;
233
+ const { fg, bold } = resolveThresholdStyle(
234
+ pct,
235
+ defaultFg,
236
+ colors.weeklyBold,
237
+ colors,
238
+ );
239
+ return buildBarString(pct, barWidth, sym, reset, fg, bold);
240
+ }
241
+
242
+ export function buildContextLine(
243
+ data: TuiData,
244
+ contentWidth: number,
245
+ sym: SymbolSet,
246
+ reset: string,
247
+ colors: PowerlineColors,
248
+ ): string | null {
249
+ if (!data.contextInfo) {
250
+ return null;
251
+ }
252
+
253
+ const usedPct = data.contextInfo.usablePercentage;
254
+ const tokenStr = formatTokenCount(data.contextInfo.totalTokens);
255
+ const maxStr = formatTokenCount(data.contextInfo.maxTokens);
256
+ const suffix = ` ${usedPct}% ${tokenStr}/${maxStr}`;
257
+ const barLen = Math.max(5, contentWidth - suffix.length);
258
+ const filledCount = Math.max(
259
+ 0,
260
+ Math.min(barLen, Math.round((usedPct / 100) * barLen)),
261
+ );
262
+ const emptyCount = barLen - filledCount;
263
+ const bar =
264
+ sym.bar_filled.repeat(filledCount) + sym.bar_empty.repeat(emptyCount);
265
+
266
+ const { fg, bold } = resolveThresholdStyle(
267
+ usedPct,
268
+ colors.contextFg,
269
+ colors.contextBold,
270
+ colors,
271
+ );
272
+
273
+ return colorize(`${bar}${suffix}`, fg, reset, bold);
274
+ }
275
+
276
+ function getDirectoryDisplay(hookData: TuiData["hookData"]): string {
277
+ const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
278
+ return collapseHome(currentDir);
279
+ }
280
+
281
+ export function collectMetricSegments(
282
+ data: TuiData,
283
+ sym: SymbolSet,
284
+ config: PowerlineConfig,
285
+ reset: string,
286
+ colors: PowerlineColors,
287
+ ): string[] {
288
+ const segments: string[] = [];
289
+
290
+ if (data.blockInfo) {
291
+ segments.push(
292
+ colorize(
293
+ formatBlockSegment(
294
+ data.blockInfo,
295
+ sym,
296
+ config,
297
+ resolveIconVisibility(config, "block"),
298
+ ),
299
+ colors.blockFg,
300
+ reset,
301
+ colors.blockBold,
302
+ ),
303
+ );
304
+ }
305
+ const sevenDay = data.hookData.rate_limits?.seven_day;
306
+ if (sevenDay) {
307
+ segments.push(
308
+ colorize(
309
+ formatWeeklySegment(
310
+ sevenDay,
311
+ sym,
312
+ resolveIconVisibility(config, "weekly"),
313
+ ),
314
+ colors.weeklyFg,
315
+ reset,
316
+ colors.weeklyBold,
317
+ ),
318
+ );
319
+ }
320
+ if (data.usageInfo) {
321
+ const sessionStr = formatSessionSegment(
322
+ data.usageInfo,
323
+ sym,
324
+ config,
325
+ resolveIconVisibility(config, "session"),
326
+ );
327
+ if (sessionStr) {
328
+ segments.push(
329
+ colorize(sessionStr, colors.sessionFg, reset, colors.sessionBold),
330
+ );
331
+ }
332
+ }
333
+ if (data.todayInfo) {
334
+ const todayStr = formatTodaySegment(
335
+ data.todayInfo,
336
+ sym,
337
+ config,
338
+ resolveIconVisibility(config, "today"),
339
+ );
340
+ if (todayStr) {
341
+ segments.push(
342
+ colorize(todayStr, colors.todayFg, reset, colors.todayBold),
343
+ );
344
+ }
345
+ }
346
+
347
+ const activityParts = collectActivityParts(data, sym);
348
+ if (activityParts.length > 0) {
349
+ segments.push(
350
+ colorize(
351
+ activityParts.join(" · "),
352
+ colors.metricsFg,
353
+ reset,
354
+ colors.metricsBold,
355
+ ),
356
+ );
357
+ }
358
+
359
+ return segments;
360
+ }
361
+
362
+ export function collectActivityParts(data: TuiData, sym: SymbolSet): string[] {
363
+ const parts: string[] = [];
364
+ if (data.metricsInfo) {
365
+ if (
366
+ data.metricsInfo.sessionDuration !== null &&
367
+ data.metricsInfo.sessionDuration > 0
368
+ ) {
369
+ parts.push(
370
+ `${sym.metrics_duration} ${formatDuration(data.metricsInfo.sessionDuration)}`,
371
+ );
372
+ }
373
+ if (
374
+ data.metricsInfo.messageCount !== null &&
375
+ data.metricsInfo.messageCount > 0
376
+ ) {
377
+ parts.push(`${sym.metrics_messages} ${data.metricsInfo.messageCount}`);
378
+ }
379
+ }
380
+ return parts;
381
+ }
382
+
383
+ export function collectWorkspaceParts(
384
+ data: TuiData,
385
+ sym: SymbolSet,
386
+ reset: string,
387
+ colors: PowerlineColors,
388
+ config: PowerlineConfig,
389
+ ): string[] {
390
+ const parts: string[] = [];
391
+
392
+ const gitStr = formatGitSegment(
393
+ data,
394
+ sym,
395
+ resolveIconVisibility(config, "git"),
396
+ );
397
+ if (gitStr) parts.push(colorize(gitStr, colors.gitFg, reset, colors.gitBold));
398
+
399
+ const dir = abbreviateFishStyle(getDirectoryDisplay(data.hookData));
400
+ parts.push(colorize(dir, colors.modeFg, reset, colors.modeBold));
401
+
402
+ return parts;
403
+ }
404
+
405
+ export function collectFooterParts(
406
+ data: TuiData,
407
+ sym: SymbolSet,
408
+ config: PowerlineConfig,
409
+ reset: string,
410
+ colors: PowerlineColors,
411
+ ): string[] {
412
+ const parts: string[] = [];
413
+
414
+ const versionText = formatVersionSegment(
415
+ data,
416
+ sym,
417
+ resolveIconVisibility(config, "version"),
418
+ );
419
+ if (versionText) {
420
+ parts.push(
421
+ colorize(versionText, colors.versionFg, reset, colors.versionBold),
422
+ );
423
+ }
424
+
425
+ const thinkingSegConfig = config.display.lines
426
+ .map((line) => line.segments.thinking)
427
+ .find((t) => t?.enabled);
428
+ const thinkingText = formatThinkingSegment(
429
+ data,
430
+ sym,
431
+ thinkingSegConfig,
432
+ resolveIconVisibility(config, "thinking"),
433
+ );
434
+ if (thinkingText) {
435
+ parts.push(
436
+ colorize(thinkingText, colors.thinkingFg, reset, colors.thinkingBold),
437
+ );
438
+ }
439
+
440
+ const cacheTimerEnabled = config.display.lines.some(
441
+ (line) => line.segments.cacheTimer?.enabled,
442
+ );
443
+ if (cacheTimerEnabled && data.cacheTimerInfo) {
444
+ const cacheTimerText = formatCacheTimerSegment(
445
+ data,
446
+ sym,
447
+ resolveIconVisibility(config, "cacheTimer"),
448
+ );
449
+ if (cacheTimerText) {
450
+ const { fg, bold } = cacheTimerStyle(
451
+ data.cacheTimerInfo.elapsedSeconds,
452
+ colors,
453
+ );
454
+ parts.push(colorize(cacheTimerText, fg, reset, bold));
455
+ }
456
+ }
457
+
458
+ if (data.tmuxSessionId) {
459
+ parts.push(
460
+ colorize(
461
+ `tmux:${data.tmuxSessionId}`,
462
+ colors.tmuxFg,
463
+ reset,
464
+ colors.tmuxBold,
465
+ ),
466
+ );
467
+ }
468
+
469
+ if (data.metricsInfo) {
470
+ const metricParts: string[] = [];
471
+ if (
472
+ data.metricsInfo.responseTime !== null &&
473
+ !isNaN(data.metricsInfo.responseTime) &&
474
+ data.metricsInfo.responseTime > 0
475
+ ) {
476
+ metricParts.push(
477
+ `${sym.metrics_response} ${formatResponseTime(data.metricsInfo.responseTime)}`,
478
+ );
479
+ }
480
+ if (
481
+ data.metricsInfo.linesAdded !== null &&
482
+ data.metricsInfo.linesAdded > 0
483
+ ) {
484
+ metricParts.push(
485
+ `${sym.metrics_lines_added}${data.metricsInfo.linesAdded}`,
486
+ );
487
+ }
488
+ if (
489
+ data.metricsInfo.linesRemoved !== null &&
490
+ data.metricsInfo.linesRemoved > 0
491
+ ) {
492
+ metricParts.push(
493
+ `${sym.metrics_lines_removed}${data.metricsInfo.linesRemoved}`,
494
+ );
495
+ }
496
+ if (metricParts.length > 0) {
497
+ parts.push(
498
+ colorize(
499
+ metricParts.join(" · "),
500
+ colors.metricsFg,
501
+ reset,
502
+ colors.metricsBold,
503
+ ),
504
+ );
505
+ }
506
+ }
507
+
508
+ const envConfig = config.display.lines
509
+ .map((line) => line.segments.env)
510
+ .find((env) => env?.enabled);
511
+
512
+ if (envConfig && envConfig.variable) {
513
+ const envVal = globalThis.process?.env?.[envConfig.variable];
514
+ if (envVal) {
515
+ const prefix = envConfig.prefix ?? envConfig.variable;
516
+ parts.push(
517
+ colorize(
518
+ prefix ? `${prefix}:${envVal}` : envVal,
519
+ colors.envFg,
520
+ reset,
521
+ colors.envBold,
522
+ ),
523
+ );
524
+ }
525
+ }
526
+
527
+ return parts;
528
+ }
529
+
530
+ export function formatBlockParts(
531
+ blockInfo: TuiData["blockInfo"] & {},
532
+ sym: SymbolSet,
533
+ _config: PowerlineConfig,
534
+ iconVisible = true,
535
+ ): Record<string, string> {
536
+ const value = `${Math.round(blockInfo.nativeUtilization)}%`;
537
+ const time = formatTimeRemaining(blockInfo.timeRemaining);
538
+
539
+ return {
540
+ icon: iconVisible ? sym.block_cost : "",
541
+ label: "block",
542
+ value,
543
+ time,
544
+ budget: "",
545
+ bar: " ",
546
+ };
547
+ }
548
+
549
+ export function formatBlockSegment(
550
+ blockInfo: TuiData["blockInfo"] & {},
551
+ sym: SymbolSet,
552
+ config: PowerlineConfig,
553
+ iconVisible = true,
554
+ ): string {
555
+ const parts = formatBlockParts(blockInfo, sym, config, iconVisible);
556
+ let text = parts.icon ? `${parts.icon} ${parts.value}` : (parts.value ?? "");
557
+ if (parts.time) text += ` · ${parts.time}`;
558
+ if (parts.budget) text += parts.budget;
559
+ return text;
560
+ }
561
+
562
+ export function formatWeeklyParts(
563
+ sevenDay: { used_percentage: number; resets_at: number },
564
+ sym: SymbolSet,
565
+ iconVisible = true,
566
+ ): Record<string, string> {
567
+ const pct = `${Math.round(sevenDay.used_percentage)}%`;
568
+ const time = formatLongTimeRemaining(minutesUntilReset(sevenDay.resets_at));
569
+ return {
570
+ icon: iconVisible ? sym.weekly_cost : "",
571
+ label: "weekly",
572
+ pct,
573
+ time,
574
+ bar: " ",
575
+ };
576
+ }
577
+
578
+ export function formatWeeklySegment(
579
+ sevenDay: { used_percentage: number; resets_at: number },
580
+ sym: SymbolSet,
581
+ iconVisible = true,
582
+ ): string {
583
+ const parts = formatWeeklyParts(sevenDay, sym, iconVisible);
584
+ let text = parts.icon ? `${parts.icon} ${parts.pct}` : (parts.pct ?? "");
585
+ if (parts.time) text += ` · ${parts.time}`;
586
+ return text;
587
+ }
588
+
589
+ export function formatSessionParts(
590
+ usageInfo: TuiData["usageInfo"] & {},
591
+ sym: SymbolSet,
592
+ config: PowerlineConfig,
593
+ iconVisible = true,
594
+ ): Record<string, string> {
595
+ const state = resolveBudgetDisplay(
596
+ usageInfo.session.cost,
597
+ usageInfo.session.tokens,
598
+ config.budget?.session,
599
+ );
600
+
601
+ if (state.suppressAll) {
602
+ return { icon: "", label: "", cost: "", tokens: "", budget: "" };
603
+ }
604
+
605
+ const sessionTokens = usageInfo.session.tokens;
606
+ const tokenStr =
607
+ state.showBase && sessionTokens !== null && sessionTokens > 0
608
+ ? formatTokenCount(sessionTokens)
609
+ : "";
610
+
611
+ return {
612
+ icon: iconVisible ? sym.session_cost : "",
613
+ label: state.percentageOnly ? "" : "session",
614
+ cost: state.showBase ? formatCost(usageInfo.session.cost) : "",
615
+ tokens: tokenStr,
616
+ budget: state.percentText ? ` ${state.percentText}` : "",
617
+ };
618
+ }
619
+
620
+ export function formatSessionSegment(
621
+ usageInfo: TuiData["usageInfo"] & {},
622
+ sym: SymbolSet,
623
+ config: PowerlineConfig,
624
+ iconVisible = true,
625
+ ): string {
626
+ const state = resolveBudgetDisplay(
627
+ usageInfo.session.cost,
628
+ usageInfo.session.tokens,
629
+ config.budget?.session,
630
+ );
631
+ if (state.suppressAll) return "";
632
+
633
+ const icon = iconVisible ? sym.session_cost : "";
634
+
635
+ if (!state.showBase) {
636
+ return icon ? `${icon} ${state.percentText}` : state.percentText;
637
+ }
638
+
639
+ const costStr = formatCost(usageInfo.session.cost);
640
+ const sessionTokens = usageInfo.session.tokens;
641
+ let text = icon ? `${icon} ${costStr}` : costStr;
642
+ if (sessionTokens !== null && sessionTokens > 0) {
643
+ text += ` · ${formatTokenCount(sessionTokens)}`;
644
+ }
645
+ if (state.percentText) text += ` ${state.percentText}`;
646
+ return text;
647
+ }
648
+
649
+ export function formatTodayParts(
650
+ todayInfo: TuiData["todayInfo"] & {},
651
+ sym: SymbolSet,
652
+ config: PowerlineConfig,
653
+ iconVisible = true,
654
+ ): Record<string, string> {
655
+ const state = resolveBudgetDisplay(
656
+ todayInfo.cost,
657
+ todayInfo.tokens,
658
+ config.budget?.today,
659
+ );
660
+
661
+ if (state.suppressAll) {
662
+ return { icon: "", label: "", cost: "", budget: "" };
663
+ }
664
+
665
+ return {
666
+ icon: iconVisible ? sym.today_cost : "",
667
+ cost: state.showBase ? formatCost(todayInfo.cost) : "",
668
+ label: state.percentageOnly ? "" : "today",
669
+ budget: state.percentText ? ` ${state.percentText}` : "",
670
+ };
671
+ }
672
+
673
+ export function formatTodaySegment(
674
+ todayInfo: TuiData["todayInfo"] & {},
675
+ sym: SymbolSet,
676
+ config: PowerlineConfig,
677
+ iconVisible = true,
678
+ ): string {
679
+ const state = resolveBudgetDisplay(
680
+ todayInfo.cost,
681
+ todayInfo.tokens,
682
+ config.budget?.today,
683
+ );
684
+ if (state.suppressAll) return "";
685
+
686
+ const icon = iconVisible ? sym.today_cost : "";
687
+
688
+ if (!state.showBase) {
689
+ return icon ? `${icon} ${state.percentText}` : state.percentText;
690
+ }
691
+
692
+ const costStr = formatCost(todayInfo.cost);
693
+ let text = icon ? `${icon} ${costStr} today` : `${costStr} today`;
694
+ if (state.percentText) text += ` ${state.percentText}`;
695
+ return text;
696
+ }
697
+
698
+ function formatMetricsParts(
699
+ data: TuiData,
700
+ sym: SymbolSet,
701
+ ): Record<string, string> {
702
+ const empty = {
703
+ response: "",
704
+ responseIcon: "",
705
+ responseVal: "",
706
+ lastResponse: "",
707
+ lastResponseIcon: "",
708
+ lastResponseVal: "",
709
+ added: "",
710
+ addedIcon: "",
711
+ addedVal: "",
712
+ removed: "",
713
+ removedIcon: "",
714
+ removedVal: "",
715
+ };
716
+ if (!data.metricsInfo) return empty;
717
+
718
+ const hasResponse =
719
+ data.metricsInfo.responseTime !== null &&
720
+ !isNaN(data.metricsInfo.responseTime) &&
721
+ data.metricsInfo.responseTime > 0;
722
+ const responseValStr = hasResponse
723
+ ? formatResponseTime(data.metricsInfo.responseTime!)
724
+ : "";
725
+
726
+ const hasLast =
727
+ data.metricsInfo.lastResponseTime !== null &&
728
+ !isNaN(data.metricsInfo.lastResponseTime) &&
729
+ data.metricsInfo.lastResponseTime > 0;
730
+ const lastValStr = hasLast
731
+ ? formatResponseTime(data.metricsInfo.lastResponseTime!)
732
+ : "";
733
+
734
+ const hasAdded =
735
+ data.metricsInfo.linesAdded !== null && data.metricsInfo.linesAdded > 0;
736
+ const addedValStr = hasAdded ? `${data.metricsInfo.linesAdded}` : "";
737
+
738
+ const hasRemoved =
739
+ data.metricsInfo.linesRemoved !== null && data.metricsInfo.linesRemoved > 0;
740
+ const removedValStr = hasRemoved ? `${data.metricsInfo.linesRemoved}` : "";
741
+
742
+ return {
743
+ response: hasResponse ? `${sym.metrics_response} ${responseValStr}` : "",
744
+ responseIcon: hasResponse ? sym.metrics_response : "",
745
+ responseVal: responseValStr,
746
+ lastResponse: hasLast
747
+ ? `${sym.metrics_last_response} ${lastValStr}`
748
+ : `${sym.metrics_last_response} --`,
749
+ lastResponseIcon: sym.metrics_last_response,
750
+ lastResponseVal: hasLast ? lastValStr : "--",
751
+ added: hasAdded ? `${sym.metrics_lines_added}${addedValStr}` : "",
752
+ addedIcon: hasAdded ? sym.metrics_lines_added : "",
753
+ addedVal: addedValStr,
754
+ removed: hasRemoved ? `${sym.metrics_lines_removed}${removedValStr}` : "",
755
+ removedIcon: hasRemoved ? sym.metrics_lines_removed : "",
756
+ removedVal: removedValStr,
757
+ };
758
+ }
759
+
760
+ function formatMetricsSegment(data: TuiData, sym: SymbolSet): string {
761
+ const parts = formatMetricsParts(data, sym);
762
+ const filled = [
763
+ parts.response,
764
+ parts.lastResponse,
765
+ parts.added,
766
+ parts.removed,
767
+ ].filter(Boolean);
768
+ return filled.length > 0 ? filled.join(" · ") : "";
769
+ }
770
+
771
+ function formatActivityParts(
772
+ data: TuiData,
773
+ sym: SymbolSet,
774
+ ): Record<string, string> {
775
+ const empty = {
776
+ icon: "",
777
+ duration: "",
778
+ durationIcon: "",
779
+ durationVal: "",
780
+ messages: "",
781
+ messagesIcon: "",
782
+ messagesVal: "",
783
+ };
784
+ if (!data.metricsInfo) return empty;
785
+
786
+ const hasDuration =
787
+ data.metricsInfo.sessionDuration !== null &&
788
+ data.metricsInfo.sessionDuration > 0;
789
+ const durationValStr = hasDuration
790
+ ? formatDuration(data.metricsInfo.sessionDuration!)
791
+ : "";
792
+
793
+ const hasMessages =
794
+ data.metricsInfo.messageCount !== null && data.metricsInfo.messageCount > 0;
795
+ const messagesValStr = hasMessages ? `${data.metricsInfo.messageCount}` : "";
796
+
797
+ return {
798
+ icon: sym.activity,
799
+ duration: hasDuration ? `${sym.metrics_duration} ${durationValStr}` : "",
800
+ durationIcon: hasDuration ? sym.metrics_duration : "",
801
+ durationVal: durationValStr,
802
+ messages: hasMessages ? `${sym.metrics_messages} ${messagesValStr}` : "",
803
+ messagesIcon: hasMessages ? sym.metrics_messages : "",
804
+ messagesVal: messagesValStr,
805
+ };
806
+ }
807
+
808
+ function formatActivitySegment(data: TuiData, sym: SymbolSet): string {
809
+ const parts = formatActivityParts(data, sym);
810
+ const filled = [parts.duration, parts.messages].filter(Boolean);
811
+ return filled.length > 0 ? filled.join(" · ") : "";
812
+ }
813
+
814
+ function formatGitParts(
815
+ data: TuiData,
816
+ sym: SymbolSet,
817
+ iconVisible = true,
818
+ ): Record<string, string> {
819
+ if (!data.gitInfo)
820
+ return {
821
+ icon: "",
822
+ headVal: "",
823
+ branch: "",
824
+ status: "",
825
+ ahead: "",
826
+ behind: "",
827
+ working: "",
828
+ head: "",
829
+ };
830
+
831
+ let statusIcon: string;
832
+ if (data.gitInfo.status === "conflicts") {
833
+ statusIcon = sym.git_conflicts;
834
+ } else if (data.gitInfo.status === "dirty") {
835
+ statusIcon = sym.git_dirty;
836
+ } else {
837
+ statusIcon = sym.git_clean;
838
+ }
839
+
840
+ const ahead =
841
+ data.gitInfo.ahead > 0 ? `${sym.git_ahead}${data.gitInfo.ahead}` : "";
842
+ const behind =
843
+ data.gitInfo.behind > 0 ? `${sym.git_behind}${data.gitInfo.behind}` : "";
844
+
845
+ const counts: string[] = [];
846
+ if (data.gitInfo.staged && data.gitInfo.staged > 0)
847
+ counts.push(`+${data.gitInfo.staged}`);
848
+ if (data.gitInfo.unstaged && data.gitInfo.unstaged > 0)
849
+ counts.push(`~${data.gitInfo.unstaged}`);
850
+ if (data.gitInfo.untracked && data.gitInfo.untracked > 0)
851
+ counts.push(`?${data.gitInfo.untracked}`);
852
+ const working = counts.length > 0 ? `(${counts.join(" ")})` : "";
853
+
854
+ const headParts: string[] = [];
855
+ if (iconVisible) headParts.push(sym.branch);
856
+ headParts.push(data.gitInfo.branch, statusIcon);
857
+ if (ahead) headParts.push(ahead);
858
+ if (behind) headParts.push(behind);
859
+
860
+ const infoParts = [data.gitInfo.branch, statusIcon];
861
+ if (ahead) infoParts.push(ahead);
862
+ if (behind) infoParts.push(behind);
863
+
864
+ return {
865
+ icon: iconVisible ? sym.branch : "",
866
+ headVal: infoParts.join(" "),
867
+ branch: data.gitInfo.branch,
868
+ status: statusIcon,
869
+ ahead,
870
+ behind,
871
+ working,
872
+ head: headParts.join(" "),
873
+ };
874
+ }
875
+
876
+ function formatGitSegment(
877
+ data: TuiData,
878
+ sym: SymbolSet,
879
+ iconVisible = true,
880
+ ): string {
881
+ const parts = formatGitParts(data, sym, iconVisible);
882
+ if (!parts.branch) return "";
883
+ let text = parts.icon
884
+ ? `${parts.icon} ${parts.branch} ${parts.status}`
885
+ : `${parts.branch} ${parts.status}`;
886
+ if (parts.ahead) text += ` ${parts.ahead}`;
887
+ if (parts.behind) text += `${parts.behind}`;
888
+ if (parts.working) text += ` ${parts.working}`;
889
+ return text;
890
+ }
891
+
892
+ function formatDirParts(
893
+ data: TuiData,
894
+ config: PowerlineConfig,
895
+ sym: SymbolSet,
896
+ iconVisible = true,
897
+ ): Record<string, string> {
898
+ return {
899
+ icon: iconVisible ? sym.dir : "",
900
+ value: formatDirValue(data, config),
901
+ };
902
+ }
903
+
904
+ function formatDirValue(data: TuiData, config: PowerlineConfig): string {
905
+ const raw = getDirectoryDisplay(data.hookData);
906
+ const dirConfig = config.display.lines
907
+ .map((line) => line.segments.directory)
908
+ .find((d) => d?.enabled);
909
+ const style =
910
+ dirConfig?.style ?? (dirConfig?.showBasename ? "basename" : "fish");
911
+ if (style === "basename") {
912
+ const sep = raw.includes("/") ? "/" : "\\";
913
+ return raw.split(sep).pop() || raw;
914
+ }
915
+ if (style === "full") return raw;
916
+ return abbreviateFishStyle(raw);
917
+ }
918
+
919
+ function formatVersionParts(
920
+ data: TuiData,
921
+ sym: SymbolSet,
922
+ iconVisible = true,
923
+ ): Record<string, string> {
924
+ if (!data.hookData.version) return { icon: "", value: "" };
925
+ return {
926
+ icon: iconVisible ? sym.version : "",
927
+ value: `v${data.hookData.version}`,
928
+ };
929
+ }
930
+
931
+ function formatVersionSegment(
932
+ data: TuiData,
933
+ sym: SymbolSet,
934
+ iconVisible = true,
935
+ ): string {
936
+ const parts = formatVersionParts(data, sym, iconVisible);
937
+ if (!parts.value) return "";
938
+ return parts.icon ? `${parts.icon} ${parts.value}` : parts.value;
939
+ }
940
+
941
+ function formatAgentParts(
942
+ data: TuiData,
943
+ sym: SymbolSet,
944
+ iconVisible = true,
945
+ ): Record<string, string> {
946
+ const raw = data.hookData.agent?.name;
947
+ if (typeof raw !== "string") return { icon: "", name: "" };
948
+ const name = raw.trim();
949
+ if (!name) return { icon: "", name: "" };
950
+ return {
951
+ icon: iconVisible ? sym.agent : "",
952
+ name,
953
+ };
954
+ }
955
+
956
+ function formatAgentSegment(
957
+ data: TuiData,
958
+ sym: SymbolSet,
959
+ config: PowerlineConfig,
960
+ iconVisible = true,
961
+ ): string {
962
+ const parts = formatAgentParts(data, sym, iconVisible);
963
+ if (!parts.name) return "";
964
+ const agentConfig = config.display.lines
965
+ .map((line) => line.segments.agent)
966
+ .find((a) => a?.enabled);
967
+ const body = agentConfig?.showLabel ? `agent: ${parts.name}` : parts.name;
968
+ return parts.icon ? `${parts.icon} ${body}` : body;
969
+ }
970
+
971
+ function buildThinkingBody(
972
+ data: TuiData,
973
+ thinkingConfig: { showEnabled?: boolean; showEffort?: boolean } | undefined,
974
+ ): string {
975
+ const showEnabled = thinkingConfig?.showEnabled ?? true;
976
+ const showEffort = thinkingConfig?.showEffort ?? true;
977
+ if (!showEnabled && !showEffort) return "";
978
+
979
+ const enabled = showEnabled ? getThinkingEnabled(data.hookData) : null;
980
+ const level = showEffort ? getEffortLevel(data.hookData) : null;
981
+
982
+ const segments: string[] = [];
983
+ if (enabled !== null) segments.push(enabled ? "On" : "Off");
984
+ if (level) segments.push(level);
985
+ return segments.join(" · ");
986
+ }
987
+
988
+ function formatThinkingParts(
989
+ data: TuiData,
990
+ sym: SymbolSet,
991
+ thinkingConfig: { showEnabled?: boolean; showEffort?: boolean } | undefined,
992
+ iconVisible = true,
993
+ ): Record<string, string> {
994
+ const showEnabled = thinkingConfig?.showEnabled ?? true;
995
+ const showEffort = thinkingConfig?.showEffort ?? true;
996
+ const enabled = showEnabled ? getThinkingEnabled(data.hookData) : null;
997
+ const level = showEffort ? getEffortLevel(data.hookData) : null;
998
+
999
+ const enabledText = enabled === null ? "" : enabled ? "On" : "Off";
1000
+ const effortText = level ?? "";
1001
+ const hasAny = enabledText !== "" || effortText !== "";
1002
+ return {
1003
+ icon: hasAny && iconVisible ? sym.thinking : "",
1004
+ enabled: enabledText,
1005
+ effort: effortText,
1006
+ };
1007
+ }
1008
+
1009
+ function formatThinkingSegment(
1010
+ data: TuiData,
1011
+ sym: SymbolSet,
1012
+ thinkingConfig: { showEnabled?: boolean; showEffort?: boolean } | undefined,
1013
+ iconVisible = true,
1014
+ ): string {
1015
+ const body = buildThinkingBody(data, thinkingConfig);
1016
+ if (!body) return "";
1017
+ return iconVisible ? `${sym.thinking} ${body}` : body;
1018
+ }
1019
+
1020
+ function formatCacheTimerParts(
1021
+ data: TuiData,
1022
+ sym: SymbolSet,
1023
+ iconVisible = true,
1024
+ ): Record<string, string> {
1025
+ if (!data.cacheTimerInfo) return { icon: "", value: "" };
1026
+ return {
1027
+ icon: iconVisible ? sym.cache_timer : "",
1028
+ value: formatCacheTimerElapsed(data.cacheTimerInfo.elapsedSeconds),
1029
+ };
1030
+ }
1031
+
1032
+ function formatCacheTimerSegment(
1033
+ data: TuiData,
1034
+ sym: SymbolSet,
1035
+ iconVisible = true,
1036
+ ): string {
1037
+ const parts = formatCacheTimerParts(data, sym, iconVisible);
1038
+ if (!parts.value) return "";
1039
+ return parts.icon ? `${parts.icon} ${parts.value}` : parts.value;
1040
+ }
1041
+
1042
+ function cacheTimerStyle(
1043
+ elapsed: number,
1044
+ colors: PowerlineColors,
1045
+ ): { fg: string; bold: boolean } {
1046
+ if (elapsed >= 300) {
1047
+ return { fg: colors.contextCriticalFg, bold: colors.contextCriticalBold };
1048
+ }
1049
+ if (elapsed >= 180) {
1050
+ return { fg: colors.contextWarningFg, bold: colors.contextWarningBold };
1051
+ }
1052
+ return { fg: colors.cacheTimerFg, bold: colors.cacheTimerBold };
1053
+ }
1054
+
1055
+ function formatTmuxParts(data: TuiData): Record<string, string> {
1056
+ if (!data.tmuxSessionId) return { label: "", value: "" };
1057
+ return { label: "tmux", value: data.tmuxSessionId };
1058
+ }
1059
+
1060
+ function formatTmuxSegment(data: TuiData): string {
1061
+ const parts = formatTmuxParts(data);
1062
+ if (!parts.label) return "";
1063
+ return `${parts.label}:${parts.value}`;
1064
+ }
1065
+
1066
+ function formatEnvParts(config: PowerlineConfig): Record<string, string> {
1067
+ const envConfig = config.display.lines
1068
+ .map((line) => line.segments.env)
1069
+ .find((env) => env?.enabled);
1070
+
1071
+ if (!envConfig || !envConfig.variable) return { prefix: "", value: "" };
1072
+ const envVal = globalThis.process?.env?.[envConfig.variable];
1073
+ if (!envVal) return { prefix: "", value: "" };
1074
+ const prefix = envConfig.prefix ?? envConfig.variable;
1075
+ return { prefix: prefix || "", value: envVal };
1076
+ }
1077
+
1078
+ function formatEnvSegment(config: PowerlineConfig): string {
1079
+ const parts = formatEnvParts(config);
1080
+ if (!parts.value) return "";
1081
+ return parts.prefix ? `${parts.prefix}:${parts.value}` : parts.value;
1082
+ }
1083
+
1084
+ function addParts(
1085
+ result: Record<string, string>,
1086
+ segment: string,
1087
+ parts: Record<string, string>,
1088
+ color: string,
1089
+ reset: string,
1090
+ partFg?: Record<string, string>,
1091
+ bold = false,
1092
+ ): void {
1093
+ for (const [key, value] of Object.entries(parts)) {
1094
+ const partKey = `${segment}.${key}`;
1095
+ const partColor = partFg?.[partKey] ?? partFg?.[segment] ?? color;
1096
+ result[partKey] = value ? colorize(value, partColor, reset, bold) : "";
1097
+ }
1098
+ }
1099
+
1100
+ // --- Template Composition ---
1101
+
1102
+ export interface ResolvedTemplate {
1103
+ items: string[];
1104
+ gap: number;
1105
+ justify: JustifyValue;
1106
+ }
1107
+
1108
+ function resolveTemplateItems(
1109
+ template: SegmentTemplate,
1110
+ segmentRef: string,
1111
+ resolvedData: Record<string, string>,
1112
+ ): string[] {
1113
+ const dotIdx = segmentRef.indexOf(".");
1114
+ const baseSegment = dotIdx !== -1 ? segmentRef.slice(0, dotIdx) : segmentRef;
1115
+
1116
+ return template.items
1117
+ .map((item) => {
1118
+ const match = item.match(/^\{(.+)\}$/);
1119
+ if (!match) return item ? colorize(item, "", "") : "";
1120
+ const partName = match[1]!;
1121
+ const key = `${baseSegment}.${partName}`;
1122
+ return resolvedData[key] ?? "";
1123
+ })
1124
+ .filter(Boolean);
1125
+ }
1126
+
1127
+ export function composeTemplate(
1128
+ items: string[],
1129
+ gap: number,
1130
+ justify: JustifyValue,
1131
+ cellWidth?: number,
1132
+ ): string {
1133
+ if (items.length === 0) return "";
1134
+
1135
+ if (justify === "between" && cellWidth !== undefined && items.length > 1) {
1136
+ const totalContent = items.reduce(
1137
+ (sum, item) => sum + visibleLength(item),
1138
+ 0,
1139
+ );
1140
+ const totalGap = Math.max(
1141
+ gap * (items.length - 1),
1142
+ cellWidth - totalContent,
1143
+ );
1144
+ const baseGap = Math.floor(totalGap / (items.length - 1));
1145
+ const extraSpaces = totalGap % (items.length - 1);
1146
+
1147
+ let result = items[0]!;
1148
+ for (let i = 1; i < items.length; i++) {
1149
+ result += " ".repeat(baseGap + (i <= extraSpaces ? 1 : 0)) + items[i];
1150
+ }
1151
+ return result;
1152
+ }
1153
+
1154
+ return items.join(" ".repeat(gap));
1155
+ }
1156
+
1157
+ export interface ResolvedSegments {
1158
+ data: Record<string, string>;
1159
+ templates: Record<string, ResolvedTemplate>;
1160
+ }
1161
+
1162
+ export function resolveSegments(
1163
+ data: TuiData,
1164
+ ctx: RenderCtx,
1165
+ ): ResolvedSegments {
1166
+ const { sym, config, reset, colors } = ctx;
1167
+ const pf = colors.partFg;
1168
+
1169
+ const colorizeOrEmpty = (
1170
+ text: string,
1171
+ color: string,
1172
+ bold = false,
1173
+ ): string => (text ? colorize(text, color, reset, bold) : "");
1174
+
1175
+ const result: Record<string, string> = {};
1176
+
1177
+ const iconVisible = {
1178
+ model: resolveIconVisibility(config, "model"),
1179
+ context: resolveIconVisibility(config, "context"),
1180
+ block: resolveIconVisibility(config, "block"),
1181
+ session: resolveIconVisibility(config, "session"),
1182
+ today: resolveIconVisibility(config, "today"),
1183
+ weekly: resolveIconVisibility(config, "weekly"),
1184
+ git: resolveIconVisibility(config, "git"),
1185
+ directory: resolveIconVisibility(config, "directory"),
1186
+ version: resolveIconVisibility(config, "version"),
1187
+ agent: resolveIconVisibility(config, "agent"),
1188
+ thinking: resolveIconVisibility(config, "thinking"),
1189
+ cacheTimer: resolveIconVisibility(config, "cacheTimer"),
1190
+ };
1191
+
1192
+ // Model
1193
+ const rawModelName = data.hookData.model?.display_name || "Claude";
1194
+ const modelName = formatModelName(rawModelName).toLowerCase();
1195
+ const modelColor = pf?.["model"] ?? colors.modelFg;
1196
+ const modelIcon = iconVisible.model ? sym.model : "";
1197
+ result.model = colorizeOrEmpty(
1198
+ modelIcon ? `${modelIcon} ${modelName}` : modelName,
1199
+ modelColor,
1200
+ colors.modelBold,
1201
+ );
1202
+ addParts(
1203
+ result,
1204
+ "model",
1205
+ { icon: modelIcon, value: modelName },
1206
+ colors.modelFg,
1207
+ reset,
1208
+ pf,
1209
+ colors.modelBold,
1210
+ );
1211
+
1212
+ // Context (bar is width-dependent, resolved later via lateResolve)
1213
+ const contextLine = buildContextLine(
1214
+ data,
1215
+ ctx.contentWidth,
1216
+ sym,
1217
+ reset,
1218
+ colors,
1219
+ );
1220
+ result.context = contextLine ?? "";
1221
+ const ctxParts = formatContextParts(data, sym, iconVisible.context);
1222
+ const ctxStyle = data.contextInfo
1223
+ ? resolveThresholdStyle(
1224
+ data.contextInfo.usablePercentage,
1225
+ colors.contextFg,
1226
+ colors.contextBold,
1227
+ colors,
1228
+ )
1229
+ : { fg: colors.contextFg, bold: colors.contextBold };
1230
+ addParts(result, "context", ctxParts, ctxStyle.fg, reset, pf, ctxStyle.bold);
1231
+
1232
+ // Block
1233
+ if (data.blockInfo) {
1234
+ const blockColor = pf?.["block"] ?? colors.blockFg;
1235
+ result.block = colorizeOrEmpty(
1236
+ formatBlockSegment(data.blockInfo, sym, config, iconVisible.block),
1237
+ blockColor,
1238
+ colors.blockBold,
1239
+ );
1240
+ addParts(
1241
+ result,
1242
+ "block",
1243
+ formatBlockParts(data.blockInfo, sym, config, iconVisible.block),
1244
+ colors.blockFg,
1245
+ reset,
1246
+ pf,
1247
+ colors.blockBold,
1248
+ );
1249
+ } else {
1250
+ result.block = "";
1251
+ }
1252
+
1253
+ // Session
1254
+ if (data.usageInfo) {
1255
+ const sessionColor = pf?.["session"] ?? colors.sessionFg;
1256
+ result.session = colorizeOrEmpty(
1257
+ formatSessionSegment(data.usageInfo, sym, config, iconVisible.session),
1258
+ sessionColor,
1259
+ colors.sessionBold,
1260
+ );
1261
+ addParts(
1262
+ result,
1263
+ "session",
1264
+ formatSessionParts(data.usageInfo, sym, config, iconVisible.session),
1265
+ colors.sessionFg,
1266
+ reset,
1267
+ pf,
1268
+ colors.sessionBold,
1269
+ );
1270
+ } else {
1271
+ result.session = "";
1272
+ }
1273
+
1274
+ // Today
1275
+ if (data.todayInfo) {
1276
+ const todayColor = pf?.["today"] ?? colors.todayFg;
1277
+ result.today = colorizeOrEmpty(
1278
+ formatTodaySegment(data.todayInfo, sym, config, iconVisible.today),
1279
+ todayColor,
1280
+ colors.todayBold,
1281
+ );
1282
+ addParts(
1283
+ result,
1284
+ "today",
1285
+ formatTodayParts(data.todayInfo, sym, config, iconVisible.today),
1286
+ colors.todayFg,
1287
+ reset,
1288
+ pf,
1289
+ colors.todayBold,
1290
+ );
1291
+ } else {
1292
+ result.today = "";
1293
+ }
1294
+
1295
+ // Weekly
1296
+ const sevenDay = data.hookData.rate_limits?.seven_day;
1297
+ if (sevenDay) {
1298
+ const weeklyColor = pf?.["weekly"] ?? colors.weeklyFg;
1299
+ result.weekly = colorizeOrEmpty(
1300
+ formatWeeklySegment(sevenDay, sym, iconVisible.weekly),
1301
+ weeklyColor,
1302
+ colors.weeklyBold,
1303
+ );
1304
+ addParts(
1305
+ result,
1306
+ "weekly",
1307
+ formatWeeklyParts(sevenDay, sym, iconVisible.weekly),
1308
+ colors.weeklyFg,
1309
+ reset,
1310
+ pf,
1311
+ colors.weeklyBold,
1312
+ );
1313
+ } else {
1314
+ result.weekly = "";
1315
+ }
1316
+
1317
+ // Git
1318
+ const gitColor = pf?.["git"] ?? colors.gitFg;
1319
+ result.git = colorizeOrEmpty(
1320
+ formatGitSegment(data, sym, iconVisible.git),
1321
+ gitColor,
1322
+ colors.gitBold,
1323
+ );
1324
+ addParts(
1325
+ result,
1326
+ "git",
1327
+ formatGitParts(data, sym, iconVisible.git),
1328
+ colors.gitFg,
1329
+ reset,
1330
+ pf,
1331
+ colors.gitBold,
1332
+ );
1333
+
1334
+ // Dir
1335
+ const dirColor = pf?.["dir"] ?? colors.modeFg;
1336
+ result.dir = colorizeOrEmpty(
1337
+ formatDirValue(data, config),
1338
+ dirColor,
1339
+ colors.modeBold,
1340
+ );
1341
+ addParts(
1342
+ result,
1343
+ "dir",
1344
+ formatDirParts(data, config, sym, iconVisible.directory),
1345
+ colors.modeFg,
1346
+ reset,
1347
+ pf,
1348
+ colors.modeBold,
1349
+ );
1350
+
1351
+ // Version
1352
+ const versionColor = pf?.["version"] ?? colors.versionFg;
1353
+ result.version = colorizeOrEmpty(
1354
+ formatVersionSegment(data, sym, iconVisible.version),
1355
+ versionColor,
1356
+ colors.versionBold,
1357
+ );
1358
+ addParts(
1359
+ result,
1360
+ "version",
1361
+ formatVersionParts(data, sym, iconVisible.version),
1362
+ colors.versionFg,
1363
+ reset,
1364
+ pf,
1365
+ colors.versionBold,
1366
+ );
1367
+
1368
+ // Tmux
1369
+ const tmuxColor = pf?.["tmux"] ?? colors.tmuxFg;
1370
+ result.tmux = colorizeOrEmpty(
1371
+ formatTmuxSegment(data),
1372
+ tmuxColor,
1373
+ colors.tmuxBold,
1374
+ );
1375
+ addParts(
1376
+ result,
1377
+ "tmux",
1378
+ formatTmuxParts(data),
1379
+ colors.tmuxFg,
1380
+ reset,
1381
+ pf,
1382
+ colors.tmuxBold,
1383
+ );
1384
+
1385
+ // Metrics
1386
+ const metricsColor = pf?.["metrics"] ?? colors.metricsFg;
1387
+ result.metrics = colorizeOrEmpty(
1388
+ formatMetricsSegment(data, sym),
1389
+ metricsColor,
1390
+ colors.metricsBold,
1391
+ );
1392
+ addParts(
1393
+ result,
1394
+ "metrics",
1395
+ formatMetricsParts(data, sym),
1396
+ colors.metricsFg,
1397
+ reset,
1398
+ pf,
1399
+ colors.metricsBold,
1400
+ );
1401
+
1402
+ // Activity
1403
+ const activityColor = pf?.["activity"] ?? colors.metricsFg;
1404
+ result.activity = colorizeOrEmpty(
1405
+ formatActivitySegment(data, sym),
1406
+ activityColor,
1407
+ colors.metricsBold,
1408
+ );
1409
+ addParts(
1410
+ result,
1411
+ "activity",
1412
+ formatActivityParts(data, sym),
1413
+ colors.metricsFg,
1414
+ reset,
1415
+ pf,
1416
+ colors.metricsBold,
1417
+ );
1418
+
1419
+ // Env
1420
+ const envColor = pf?.["env"] ?? colors.envFg;
1421
+ result.env = colorizeOrEmpty(
1422
+ formatEnvSegment(config),
1423
+ envColor,
1424
+ colors.envBold,
1425
+ );
1426
+ addParts(
1427
+ result,
1428
+ "env",
1429
+ formatEnvParts(config),
1430
+ colors.envFg,
1431
+ reset,
1432
+ pf,
1433
+ colors.envBold,
1434
+ );
1435
+
1436
+ // Agent
1437
+ const agentColor = pf?.["agent"] ?? colors.agentFg;
1438
+ result.agent = colorizeOrEmpty(
1439
+ formatAgentSegment(data, sym, config, iconVisible.agent),
1440
+ agentColor,
1441
+ colors.agentBold,
1442
+ );
1443
+ addParts(
1444
+ result,
1445
+ "agent",
1446
+ formatAgentParts(data, sym, iconVisible.agent),
1447
+ colors.agentFg,
1448
+ reset,
1449
+ pf,
1450
+ colors.agentBold,
1451
+ );
1452
+
1453
+ // Thinking (combined enabled + effort)
1454
+ const thinkingSegConfig = config.display.lines
1455
+ .map((line) => line.segments.thinking)
1456
+ .find((t) => t?.enabled);
1457
+ const thinkingColor = pf?.["thinking"] ?? colors.thinkingFg;
1458
+ result.thinking = colorizeOrEmpty(
1459
+ formatThinkingSegment(data, sym, thinkingSegConfig, iconVisible.thinking),
1460
+ thinkingColor,
1461
+ colors.thinkingBold,
1462
+ );
1463
+ addParts(
1464
+ result,
1465
+ "thinking",
1466
+ formatThinkingParts(data, sym, thinkingSegConfig, iconVisible.thinking),
1467
+ colors.thinkingFg,
1468
+ reset,
1469
+ pf,
1470
+ colors.thinkingBold,
1471
+ );
1472
+
1473
+ // CacheTimer
1474
+ const cacheTimerElapsed = data.cacheTimerInfo?.elapsedSeconds ?? 0;
1475
+ const cacheTimerStyleResolved = cacheTimerStyle(cacheTimerElapsed, colors);
1476
+ const cacheTimerColor = pf?.["cacheTimer"] ?? cacheTimerStyleResolved.fg;
1477
+ result.cacheTimer = colorizeOrEmpty(
1478
+ formatCacheTimerSegment(data, sym, iconVisible.cacheTimer),
1479
+ cacheTimerColor,
1480
+ cacheTimerStyleResolved.bold,
1481
+ );
1482
+ addParts(
1483
+ result,
1484
+ "cacheTimer",
1485
+ formatCacheTimerParts(data, sym, iconVisible.cacheTimer),
1486
+ cacheTimerStyleResolved.fg,
1487
+ reset,
1488
+ pf,
1489
+ cacheTimerStyleResolved.bold,
1490
+ );
1491
+
1492
+ // Apply segment templates: resolve items and compose default value
1493
+ const templates: Record<string, ResolvedTemplate> = {};
1494
+ const segmentConfigs = config.display.tui?.segments;
1495
+ if (segmentConfigs) {
1496
+ for (const [segRef, tmpl] of Object.entries(segmentConfigs)) {
1497
+ const items = resolveTemplateItems(tmpl, segRef, result);
1498
+ const gap = tmpl.gap ?? 1;
1499
+ const justify = tmpl.justify ?? "start";
1500
+ templates[segRef] = { items, gap, justify };
1501
+ // Compose default (without cell width for "between")
1502
+ result[segRef] = composeTemplate(
1503
+ items,
1504
+ gap,
1505
+ justify === "between" ? "start" : justify,
1506
+ );
1507
+ }
1508
+ }
1509
+
1510
+ return { data: result, templates };
1511
+ }