@bastra-recall/statusline 0.9.2 → 1.0.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.
package/src/powerline.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { ClaudeHookData } from "./utils/claude";
2
- import type { PowerlineColors, ColorTheme } from "./themes";
1
+ import { dropUnreadableRateLimits, type ClaudeHookData } from "./utils/claude";
3
2
  import type { PowerlineConfig, LineConfig } from "./config/loader";
4
3
  import type {
5
4
  UsageInfo,
@@ -7,191 +6,73 @@ import type {
7
6
  MetricsInfo,
8
7
  PowerlineSymbols,
9
8
  AnySegmentConfig,
10
- DirectorySegmentConfig,
11
- GitSegmentConfig,
12
- UsageSegmentConfig,
13
9
  ContextSegmentConfig,
14
- MetricsSegmentConfig,
15
- BlockSegmentConfig,
16
- TodaySegmentConfig,
17
- VersionSegmentConfig,
18
- SessionIdSegmentConfig,
19
- EnvSegmentConfig,
20
- WeeklySegmentConfig,
21
- AgentSegmentConfig,
22
- ThinkingSegmentConfig,
23
- CacheTimerSegmentConfig,
24
10
  } from "./segments";
25
11
  import type { BlockInfo } from "./segments/block";
26
12
  import type { TodayInfo } from "./segments/today";
27
13
  import type { CacheTimerInfo } from "./segments/cacheTimer";
28
14
  import type { TuiData } from "./tui";
15
+ import type { RenderedSegment } from "./powerline-layout";
29
16
 
30
- import {
31
- hexToAnsi,
32
- extractBgToFg,
33
- hexToBasicAnsi,
34
- hexTo256Ansi,
35
- hexColorDistance,
36
- } from "./utils/colors";
37
- import { getColorSupport } from "./utils/color-support";
38
- import { getTheme } from "./themes";
39
- import {
40
- UsageProvider,
41
- ContextProvider,
42
- GitService,
43
- TmuxService,
44
- MetricsProvider,
45
- SegmentRenderer,
46
- } from "./segments";
47
- import { BlockProvider } from "./segments/block";
48
- import { TodayProvider } from "./segments/today";
49
- import { CacheTimerProvider } from "./segments/cacheTimer";
50
- import { BastraProvider } from "./segments/bastra";
51
- import type { BastraSegmentConfig } from "./segments/renderer";
52
- import {
53
- SYMBOLS,
54
- TEXT_SYMBOLS,
55
- RESET_CODE,
56
- BOX_CHARS,
57
- BOX_CHARS_TEXT,
58
- } from "./utils/constants";
59
- import { visibleLength } from "./utils/terminal";
17
+ import { BOX_CHARS, BOX_CHARS_TEXT } from "./utils/constants";
60
18
  import { getTerminalWidth, getRawTerminalWidth } from "./utils/terminal-width";
61
19
  import { renderTuiPanel } from "./tui";
62
-
63
- interface RenderedSegment {
64
- type: string;
65
- text: string;
66
- bgColor: string;
67
- fgColor: string;
68
- bold?: boolean;
69
- }
70
-
20
+ import { PowerlineTheme } from "./powerline-theme";
21
+ import { PowerlineSegments } from "./powerline-segments";
22
+ import { PowerlineLayout } from "./powerline-layout";
23
+
24
+ /**
25
+ * Renders the statusline for one hook tick. Since #1039 the work is split
26
+ * over three collaborators: PowerlineTheme (glyphs, colours), PowerlineSegments
27
+ * (data providers, per-segment dispatch) and PowerlineLayout (measuring,
28
+ * wrapping, ANSI line assembly). This class keeps the flow: which data to
29
+ * fetch, which lines to render, and the TUI hand-over.
30
+ */
71
31
  export class PowerlineRenderer {
72
32
  private readonly symbols: PowerlineSymbols;
73
- private _usageProvider?: UsageProvider;
74
- private _blockProvider?: BlockProvider;
75
- private _todayProvider?: TodayProvider;
76
- private _contextProvider?: ContextProvider;
77
- private _gitService?: GitService;
78
- private _tmuxService?: TmuxService;
79
- private _metricsProvider?: MetricsProvider;
80
- private _cacheTimerProvider?: CacheTimerProvider;
81
- private _bastraProvider?: BastraProvider;
82
- private _segmentRenderer?: SegmentRenderer;
33
+ private readonly theme: PowerlineTheme;
34
+ private readonly segments: PowerlineSegments;
35
+ private readonly layout: PowerlineLayout;
83
36
 
84
37
  constructor(private readonly config: PowerlineConfig) {
85
- this.symbols = this.initializeSymbols();
86
- }
87
-
88
- private get usageProvider(): UsageProvider {
89
- if (!this._usageProvider) {
90
- this._usageProvider = new UsageProvider();
91
- }
92
- return this._usageProvider;
93
- }
94
-
95
- private get blockProvider(): BlockProvider {
96
- if (!this._blockProvider) {
97
- this._blockProvider = new BlockProvider();
98
- }
99
- return this._blockProvider;
100
- }
101
-
102
- private get todayProvider(): TodayProvider {
103
- if (!this._todayProvider) {
104
- this._todayProvider = new TodayProvider();
105
- }
106
- return this._todayProvider;
107
- }
108
-
109
- private get contextProvider(): ContextProvider {
110
- if (!this._contextProvider) {
111
- this._contextProvider = new ContextProvider(this.config);
112
- }
113
- return this._contextProvider;
114
- }
115
-
116
- private get gitService(): GitService {
117
- if (!this._gitService) {
118
- this._gitService = new GitService();
119
- }
120
- return this._gitService;
121
- }
122
-
123
- private get tmuxService(): TmuxService {
124
- if (!this._tmuxService) {
125
- this._tmuxService = new TmuxService();
126
- }
127
- return this._tmuxService;
128
- }
129
-
130
- private get metricsProvider(): MetricsProvider {
131
- if (!this._metricsProvider) {
132
- this._metricsProvider = new MetricsProvider();
133
- }
134
- return this._metricsProvider;
135
- }
136
-
137
- private get cacheTimerProvider(): CacheTimerProvider {
138
- if (!this._cacheTimerProvider) {
139
- this._cacheTimerProvider = new CacheTimerProvider();
140
- }
141
- return this._cacheTimerProvider;
142
- }
143
-
144
- private get bastraProvider(): BastraProvider {
145
- if (!this._bastraProvider) {
146
- this._bastraProvider = new BastraProvider();
147
- }
148
- return this._bastraProvider;
149
- }
150
-
151
- private get segmentRenderer(): SegmentRenderer {
152
- if (!this._segmentRenderer) {
153
- this._segmentRenderer = new SegmentRenderer(this.config, this.symbols);
154
- }
155
- return this._segmentRenderer;
156
- }
157
-
158
- private needsSegmentInfo(segmentType: keyof LineConfig["segments"]): boolean {
159
- return this.config.display.lines.some(
160
- (line) => line.segments[segmentType]?.enabled,
161
- );
38
+ this.theme = new PowerlineTheme(config);
39
+ this.symbols = this.theme.initializeSymbols();
40
+ this.segments = new PowerlineSegments(config, this.symbols);
41
+ this.layout = new PowerlineLayout(config, this.symbols, this.theme);
162
42
  }
163
43
 
164
- async generateStatusline(hookData: ClaudeHookData): Promise<string> {
44
+ async generateStatusline(rawHookData: ClaudeHookData): Promise<string> {
45
+ const hookData = dropUnreadableRateLimits(rawHookData);
165
46
  if (this.config.display.style === "tui") {
166
47
  return this.generateTuiStatusline(hookData);
167
48
  }
168
49
 
169
- const usageInfo = this.needsSegmentInfo("session")
170
- ? await this.usageProvider.getUsageInfo(hookData.session_id, hookData)
50
+ const usageInfo = this.segments.needsSegmentInfo("session")
51
+ ? await this.segments.usageProvider.getUsageInfo(hookData.session_id, hookData)
171
52
  : null;
172
53
 
173
- const blockInfo = this.needsSegmentInfo("block")
174
- ? await this.blockProvider.getActiveBlockInfo(hookData)
54
+ const blockInfo = this.segments.needsSegmentInfo("block")
55
+ ? await this.segments.blockProvider.getActiveBlockInfo(hookData)
175
56
  : null;
176
57
 
177
- const todayInfo = this.needsSegmentInfo("today")
178
- ? await this.todayProvider.getTodayInfo()
58
+ const todayInfo = this.segments.needsSegmentInfo("today")
59
+ ? await this.segments.todayProvider.getTodayInfo()
179
60
  : null;
180
61
 
181
62
  const contextSegmentConfig = this.config.display.lines
182
63
  .map((line) => line.segments.context)
183
64
  .find((c) => c?.enabled) as ContextSegmentConfig | undefined;
184
65
  const autocompactBuffer = contextSegmentConfig?.autocompactBuffer ?? 33000;
185
- const contextInfo = this.needsSegmentInfo("context")
186
- ? await this.contextProvider.getContextInfo(hookData, autocompactBuffer)
66
+ const contextInfo = this.segments.needsSegmentInfo("context")
67
+ ? await this.segments.contextProvider.getContextInfo(hookData, autocompactBuffer)
187
68
  : null;
188
69
 
189
- const metricsInfo = this.needsSegmentInfo("metrics")
190
- ? await this.metricsProvider.getMetricsInfo(hookData.session_id, hookData)
70
+ const metricsInfo = this.segments.needsSegmentInfo("metrics")
71
+ ? await this.segments.metricsProvider.getMetricsInfo(hookData.session_id, hookData)
191
72
  : null;
192
73
 
193
- const cacheTimerInfo = this.needsSegmentInfo("cacheTimer")
194
- ? await this.cacheTimerProvider.getCacheTimerInfo(hookData)
74
+ const cacheTimerInfo = this.segments.needsSegmentInfo("cacheTimer")
75
+ ? await this.segments.cacheTimerProvider.getCacheTimerInfo(hookData)
195
76
  : null;
196
77
 
197
78
  if (this.config.display.autoWrap) {
@@ -233,7 +114,7 @@ export class PowerlineRenderer {
233
114
  metricsInfo: MetricsInfo | null,
234
115
  cacheTimerInfo: CacheTimerInfo | null,
235
116
  ): Promise<string> {
236
- const colors = this.getThemeColors();
117
+ const colors = this.theme.getThemeColors();
237
118
  const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
238
119
  const terminalWidth = getTerminalWidth();
239
120
 
@@ -252,7 +133,7 @@ export class PowerlineRenderer {
252
133
 
253
134
  const renderedSegments: RenderedSegment[] = [];
254
135
  for (const segment of segments) {
255
- const segmentData = await this.renderSegment(
136
+ const segmentData = await this.segments.renderSegment(
256
137
  segment,
257
138
  hookData,
258
139
  usageInfo,
@@ -278,40 +159,9 @@ export class PowerlineRenderer {
278
159
 
279
160
  if (renderedSegments.length === 0) continue;
280
161
 
281
- if (!terminalWidth || terminalWidth <= 0) {
282
- outputLines.push(this.buildLineFromSegments(renderedSegments, colors));
283
- continue;
284
- }
285
-
286
- let currentLineSegments: RenderedSegment[] = [];
287
- let currentLineWidth = 0;
288
-
289
- for (const segment of renderedSegments) {
290
- const segmentWidth = this.calculateSegmentWidth(
291
- segment,
292
- currentLineSegments.length === 0,
293
- );
294
-
295
- if (
296
- currentLineSegments.length > 0 &&
297
- currentLineWidth + segmentWidth > terminalWidth
298
- ) {
299
- outputLines.push(
300
- this.buildLineFromSegments(currentLineSegments, colors),
301
- );
302
- currentLineSegments = [];
303
- currentLineWidth = 0;
304
- }
305
-
306
- currentLineSegments.push(segment);
307
- currentLineWidth += segmentWidth;
308
- }
309
-
310
- if (currentLineSegments.length > 0) {
311
- outputLines.push(
312
- this.buildLineFromSegments(currentLineSegments, colors),
313
- );
314
- }
162
+ outputLines.push(
163
+ ...this.layout.wrapSegments(renderedSegments, terminalWidth, colors),
164
+ );
315
165
  }
316
166
 
317
167
  return outputLines.join("\n");
@@ -320,7 +170,7 @@ export class PowerlineRenderer {
320
170
  private async generateTuiStatusline(
321
171
  hookData: ClaudeHookData,
322
172
  ): Promise<string> {
323
- const colors = this.getThemeColors();
173
+ const colors = this.theme.getThemeColors();
324
174
  const terminalWidth = getTerminalWidth();
325
175
  const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
326
176
  const charset = this.config.display.charset || "unicode";
@@ -331,12 +181,12 @@ export class PowerlineRenderer {
331
181
  const autocompactBuffer = contextSegmentConfig?.autocompactBuffer ?? 33000;
332
182
 
333
183
  const results = await Promise.allSettled([
334
- this.usageProvider.getUsageInfo(hookData.session_id, hookData),
335
- this.blockProvider.getActiveBlockInfo(hookData),
336
- this.todayProvider.getTodayInfo(),
337
- this.contextProvider.getContextInfo(hookData, autocompactBuffer),
338
- this.metricsProvider.getMetricsInfo(hookData.session_id, hookData),
339
- this.gitService.getGitInfo(
184
+ this.segments.usageProvider.getUsageInfo(hookData.session_id, hookData),
185
+ this.segments.blockProvider.getActiveBlockInfo(hookData),
186
+ this.segments.todayProvider.getTodayInfo(),
187
+ this.segments.contextProvider.getContextInfo(hookData, autocompactBuffer),
188
+ this.segments.metricsProvider.getMetricsInfo(hookData.session_id, hookData),
189
+ this.segments.gitService.getGitInfo(
340
190
  currentDir,
341
191
  {
342
192
  showSha: false,
@@ -350,8 +200,8 @@ export class PowerlineRenderer {
350
200
  },
351
201
  hookData.workspace?.project_dir,
352
202
  ),
353
- this.tmuxService.getSessionId(),
354
- this.cacheTimerProvider.getCacheTimerInfo(hookData),
203
+ this.segments.tmuxService.getSessionId(),
204
+ this.segments.cacheTimerProvider.getCacheTimerInfo(hookData),
355
205
  ]);
356
206
  const val = <T>(r: PromiseSettledResult<T>) =>
357
207
  r.status === "fulfilled" ? r.value : null;
@@ -384,7 +234,7 @@ export class PowerlineRenderer {
384
234
  metricsInfo,
385
235
  gitInfo,
386
236
  cacheTimerInfo,
387
- bastraInfo: this.bastraProvider.getBastraInfo(), // #53: TUI parity
237
+ bastraInfo: this.segments.bastraProvider.getBastraInfo(), // #53: TUI parity
388
238
  tmuxSessionId,
389
239
  colors,
390
240
  };
@@ -399,58 +249,6 @@ export class PowerlineRenderer {
399
249
  );
400
250
  }
401
251
 
402
- private calculateSegmentWidth(
403
- segment: RenderedSegment,
404
- isFirst: boolean,
405
- ): number {
406
- const isCapsuleStyle = this.config.display.style === "capsule";
407
- const textWidth = visibleLength(segment.text);
408
- const padding = this.config.display.padding ?? 1;
409
- const paddingWidth = padding * 2;
410
-
411
- if (isCapsuleStyle) {
412
- const capsuleOverhead = 2 + paddingWidth + (isFirst ? 0 : 1);
413
- return textWidth + capsuleOverhead;
414
- }
415
-
416
- const powerlineOverhead = 1 + paddingWidth;
417
- return textWidth + powerlineOverhead;
418
- }
419
-
420
- private buildLineFromSegments(
421
- segments: RenderedSegment[],
422
- colors: PowerlineColors,
423
- ): string {
424
- const isCapsuleStyle = this.config.display.style === "capsule";
425
- let line = colors.reset;
426
-
427
- for (let i = 0; i < segments.length; i++) {
428
- const segment = segments[i];
429
- if (!segment) continue;
430
-
431
- const isFirst = i === 0;
432
- const isLast = i === segments.length - 1;
433
- const nextSegment = !isLast ? segments[i + 1] : null;
434
-
435
- if (isCapsuleStyle && !isFirst) {
436
- line += " ";
437
- }
438
-
439
- const bold =
440
- segment.bold ?? this.getSegmentBoldFlag(segment.type, colors);
441
-
442
- line += this.formatSegment(
443
- segment.bgColor,
444
- segment.fgColor,
445
- segment.text,
446
- nextSegment?.bgColor,
447
- colors,
448
- bold,
449
- );
450
- }
451
-
452
- return line;
453
- }
454
252
 
455
253
  private async renderLine(
456
254
  lineConfig: LineConfig,
@@ -462,7 +260,7 @@ export class PowerlineRenderer {
462
260
  metricsInfo: MetricsInfo | null,
463
261
  cacheTimerInfo: CacheTimerInfo | null,
464
262
  ): Promise<string> {
465
- const colors = this.getThemeColors();
263
+ const colors = this.theme.getThemeColors();
466
264
  const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
467
265
 
468
266
  const segments = Object.entries(lineConfig.segments)
@@ -474,7 +272,7 @@ export class PowerlineRenderer {
474
272
 
475
273
  const renderedSegments: RenderedSegment[] = [];
476
274
  for (const segment of segments) {
477
- const segmentData = await this.renderSegment(
275
+ const segmentData = await this.segments.renderSegment(
478
276
  segment,
479
277
  hookData,
480
278
  usageInfo,
@@ -498,586 +296,6 @@ export class PowerlineRenderer {
498
296
  }
499
297
  }
500
298
 
501
- return this.buildLineFromSegments(renderedSegments, colors);
502
- }
503
-
504
- private async renderSegment(
505
- segment: { type: string; config: AnySegmentConfig },
506
- hookData: ClaudeHookData,
507
- usageInfo: UsageInfo | null,
508
- blockInfo: BlockInfo | null,
509
- todayInfo: TodayInfo | null,
510
- contextInfo: ContextInfo | null,
511
- metricsInfo: MetricsInfo | null,
512
- cacheTimerInfo: CacheTimerInfo | null,
513
- colors: PowerlineColors,
514
- currentDir: string,
515
- ) {
516
- if (segment.type === "directory") {
517
- return this.segmentRenderer.renderDirectory(
518
- hookData,
519
- colors,
520
- segment.config as DirectorySegmentConfig,
521
- );
522
- }
523
- if (segment.type === "model") {
524
- return this.segmentRenderer.renderModel(hookData, colors, segment.config);
525
- }
526
-
527
- if (segment.type === "git") {
528
- return await this.renderGitSegment(
529
- segment.config as GitSegmentConfig,
530
- hookData,
531
- colors,
532
- currentDir,
533
- );
534
- }
535
-
536
- if (segment.type === "session") {
537
- return this.renderSessionSegment(
538
- segment.config as UsageSegmentConfig,
539
- usageInfo,
540
- colors,
541
- );
542
- }
543
-
544
- if (segment.type === "sessionId") {
545
- return hookData.session_id
546
- ? this.segmentRenderer.renderSessionId(
547
- hookData.session_id,
548
- colors,
549
- segment.config as SessionIdSegmentConfig,
550
- )
551
- : null;
552
- }
553
-
554
- if (segment.type === "tmux") {
555
- return await this.renderTmuxSegment(colors);
556
- }
557
-
558
- if (segment.type === "context") {
559
- return this.renderContextSegment(
560
- segment.config as ContextSegmentConfig,
561
- contextInfo,
562
- colors,
563
- );
564
- }
565
-
566
- if (segment.type === "metrics") {
567
- return this.renderMetricsSegment(
568
- segment.config as MetricsSegmentConfig,
569
- metricsInfo,
570
- blockInfo,
571
- colors,
572
- );
573
- }
574
-
575
- if (segment.type === "block") {
576
- return this.renderBlockSegment(
577
- segment.config as BlockSegmentConfig,
578
- blockInfo,
579
- colors,
580
- );
581
- }
582
-
583
- if (segment.type === "today") {
584
- return this.renderTodaySegment(
585
- segment.config as TodaySegmentConfig,
586
- todayInfo,
587
- colors,
588
- );
589
- }
590
-
591
- if (segment.type === "version") {
592
- return this.renderVersionSegment(
593
- segment.config as VersionSegmentConfig,
594
- hookData,
595
- colors,
596
- );
597
- }
598
-
599
- if (segment.type === "env") {
600
- return this.segmentRenderer.renderEnv(
601
- colors,
602
- segment.config as EnvSegmentConfig,
603
- );
604
- }
605
-
606
- if (segment.type === "weekly") {
607
- return this.segmentRenderer.renderWeekly(
608
- hookData,
609
- colors,
610
- segment.config as WeeklySegmentConfig,
611
- );
612
- }
613
-
614
- if (segment.type === "agent") {
615
- return this.segmentRenderer.renderAgent(
616
- hookData,
617
- colors,
618
- segment.config as AgentSegmentConfig,
619
- );
620
- }
621
-
622
- if (segment.type === "thinking") {
623
- return this.segmentRenderer.renderThinking(
624
- hookData,
625
- colors,
626
- segment.config as ThinkingSegmentConfig,
627
- );
628
- }
629
-
630
- if (segment.type === "cacheTimer") {
631
- if (!cacheTimerInfo) return null;
632
- return this.segmentRenderer.renderCacheTimer(
633
- cacheTimerInfo,
634
- colors,
635
- segment.config as CacheTimerSegmentConfig,
636
- );
637
- }
638
-
639
- if (segment.type === "bastra") {
640
- const info = this.bastraProvider.getBastraInfo();
641
- return this.segmentRenderer.renderBastra(
642
- info,
643
- colors,
644
- segment.config as BastraSegmentConfig,
645
- );
646
- }
647
-
648
- return null;
649
- }
650
-
651
- private async renderGitSegment(
652
- config: GitSegmentConfig,
653
- hookData: ClaudeHookData,
654
- colors: PowerlineColors,
655
- currentDir: string,
656
- ) {
657
- if (!this.needsSegmentInfo("git")) return null;
658
-
659
- const gitInfo = await this.gitService.getGitInfo(
660
- currentDir,
661
- {
662
- showSha: config?.showSha,
663
- showWorkingTree: config?.showWorkingTree,
664
- showOperation: config?.showOperation,
665
- showTag: config?.showTag,
666
- showTimeSinceCommit: config?.showTimeSinceCommit,
667
- showStashCount: config?.showStashCount,
668
- showUpstream: config?.showUpstream,
669
- showRepoName: config?.showRepoName,
670
- },
671
- hookData.workspace?.project_dir,
672
- );
673
-
674
- return gitInfo
675
- ? this.segmentRenderer.renderGit(gitInfo, colors, config)
676
- : null;
677
- }
678
-
679
- private renderSessionSegment(
680
- config: UsageSegmentConfig,
681
- usageInfo: UsageInfo | null,
682
- colors: PowerlineColors,
683
- ) {
684
- if (!usageInfo) return null;
685
- return this.segmentRenderer.renderSession(usageInfo, colors, config);
686
- }
687
-
688
- private async renderTmuxSegment(colors: PowerlineColors) {
689
- if (!this.needsSegmentInfo("tmux")) return null;
690
- const tmuxSessionId = await this.tmuxService.getSessionId();
691
- return this.segmentRenderer.renderTmux(tmuxSessionId, colors);
692
- }
693
-
694
- private renderContextSegment(
695
- config: ContextSegmentConfig,
696
- contextInfo: ContextInfo | null,
697
- colors: PowerlineColors,
698
- ) {
699
- if (!this.needsSegmentInfo("context")) return null;
700
- return this.segmentRenderer.renderContext(contextInfo, colors, config);
701
- }
702
-
703
- private renderMetricsSegment(
704
- config: MetricsSegmentConfig,
705
- metricsInfo: MetricsInfo | null,
706
- _blockInfo: BlockInfo | null,
707
- colors: PowerlineColors,
708
- ) {
709
- return this.segmentRenderer.renderMetrics(metricsInfo, colors, config);
710
- }
711
-
712
- private renderBlockSegment(
713
- config: BlockSegmentConfig,
714
- blockInfo: BlockInfo | null,
715
- colors: PowerlineColors,
716
- ) {
717
- if (!blockInfo) return null;
718
- return this.segmentRenderer.renderBlock(blockInfo, colors, config);
719
- }
720
-
721
- private renderTodaySegment(
722
- config: TodaySegmentConfig,
723
- todayInfo: TodayInfo | null,
724
- colors: PowerlineColors,
725
- ) {
726
- if (!todayInfo) return null;
727
- return this.segmentRenderer.renderToday(todayInfo, colors, config);
728
- }
729
-
730
- private renderVersionSegment(
731
- config: VersionSegmentConfig,
732
- hookData: ClaudeHookData,
733
- colors: PowerlineColors,
734
- ) {
735
- return this.segmentRenderer.renderVersion(hookData, colors, config);
736
- }
737
-
738
- private initializeSymbols(): PowerlineSymbols {
739
- const style = this.config.display.style;
740
- const charset = this.config.display.charset || "unicode";
741
- const isMinimalStyle = style === "minimal";
742
- const isCapsuleStyle = style === "capsule";
743
- const symbolSet = charset === "text" ? TEXT_SYMBOLS : SYMBOLS;
744
-
745
- return {
746
- right: isMinimalStyle
747
- ? ""
748
- : isCapsuleStyle
749
- ? symbolSet.right_rounded
750
- : symbolSet.right,
751
- left: isCapsuleStyle ? symbolSet.left_rounded : "",
752
- branch: symbolSet.branch,
753
- model: symbolSet.model,
754
- git_clean: symbolSet.git_clean,
755
- git_dirty: symbolSet.git_dirty,
756
- git_conflicts: symbolSet.git_conflicts,
757
- git_ahead: symbolSet.git_ahead,
758
- git_behind: symbolSet.git_behind,
759
- git_worktree: symbolSet.git_worktree,
760
- git_tag: symbolSet.git_tag,
761
- git_sha: symbolSet.git_sha,
762
- git_upstream: symbolSet.git_upstream,
763
- git_stash: symbolSet.git_stash,
764
- git_time: symbolSet.git_time,
765
- session_cost: symbolSet.session_cost,
766
- block_cost: symbolSet.block_cost,
767
- today_cost: symbolSet.today_cost,
768
- context_time: symbolSet.context_time,
769
- metrics_response: symbolSet.metrics_response,
770
- metrics_last_response: symbolSet.metrics_last_response,
771
- metrics_duration: symbolSet.metrics_duration,
772
- metrics_messages: symbolSet.metrics_messages,
773
- metrics_lines_added: symbolSet.metrics_lines_added,
774
- metrics_lines_removed: symbolSet.metrics_lines_removed,
775
- metrics_burn: symbolSet.metrics_burn,
776
- version: symbolSet.version,
777
- bar_filled: symbolSet.bar_filled,
778
- bar_empty: symbolSet.bar_empty,
779
- env: symbolSet.env,
780
- session_id: symbolSet.session_id,
781
- weekly_cost: symbolSet.weekly_cost,
782
- agent: symbolSet.agent,
783
- thinking: symbolSet.thinking,
784
- cache_timer: symbolSet.cache_timer,
785
- bastra: symbolSet.bastra,
786
- };
787
- }
788
-
789
- private getThemeColors(): PowerlineColors {
790
- const theme = this.config.theme;
791
- let colorTheme;
792
-
793
- const colorMode = this.config.display.colorCompatibility || "auto";
794
- const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode;
795
-
796
- if (theme === "custom") {
797
- colorTheme = this.config.colors?.custom;
798
- if (!colorTheme) {
799
- throw new Error(
800
- "Custom theme selected but no colors provided in configuration",
801
- );
802
- }
803
- } else {
804
- colorTheme = getTheme(theme, colorSupport);
805
- if (!colorTheme) {
806
- console.warn(
807
- `Built-in theme '${theme}' not found, falling back to 'dark' theme`,
808
- );
809
- colorTheme = getTheme("dark", colorSupport)!;
810
- }
811
- }
812
-
813
- const convertHex = (hex: string, isBg: boolean): string => {
814
- if (colorSupport === "none") return "";
815
- if (colorSupport === "ansi") return hexToBasicAnsi(hex, isBg);
816
- if (colorSupport === "ansi256") return hexTo256Ansi(hex, isBg);
817
- return hexToAnsi(hex, isBg);
818
- };
819
-
820
- const fallbackTheme = getTheme("dark", colorSupport)!;
821
-
822
- const isTui = this.config.display.style === "tui";
823
- const isLightTheme = theme === "light";
824
- const terminalRef = isLightTheme ? "#f0f0f0" : "#1e1e1e";
825
-
826
- const getSegmentColors = (segment: Exclude<keyof ColorTheme, "tui">) => {
827
- const fallback = fallbackTheme[segment];
828
- const custom = colorTheme[segment];
829
- const colors = {
830
- fg: custom?.fg || fallback.fg,
831
- bg: custom?.bg || fallback.bg,
832
- };
833
-
834
- let fgHex = colors.fg;
835
- if (isTui && hexColorDistance(fgHex, terminalRef) < 60) {
836
- fgHex = colors.bg;
837
- }
838
-
839
- const bold =
840
- colorSupport !== "none" && Boolean(custom?.bold ?? fallback.bold);
841
-
842
- return {
843
- bg: convertHex(colors.bg, true),
844
- fg: convertHex(fgHex, false),
845
- bold,
846
- };
847
- };
848
-
849
- const directory = getSegmentColors("directory");
850
- const git = getSegmentColors("git");
851
- const model = getSegmentColors("model");
852
- const session = getSegmentColors("session");
853
- const block = getSegmentColors("block");
854
- const today = getSegmentColors("today");
855
- const tmux = getSegmentColors("tmux");
856
- const context = getSegmentColors("context");
857
- const contextWarning = getSegmentColors("contextWarning");
858
- const contextCritical = getSegmentColors("contextCritical");
859
- const metrics = getSegmentColors("metrics");
860
- const version = getSegmentColors("version");
861
- const env = getSegmentColors("env");
862
- const weekly = getSegmentColors("weekly");
863
- const agent = getSegmentColors("agent");
864
- const thinking = getSegmentColors("thinking");
865
- const cacheTimer = getSegmentColors("cacheTimer");
866
-
867
- // bastra brand color — fixed across all themes (not theme-derived), so
868
- // the bastra segment is always recognizable. White fg on bastra purple.
869
- const bastra = {
870
- bg: convertHex("#7c3aed", true),
871
- fg: convertHex("#ffffff", false),
872
- bold: colorSupport !== "none",
873
- };
874
-
875
- return {
876
- reset: colorSupport === "none" ? "" : RESET_CODE,
877
- modeBg: directory.bg,
878
- modeFg: directory.fg,
879
- modeBold: directory.bold,
880
- gitBg: git.bg,
881
- gitFg: git.fg,
882
- gitBold: git.bold,
883
- modelBg: model.bg,
884
- modelFg: model.fg,
885
- modelBold: model.bold,
886
- sessionBg: session.bg,
887
- sessionFg: session.fg,
888
- sessionBold: session.bold,
889
- blockBg: block.bg,
890
- blockFg: block.fg,
891
- blockBold: block.bold,
892
- todayBg: today.bg,
893
- todayFg: today.fg,
894
- todayBold: today.bold,
895
- tmuxBg: tmux.bg,
896
- tmuxFg: tmux.fg,
897
- tmuxBold: tmux.bold,
898
- contextBg: context.bg,
899
- contextFg: context.fg,
900
- contextBold: context.bold,
901
- contextWarningBg: contextWarning.bg,
902
- contextWarningFg: contextWarning.fg,
903
- contextWarningBold: contextWarning.bold,
904
- contextCriticalBg: contextCritical.bg,
905
- contextCriticalFg: contextCritical.fg,
906
- contextCriticalBold: contextCritical.bold,
907
- metricsBg: metrics.bg,
908
- metricsFg: metrics.fg,
909
- metricsBold: metrics.bold,
910
- versionBg: version.bg,
911
- versionFg: version.fg,
912
- versionBold: version.bold,
913
- envBg: env.bg,
914
- envFg: env.fg,
915
- envBold: env.bold,
916
- weeklyBg: weekly.bg,
917
- weeklyFg: weekly.fg,
918
- weeklyBold: weekly.bold,
919
- agentBg: agent.bg,
920
- agentFg: agent.fg,
921
- agentBold: agent.bold,
922
- thinkingBg: thinking.bg,
923
- thinkingFg: thinking.fg,
924
- thinkingBold: thinking.bold,
925
- cacheTimerBg: cacheTimer.bg,
926
- cacheTimerFg: cacheTimer.fg,
927
- cacheTimerBold: cacheTimer.bold,
928
- bastraBg: bastra.bg,
929
- bastraFg: bastra.fg,
930
- bastraBold: bastra.bold,
931
- partFg: theme === "custom" ? this.resolvePartColors(convertHex) : {},
932
- };
933
- }
934
-
935
- private resolvePartColors(
936
- convertHex: (hex: string, isBg: boolean) => string,
937
- ): Record<string, string> {
938
- const custom = this.config.colors?.custom as
939
- | Record<string, { fg?: string }>
940
- | undefined;
941
- if (!custom) return {};
942
-
943
- const result: Record<string, string> = {};
944
- for (const key of Object.keys(custom)) {
945
- const entry = custom[key];
946
- if (!entry?.fg) continue;
947
- result[key] = convertHex(entry.fg, false);
948
- }
949
- return result;
950
- }
951
-
952
- private getSegmentBgColor(
953
- segmentType: string,
954
- colors: PowerlineColors,
955
- ): string {
956
- switch (segmentType) {
957
- case "directory":
958
- return colors.modeBg;
959
- case "git":
960
- return colors.gitBg;
961
- case "model":
962
- return colors.modelBg;
963
- case "session":
964
- case "sessionId":
965
- return colors.sessionBg;
966
- case "block":
967
- return colors.blockBg;
968
- case "today":
969
- return colors.todayBg;
970
- case "tmux":
971
- return colors.tmuxBg;
972
- case "context":
973
- return colors.contextBg;
974
- case "metrics":
975
- return colors.metricsBg;
976
- case "version":
977
- return colors.versionBg;
978
- case "env":
979
- return colors.envBg;
980
- case "weekly":
981
- return colors.weeklyBg;
982
- case "agent":
983
- return colors.agentBg;
984
- case "thinking":
985
- return colors.thinkingBg;
986
- case "cacheTimer":
987
- return colors.cacheTimerBg;
988
- case "bastra":
989
- return colors.bastraBg;
990
- default:
991
- return colors.modeBg;
992
- }
993
- }
994
-
995
- private getSegmentBoldFlag(
996
- segmentType: string,
997
- colors: PowerlineColors,
998
- ): boolean {
999
- switch (segmentType) {
1000
- case "directory":
1001
- return colors.modeBold;
1002
- case "git":
1003
- return colors.gitBold;
1004
- case "model":
1005
- return colors.modelBold;
1006
- case "session":
1007
- case "sessionId":
1008
- return colors.sessionBold;
1009
- case "block":
1010
- return colors.blockBold;
1011
- case "today":
1012
- return colors.todayBold;
1013
- case "tmux":
1014
- return colors.tmuxBold;
1015
- case "context":
1016
- return colors.contextBold;
1017
- case "metrics":
1018
- return colors.metricsBold;
1019
- case "version":
1020
- return colors.versionBold;
1021
- case "env":
1022
- return colors.envBold;
1023
- case "weekly":
1024
- return colors.weeklyBold;
1025
- case "agent":
1026
- return colors.agentBold;
1027
- case "thinking":
1028
- return colors.thinkingBold;
1029
- case "cacheTimer":
1030
- return colors.cacheTimerBold;
1031
- case "bastra":
1032
- return colors.bastraBold;
1033
- default:
1034
- return colors.modeBold;
1035
- }
1036
- }
1037
-
1038
- private formatSegment(
1039
- bgColor: string,
1040
- fgColor: string,
1041
- text: string,
1042
- nextBgColor: string | undefined,
1043
- colors: PowerlineColors,
1044
- bold: boolean,
1045
- ): string {
1046
- const isCapsuleStyle = this.config.display.style === "capsule";
1047
- const padding = " ".repeat(this.config.display.padding ?? 1);
1048
- const useBold = bold && colors.reset !== "";
1049
- const boldOn = useBold ? "\x1b[1m" : "";
1050
- const boldOff = useBold ? "\x1b[22m" : "";
1051
-
1052
- if (isCapsuleStyle) {
1053
- const colorMode = this.config.display.colorCompatibility || "auto";
1054
- const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode;
1055
- const isBasicMode = colorSupport === "ansi";
1056
-
1057
- const capFgColor = extractBgToFg(bgColor, isBasicMode);
1058
-
1059
- const leftCap = `${capFgColor}${this.symbols.left}${colors.reset}`;
1060
-
1061
- const content = `${bgColor}${fgColor}${boldOn}${padding}${text}${padding}${boldOff}${colors.reset}`;
1062
-
1063
- const rightCap = `${capFgColor}${this.symbols.right}${colors.reset}`;
1064
-
1065
- return `${leftCap}${content}${rightCap}`;
1066
- }
1067
-
1068
- let output = `${bgColor}${fgColor}${boldOn}${padding}${text}${padding}${boldOff}`;
1069
-
1070
- const colorMode = this.config.display.colorCompatibility || "auto";
1071
- const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode;
1072
- const isBasicMode = colorSupport === "ansi";
1073
-
1074
- if (nextBgColor) {
1075
- const arrowFgColor = extractBgToFg(bgColor, isBasicMode);
1076
- output += `${colors.reset}${nextBgColor}${arrowFgColor}${this.symbols.right}`;
1077
- } else {
1078
- output += `${colors.reset}${extractBgToFg(bgColor, isBasicMode)}${this.symbols.right}${colors.reset}`;
1079
- }
1080
-
1081
- return output;
299
+ return this.layout.buildLineFromSegments(renderedSegments, colors);
1082
300
  }
1083
301
  }