@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,796 @@
1
+ import type { ClaudeHookData } from "../utils/claude";
2
+ import type { PowerlineColors } from "../themes";
3
+ import type { PowerlineConfig } from "../config/loader";
4
+ import type { BlockInfo } from "./block";
5
+ import type {
6
+ UsageInfo,
7
+ TokenBreakdown,
8
+ GitInfo,
9
+ ContextInfo,
10
+ MetricsInfo,
11
+ } from ".";
12
+ import type { TodayInfo } from "./today";
13
+
14
+ import {
15
+ formatModelName,
16
+ abbreviateFishStyle,
17
+ formatCost,
18
+ formatTokens,
19
+ formatTokenBreakdown,
20
+ formatTimeSince,
21
+ formatDuration,
22
+ formatLongTimeRemaining,
23
+ collapseHome,
24
+ minutesUntilReset,
25
+ } from "../utils/formatters";
26
+ import { getBudgetStatus } from "../utils/budget";
27
+
28
+ export interface SegmentConfig {
29
+ enabled: boolean;
30
+ }
31
+
32
+ export interface DirectorySegmentConfig extends SegmentConfig {
33
+ showBasename?: boolean;
34
+ style?: "full" | "fish" | "basename";
35
+ }
36
+
37
+ export interface GitSegmentConfig extends SegmentConfig {
38
+ showSha?: boolean;
39
+ showAheadBehind?: boolean;
40
+ showWorkingTree?: boolean;
41
+ showOperation?: boolean;
42
+ showTag?: boolean;
43
+ showTimeSinceCommit?: boolean;
44
+ showStashCount?: boolean;
45
+ showUpstream?: boolean;
46
+ showRepoName?: boolean;
47
+ }
48
+
49
+ export interface UsageSegmentConfig extends SegmentConfig {
50
+ type: "cost" | "tokens" | "both" | "breakdown";
51
+ costSource?: "calculated" | "official";
52
+ }
53
+
54
+ export interface TmuxSegmentConfig extends SegmentConfig {}
55
+
56
+ export type BarDisplayStyle =
57
+ | "text"
58
+ | "ball"
59
+ | "bar"
60
+ | "blocks"
61
+ | "blocks-line"
62
+ | "capped"
63
+ | "dots"
64
+ | "filled"
65
+ | "geometric"
66
+ | "line"
67
+ | "squares";
68
+
69
+ export interface ContextSegmentConfig extends SegmentConfig {
70
+ showPercentageOnly?: boolean;
71
+ displayStyle?: BarDisplayStyle;
72
+ autocompactBuffer?: number;
73
+ percentageMode?: "remaining" | "used";
74
+ }
75
+
76
+ export interface MetricsSegmentConfig extends SegmentConfig {
77
+ showResponseTime?: boolean;
78
+ showLastResponseTime?: boolean;
79
+ showDuration?: boolean;
80
+ showMessageCount?: boolean;
81
+ showLinesAdded?: boolean;
82
+ showLinesRemoved?: boolean;
83
+ }
84
+
85
+ export interface BlockSegmentConfig extends SegmentConfig {
86
+ type: "cost" | "tokens" | "both" | "time" | "weighted";
87
+ burnType?: "cost" | "tokens" | "both" | "none";
88
+ displayStyle?: BarDisplayStyle;
89
+ }
90
+
91
+ export interface TodaySegmentConfig extends SegmentConfig {
92
+ type: "cost" | "tokens" | "both" | "breakdown";
93
+ }
94
+
95
+ export interface VersionSegmentConfig extends SegmentConfig {}
96
+
97
+ export interface SessionIdSegmentConfig extends SegmentConfig {
98
+ showIdLabel?: boolean;
99
+ }
100
+
101
+ export interface EnvSegmentConfig extends SegmentConfig {
102
+ variable: string;
103
+ prefix?: string;
104
+ }
105
+
106
+ export interface WeeklySegmentConfig extends SegmentConfig {
107
+ displayStyle?: BarDisplayStyle;
108
+ }
109
+
110
+ export type AnySegmentConfig =
111
+ | SegmentConfig
112
+ | DirectorySegmentConfig
113
+ | GitSegmentConfig
114
+ | UsageSegmentConfig
115
+ | TmuxSegmentConfig
116
+ | ContextSegmentConfig
117
+ | MetricsSegmentConfig
118
+ | BlockSegmentConfig
119
+ | TodaySegmentConfig
120
+ | VersionSegmentConfig
121
+ | SessionIdSegmentConfig
122
+ | EnvSegmentConfig
123
+ | WeeklySegmentConfig;
124
+
125
+ export interface PowerlineSymbols {
126
+ right: string;
127
+ left: string;
128
+ branch: string;
129
+ model: string;
130
+ git_clean: string;
131
+ git_dirty: string;
132
+ git_conflicts: string;
133
+ git_ahead: string;
134
+ git_behind: string;
135
+ git_worktree: string;
136
+ git_tag: string;
137
+ git_sha: string;
138
+ git_upstream: string;
139
+ git_stash: string;
140
+ git_time: string;
141
+ session_cost: string;
142
+ block_cost: string;
143
+ today_cost: string;
144
+ context_time: string;
145
+ metrics_response: string;
146
+ metrics_last_response: string;
147
+ metrics_duration: string;
148
+ metrics_messages: string;
149
+ metrics_lines_added: string;
150
+ metrics_lines_removed: string;
151
+ metrics_burn: string;
152
+ version: string;
153
+ bar_filled: string;
154
+ bar_empty: string;
155
+ env: string;
156
+ session_id: string;
157
+ weekly_cost: string;
158
+ }
159
+
160
+ export interface SegmentData {
161
+ text: string;
162
+ bgColor: string;
163
+ fgColor: string;
164
+ }
165
+
166
+ interface BarStyleDef {
167
+ filled: string;
168
+ empty: string;
169
+ cap?: string;
170
+ marker?: string;
171
+ }
172
+
173
+ const BAR_STYLES: Record<string, BarStyleDef> = {
174
+ ball: { filled: "─", empty: "─", marker: "●" },
175
+ blocks: { filled: "█", empty: "░" },
176
+ "blocks-line": { filled: "█", empty: "─" },
177
+ capped: { filled: "━", empty: "┄", cap: "╸" },
178
+ dots: { filled: "●", empty: "○" },
179
+ filled: { filled: "■", empty: "□" },
180
+ geometric: { filled: "▰", empty: "▱" },
181
+ line: { filled: "━", empty: "┄" },
182
+ squares: { filled: "◼", empty: "◻" },
183
+ };
184
+
185
+ export class SegmentRenderer {
186
+ constructor(
187
+ private readonly config: PowerlineConfig,
188
+ private readonly symbols: PowerlineSymbols,
189
+ ) {}
190
+
191
+ renderDirectory(
192
+ hookData: ClaudeHookData,
193
+ colors: PowerlineColors,
194
+ config?: DirectorySegmentConfig,
195
+ ): SegmentData {
196
+ const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
197
+ const projectDir = hookData.workspace?.project_dir;
198
+
199
+ const style = config?.style ?? (config?.showBasename ? "basename" : "full");
200
+
201
+ if (style === "basename") {
202
+ const basename = currentDir.split(/[\\/]/).pop() || "root";
203
+ return {
204
+ text: basename,
205
+ bgColor: colors.modeBg,
206
+ fgColor: colors.modeFg,
207
+ };
208
+ }
209
+
210
+ const displayDir = collapseHome(currentDir);
211
+ const displayProjectDir = projectDir
212
+ ? collapseHome(projectDir)
213
+ : projectDir;
214
+
215
+ let dirName = this.getDisplayDirectoryName(displayDir, displayProjectDir);
216
+
217
+ if (style === "fish") {
218
+ dirName = abbreviateFishStyle(dirName);
219
+ }
220
+
221
+ return {
222
+ text: dirName,
223
+ bgColor: colors.modeBg,
224
+ fgColor: colors.modeFg,
225
+ };
226
+ }
227
+
228
+ renderGit(
229
+ gitInfo: GitInfo,
230
+ colors: PowerlineColors,
231
+ config?: GitSegmentConfig,
232
+ ): SegmentData | null {
233
+ if (!gitInfo) return null;
234
+
235
+ const parts: string[] = [];
236
+
237
+ if (config?.showRepoName && gitInfo.repoName) {
238
+ parts.push(gitInfo.repoName);
239
+ if (gitInfo.isWorktree) {
240
+ parts.push(this.symbols.git_worktree);
241
+ }
242
+ }
243
+
244
+ if (config?.showOperation && gitInfo.operation) {
245
+ parts.push(`[${gitInfo.operation}]`);
246
+ }
247
+
248
+ parts.push(`${this.symbols.branch} ${gitInfo.branch}`);
249
+
250
+ if (config?.showTag && gitInfo.tag) {
251
+ parts.push(`${this.symbols.git_tag} ${gitInfo.tag}`);
252
+ }
253
+
254
+ if (config?.showSha && gitInfo.sha) {
255
+ parts.push(`${this.symbols.git_sha} ${gitInfo.sha}`);
256
+ }
257
+
258
+ if (config?.showAheadBehind !== false) {
259
+ if (gitInfo.ahead > 0 && gitInfo.behind > 0) {
260
+ parts.push(
261
+ `${this.symbols.git_ahead}${gitInfo.ahead}${this.symbols.git_behind}${gitInfo.behind}`,
262
+ );
263
+ } else if (gitInfo.ahead > 0) {
264
+ parts.push(`${this.symbols.git_ahead}${gitInfo.ahead}`);
265
+ } else if (gitInfo.behind > 0) {
266
+ parts.push(`${this.symbols.git_behind}${gitInfo.behind}`);
267
+ }
268
+ }
269
+
270
+ if (config?.showWorkingTree) {
271
+ const counts: string[] = [];
272
+ if (gitInfo.staged && gitInfo.staged > 0)
273
+ counts.push(`+${gitInfo.staged}`);
274
+ if (gitInfo.unstaged && gitInfo.unstaged > 0)
275
+ counts.push(`~${gitInfo.unstaged}`);
276
+ if (gitInfo.untracked && gitInfo.untracked > 0)
277
+ counts.push(`?${gitInfo.untracked}`);
278
+ if (gitInfo.conflicts && gitInfo.conflicts > 0)
279
+ counts.push(`!${gitInfo.conflicts}`);
280
+ if (counts.length > 0) {
281
+ parts.push(`(${counts.join(" ")})`);
282
+ }
283
+ }
284
+
285
+ if (config?.showUpstream && gitInfo.upstream) {
286
+ parts.push(`${this.symbols.git_upstream}${gitInfo.upstream}`);
287
+ }
288
+
289
+ if (
290
+ config?.showStashCount &&
291
+ gitInfo.stashCount &&
292
+ gitInfo.stashCount > 0
293
+ ) {
294
+ parts.push(`${this.symbols.git_stash} ${gitInfo.stashCount}`);
295
+ }
296
+
297
+ if (config?.showTimeSinceCommit && gitInfo.timeSinceCommit !== undefined) {
298
+ const time = formatTimeSince(gitInfo.timeSinceCommit);
299
+ parts.push(`${this.symbols.git_time} ${time}`);
300
+ }
301
+
302
+ let gitStatusIcon = this.symbols.git_clean;
303
+ if (gitInfo.status === "conflicts") {
304
+ gitStatusIcon = this.symbols.git_conflicts;
305
+ } else if (gitInfo.status === "dirty") {
306
+ gitStatusIcon = this.symbols.git_dirty;
307
+ }
308
+ parts.push(gitStatusIcon);
309
+
310
+ return {
311
+ text: parts.join(" "),
312
+ bgColor: colors.gitBg,
313
+ fgColor: colors.gitFg,
314
+ };
315
+ }
316
+
317
+ renderModel(hookData: ClaudeHookData, colors: PowerlineColors): SegmentData {
318
+ const rawName = hookData.model?.display_name || "Claude";
319
+ const modelName = formatModelName(rawName);
320
+
321
+ return {
322
+ text: `${this.symbols.model} ${modelName}`,
323
+ bgColor: colors.modelBg,
324
+ fgColor: colors.modelFg,
325
+ };
326
+ }
327
+
328
+ renderSession(
329
+ usageInfo: UsageInfo,
330
+ colors: PowerlineColors,
331
+ config?: UsageSegmentConfig,
332
+ ): SegmentData {
333
+ const type = config?.type || "cost";
334
+ const costSource = config?.costSource;
335
+ const sessionBudget = this.config.budget?.session;
336
+
337
+ const getCost = () => {
338
+ if (costSource === "calculated") return usageInfo.session.calculatedCost;
339
+ if (costSource === "official") return usageInfo.session.officialCost;
340
+ return usageInfo.session.cost;
341
+ };
342
+
343
+ const formattedUsage = this.formatUsageWithBudget(
344
+ getCost(),
345
+ usageInfo.session.tokens,
346
+ usageInfo.session.tokenBreakdown,
347
+ type,
348
+ sessionBudget?.amount,
349
+ sessionBudget?.warningThreshold || 80,
350
+ sessionBudget?.type,
351
+ );
352
+
353
+ const text = `${this.symbols.session_cost} ${formattedUsage}`;
354
+
355
+ return {
356
+ text,
357
+ bgColor: colors.sessionBg,
358
+ fgColor: colors.sessionFg,
359
+ };
360
+ }
361
+
362
+ renderSessionId(
363
+ sessionId: string,
364
+ colors: PowerlineColors,
365
+ config?: SessionIdSegmentConfig,
366
+ ): SegmentData {
367
+ const showLabel = config?.showIdLabel !== false;
368
+ const text = showLabel
369
+ ? `${this.symbols.session_id} ${sessionId}`
370
+ : sessionId;
371
+
372
+ return {
373
+ text,
374
+ bgColor: colors.sessionBg,
375
+ fgColor: colors.sessionFg,
376
+ };
377
+ }
378
+
379
+ renderTmux(
380
+ sessionId: string | null,
381
+ colors: PowerlineColors,
382
+ ): SegmentData | null {
383
+ if (!sessionId) {
384
+ return {
385
+ text: `tmux:none`,
386
+ bgColor: colors.tmuxBg,
387
+ fgColor: colors.tmuxFg,
388
+ };
389
+ }
390
+
391
+ return {
392
+ text: `tmux:${sessionId}`,
393
+ bgColor: colors.tmuxBg,
394
+ fgColor: colors.tmuxFg,
395
+ };
396
+ }
397
+
398
+ renderContext(
399
+ contextInfo: ContextInfo | null,
400
+ colors: PowerlineColors,
401
+ config?: ContextSegmentConfig,
402
+ ): SegmentData | null {
403
+ const barLength = 10;
404
+ const style = config?.displayStyle ?? "text";
405
+ const defaultMode = style === "text" ? "remaining" : "used";
406
+ const mode = config?.percentageMode ?? defaultMode;
407
+
408
+ const barStyleDef = this.resolveBarStyleDef(style);
409
+
410
+ const emptyPct = mode === "remaining" ? "100%" : "0%";
411
+ if (!contextInfo) {
412
+ if (barStyleDef) {
413
+ const emptyBar = barStyleDef.empty.repeat(barLength);
414
+ return {
415
+ text: `${emptyBar} ${emptyPct}`,
416
+ bgColor: colors.contextBg,
417
+ fgColor: colors.contextFg,
418
+ };
419
+ }
420
+ return {
421
+ text: `${this.symbols.context_time} 0 (${emptyPct})`,
422
+ bgColor: colors.contextBg,
423
+ fgColor: colors.contextFg,
424
+ };
425
+ }
426
+
427
+ let bgColor = colors.contextBg;
428
+ let fgColor = colors.contextFg;
429
+
430
+ if (contextInfo.contextLeftPercentage <= 20) {
431
+ bgColor = colors.contextCriticalBg;
432
+ fgColor = colors.contextCriticalFg;
433
+ } else if (contextInfo.contextLeftPercentage <= 40) {
434
+ bgColor = colors.contextWarningBg;
435
+ fgColor = colors.contextWarningFg;
436
+ }
437
+
438
+ const pct =
439
+ mode === "remaining"
440
+ ? contextInfo.contextLeftPercentage
441
+ : contextInfo.usablePercentage;
442
+ const filledCount = Math.round(
443
+ (contextInfo.usablePercentage / 100) * barLength,
444
+ );
445
+ const emptyCount = barLength - filledCount;
446
+
447
+ if (barStyleDef) {
448
+ const bar = this.buildBar(
449
+ barStyleDef,
450
+ filledCount,
451
+ emptyCount,
452
+ barLength,
453
+ );
454
+
455
+ const text = config?.showPercentageOnly
456
+ ? `${bar} ${pct}%`
457
+ : `${bar} ${contextInfo.totalTokens.toLocaleString()} (${pct}%)`;
458
+
459
+ return { text, bgColor, fgColor };
460
+ }
461
+
462
+ const text = config?.showPercentageOnly
463
+ ? `${this.symbols.context_time} ${pct}%`
464
+ : `${this.symbols.context_time} ${contextInfo.totalTokens.toLocaleString()} (${pct}%)`;
465
+
466
+ return { text, bgColor, fgColor };
467
+ }
468
+
469
+ private buildBar(
470
+ s: BarStyleDef,
471
+ filledCount: number,
472
+ emptyCount: number,
473
+ barLength: number,
474
+ ): string {
475
+ if (s.marker) {
476
+ const pos = Math.min(filledCount, barLength - 1);
477
+ return (
478
+ s.filled.repeat(pos) + s.marker + s.empty.repeat(barLength - pos - 1)
479
+ );
480
+ }
481
+ if (s.cap) {
482
+ if (filledCount === 0) {
483
+ return s.cap + s.empty.repeat(barLength - 1);
484
+ }
485
+ if (filledCount >= barLength) {
486
+ return s.filled.repeat(barLength);
487
+ }
488
+ return (
489
+ s.filled.repeat(filledCount - 1) + s.cap + s.empty.repeat(emptyCount)
490
+ );
491
+ }
492
+ return s.filled.repeat(filledCount) + s.empty.repeat(emptyCount);
493
+ }
494
+
495
+ private resolveBarStyleDef(style: string): BarStyleDef | null {
496
+ return style === "bar"
497
+ ? { filled: this.symbols.bar_filled, empty: this.symbols.bar_empty }
498
+ : (BAR_STYLES[style] ?? null);
499
+ }
500
+
501
+ private formatPercentageWithBar(
502
+ pct: number,
503
+ displayStyle?: BarDisplayStyle,
504
+ timeStr?: string | null,
505
+ ): string {
506
+ const style = displayStyle ?? "text";
507
+ const barStyleDef = this.resolveBarStyleDef(style);
508
+ const barLength = 10;
509
+
510
+ if (barStyleDef) {
511
+ const filledCount = Math.round((pct / 100) * barLength);
512
+ const emptyCount = barLength - filledCount;
513
+ const bar = this.buildBar(
514
+ barStyleDef,
515
+ filledCount,
516
+ emptyCount,
517
+ barLength,
518
+ );
519
+ return timeStr ? `${bar} ${pct}% (${timeStr})` : `${bar} ${pct}%`;
520
+ }
521
+ return timeStr ? `${pct}% (${timeStr})` : `${pct}%`;
522
+ }
523
+
524
+ renderMetrics(
525
+ metricsInfo: MetricsInfo | null,
526
+ colors: PowerlineColors,
527
+ config?: MetricsSegmentConfig,
528
+ ): SegmentData | null {
529
+ if (!metricsInfo) {
530
+ return {
531
+ text: `${this.symbols.metrics_response} new`,
532
+ bgColor: colors.metricsBg,
533
+ fgColor: colors.metricsFg,
534
+ };
535
+ }
536
+
537
+ const parts: string[] = [];
538
+
539
+ if (config?.showLastResponseTime && metricsInfo.lastResponseTime !== null) {
540
+ const lastResponseTime =
541
+ metricsInfo.lastResponseTime < 60
542
+ ? `${metricsInfo.lastResponseTime.toFixed(1)}s`
543
+ : `${(metricsInfo.lastResponseTime / 60).toFixed(1)}m`;
544
+ parts.push(`${this.symbols.metrics_last_response} ${lastResponseTime}`);
545
+ }
546
+
547
+ if (
548
+ config?.showResponseTime !== false &&
549
+ metricsInfo.responseTime !== null
550
+ ) {
551
+ const responseTime =
552
+ metricsInfo.responseTime < 60
553
+ ? `${metricsInfo.responseTime.toFixed(1)}s`
554
+ : `${(metricsInfo.responseTime / 60).toFixed(1)}m`;
555
+ parts.push(`${this.symbols.metrics_response} ${responseTime}`);
556
+ }
557
+
558
+ if (
559
+ config?.showDuration !== false &&
560
+ metricsInfo.sessionDuration !== null
561
+ ) {
562
+ const duration = formatDuration(metricsInfo.sessionDuration);
563
+ parts.push(`${this.symbols.metrics_duration} ${duration}`);
564
+ }
565
+
566
+ if (
567
+ config?.showMessageCount !== false &&
568
+ metricsInfo.messageCount !== null
569
+ ) {
570
+ parts.push(
571
+ `${this.symbols.metrics_messages} ${metricsInfo.messageCount}`,
572
+ );
573
+ }
574
+
575
+ if (
576
+ config?.showLinesAdded !== false &&
577
+ metricsInfo.linesAdded !== null &&
578
+ metricsInfo.linesAdded > 0
579
+ ) {
580
+ parts.push(
581
+ `${this.symbols.metrics_lines_added} ${metricsInfo.linesAdded}`,
582
+ );
583
+ }
584
+
585
+ if (
586
+ config?.showLinesRemoved !== false &&
587
+ metricsInfo.linesRemoved !== null &&
588
+ metricsInfo.linesRemoved > 0
589
+ ) {
590
+ parts.push(
591
+ `${this.symbols.metrics_lines_removed} ${metricsInfo.linesRemoved}`,
592
+ );
593
+ }
594
+
595
+ if (parts.length === 0) {
596
+ return {
597
+ text: `${this.symbols.metrics_response} active`,
598
+ bgColor: colors.metricsBg,
599
+ fgColor: colors.metricsFg,
600
+ };
601
+ }
602
+
603
+ return {
604
+ text: parts.join(" "),
605
+ bgColor: colors.metricsBg,
606
+ fgColor: colors.metricsFg,
607
+ };
608
+ }
609
+
610
+ renderBlock(
611
+ blockInfo: BlockInfo,
612
+ colors: PowerlineColors,
613
+ config?: BlockSegmentConfig,
614
+ ): SegmentData {
615
+ const pct = Math.round(blockInfo.nativeUtilization);
616
+ const timeStr = formatLongTimeRemaining(blockInfo.timeRemaining);
617
+ const blockBudget = this.config.budget?.block;
618
+ const warningThreshold = blockBudget?.warningThreshold ?? 80;
619
+
620
+ let bgColor = colors.blockBg;
621
+ let fgColor = colors.blockFg;
622
+ if (pct >= warningThreshold) {
623
+ bgColor = colors.contextCriticalBg;
624
+ fgColor = colors.contextCriticalFg;
625
+ } else if (pct >= 50) {
626
+ bgColor = colors.contextWarningBg;
627
+ fgColor = colors.contextWarningFg;
628
+ }
629
+
630
+ return {
631
+ text: `${this.symbols.block_cost} ${this.formatPercentageWithBar(pct, config?.displayStyle, timeStr)}`,
632
+ bgColor,
633
+ fgColor,
634
+ };
635
+ }
636
+
637
+ renderWeekly(
638
+ hookData: ClaudeHookData,
639
+ colors: PowerlineColors,
640
+ config?: WeeklySegmentConfig,
641
+ ): SegmentData | null {
642
+ const sevenDay = hookData.rate_limits?.seven_day;
643
+ if (!sevenDay) return null;
644
+
645
+ const pct = Math.round(sevenDay.used_percentage);
646
+ const timeStr = formatLongTimeRemaining(
647
+ minutesUntilReset(sevenDay.resets_at),
648
+ );
649
+
650
+ let bgColor = colors.weeklyBg;
651
+ let fgColor = colors.weeklyFg;
652
+ if (pct >= 80) {
653
+ bgColor = colors.contextCriticalBg;
654
+ fgColor = colors.contextCriticalFg;
655
+ } else if (pct >= 50) {
656
+ bgColor = colors.contextWarningBg;
657
+ fgColor = colors.contextWarningFg;
658
+ }
659
+
660
+ return {
661
+ text: `${this.symbols.weekly_cost} ${this.formatPercentageWithBar(pct, config?.displayStyle, timeStr)}`,
662
+ bgColor,
663
+ fgColor,
664
+ };
665
+ }
666
+
667
+ renderToday(
668
+ todayInfo: TodayInfo,
669
+ colors: PowerlineColors,
670
+ type = "cost",
671
+ ): SegmentData {
672
+ const todayBudget = this.config.budget?.today;
673
+ const text = `${this.symbols.today_cost} ${this.formatUsageWithBudget(
674
+ todayInfo.cost,
675
+ todayInfo.tokens,
676
+ todayInfo.tokenBreakdown,
677
+ type,
678
+ todayBudget?.amount,
679
+ todayBudget?.warningThreshold,
680
+ todayBudget?.type,
681
+ )}`;
682
+
683
+ return {
684
+ text,
685
+ bgColor: colors.todayBg,
686
+ fgColor: colors.todayFg,
687
+ };
688
+ }
689
+
690
+ private getDisplayDirectoryName(
691
+ currentDir: string,
692
+ projectDir?: string,
693
+ ): string {
694
+ if (currentDir.startsWith("~")) {
695
+ return currentDir;
696
+ }
697
+
698
+ if (projectDir && projectDir !== currentDir) {
699
+ if (currentDir.startsWith(projectDir)) {
700
+ const relativePath = currentDir.slice(projectDir.length + 1);
701
+ return relativePath || projectDir.split(/[\\/]/).pop() || "project";
702
+ }
703
+ }
704
+
705
+ return currentDir;
706
+ }
707
+
708
+ private formatUsageDisplay(
709
+ cost: number | null,
710
+ tokens: number | null,
711
+ tokenBreakdown: TokenBreakdown | null,
712
+ type: string,
713
+ ): string {
714
+ switch (type) {
715
+ case "cost":
716
+ return formatCost(cost);
717
+ case "tokens":
718
+ return formatTokens(tokens);
719
+ case "both":
720
+ return `${formatCost(cost)} (${formatTokens(tokens)})`;
721
+ case "breakdown":
722
+ return formatTokenBreakdown(tokenBreakdown);
723
+ default:
724
+ return formatCost(cost);
725
+ }
726
+ }
727
+
728
+ private formatUsageWithBudget(
729
+ cost: number | null,
730
+ tokens: number | null,
731
+ tokenBreakdown: TokenBreakdown | null,
732
+ type: string,
733
+ budget: number | undefined,
734
+ warningThreshold = 80,
735
+ budgetType?: "cost" | "tokens",
736
+ ): string {
737
+ const baseDisplay = this.formatUsageDisplay(
738
+ cost,
739
+ tokens,
740
+ tokenBreakdown,
741
+ type,
742
+ );
743
+
744
+ if (budget && budget > 0) {
745
+ let budgetValue: number | null = null;
746
+
747
+ if (budgetType === "tokens" && tokens !== null) {
748
+ budgetValue = tokens;
749
+ } else if (budgetType === "cost" && cost !== null) {
750
+ budgetValue = cost;
751
+ } else if (!budgetType && cost !== null) {
752
+ budgetValue = cost;
753
+ }
754
+
755
+ if (budgetValue !== null) {
756
+ const budgetStatus = getBudgetStatus(
757
+ budgetValue,
758
+ budget,
759
+ warningThreshold,
760
+ );
761
+ return baseDisplay + budgetStatus.displayText;
762
+ }
763
+ }
764
+
765
+ return baseDisplay;
766
+ }
767
+
768
+ renderVersion(
769
+ hookData: ClaudeHookData,
770
+ colors: PowerlineColors,
771
+ _config?: VersionSegmentConfig,
772
+ ): SegmentData | null {
773
+ if (!hookData.version) {
774
+ return null;
775
+ }
776
+
777
+ return {
778
+ text: `${this.symbols.version} v${hookData.version}`,
779
+ bgColor: colors.versionBg,
780
+ fgColor: colors.versionFg,
781
+ };
782
+ }
783
+
784
+ renderEnv(
785
+ colors: PowerlineColors,
786
+ config: EnvSegmentConfig,
787
+ ): SegmentData | null {
788
+ const value = globalThis.process?.env?.[config.variable];
789
+ if (!value) return null;
790
+ const prefix = config.prefix ?? config.variable;
791
+ const text = prefix
792
+ ? `${this.symbols.env} ${prefix}: ${value}`
793
+ : `${this.symbols.env} ${value}`;
794
+ return { text, bgColor: colors.envBg, fgColor: colors.envFg };
795
+ }
796
+ }