@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.
- package/dist/browser.d.ts +676 -0
- package/dist/browser.js +3 -0
- package/dist/index.mjs +10 -10
- package/package.json +9 -1
- package/plugin/templates/config-tui-compact.json +4 -4
- package/plugin/templates/config-tui-full.json +5 -5
- package/plugin/templates/config-tui-standard.json +5 -5
- package/src/browser.ts +203 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +462 -0
- package/src/index.ts +90 -0
- package/src/powerline.ts +904 -0
- package/src/segments/block.ts +31 -0
- package/src/segments/context.ts +221 -0
- package/src/segments/git.ts +492 -0
- package/src/segments/index.ts +25 -0
- package/src/segments/metrics.ts +175 -0
- package/src/segments/pricing.ts +454 -0
- package/src/segments/renderer.ts +796 -0
- package/src/segments/session.ts +207 -0
- package/src/segments/tmux.ts +35 -0
- package/src/segments/today.ts +191 -0
- package/src/themes/dark.ts +52 -0
- package/src/themes/gruvbox.ts +52 -0
- package/src/themes/index.ts +131 -0
- package/src/themes/light.ts +52 -0
- package/src/themes/nord.ts +52 -0
- package/src/themes/rose-pine.ts +52 -0
- package/src/themes/tokyo-night.ts +52 -0
- package/src/tui/grid.ts +712 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/layouts.ts +285 -0
- package/src/tui/primitives.ts +175 -0
- package/src/tui/renderer.ts +206 -0
- package/src/tui/sections.ts +1080 -0
- package/src/tui/types.ts +181 -0
- package/src/utils/budget.ts +47 -0
- package/src/utils/cache.ts +247 -0
- package/src/utils/claude.ts +489 -0
- package/src/utils/color-support.ts +118 -0
- package/src/utils/colors.ts +120 -0
- package/src/utils/constants.ts +176 -0
- package/src/utils/formatters.ts +160 -0
- package/src/utils/logger.ts +5 -0
- package/src/utils/terminal-width.ts +117 -0
- package/src/utils/terminal.ts +11 -0
package/src/powerline.ts
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import type { ClaudeHookData } from "./utils/claude";
|
|
2
|
+
import type { PowerlineColors, ColorTheme } from "./themes";
|
|
3
|
+
import type { PowerlineConfig, LineConfig } from "./config/loader";
|
|
4
|
+
import type {
|
|
5
|
+
UsageInfo,
|
|
6
|
+
ContextInfo,
|
|
7
|
+
MetricsInfo,
|
|
8
|
+
PowerlineSymbols,
|
|
9
|
+
AnySegmentConfig,
|
|
10
|
+
DirectorySegmentConfig,
|
|
11
|
+
GitSegmentConfig,
|
|
12
|
+
UsageSegmentConfig,
|
|
13
|
+
ContextSegmentConfig,
|
|
14
|
+
MetricsSegmentConfig,
|
|
15
|
+
BlockSegmentConfig,
|
|
16
|
+
TodaySegmentConfig,
|
|
17
|
+
VersionSegmentConfig,
|
|
18
|
+
SessionIdSegmentConfig,
|
|
19
|
+
EnvSegmentConfig,
|
|
20
|
+
WeeklySegmentConfig,
|
|
21
|
+
} from "./segments";
|
|
22
|
+
import type { BlockInfo } from "./segments/block";
|
|
23
|
+
import type { TodayInfo } from "./segments/today";
|
|
24
|
+
import type { TuiData } from "./tui";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
hexToAnsi,
|
|
28
|
+
extractBgToFg,
|
|
29
|
+
hexToBasicAnsi,
|
|
30
|
+
hexTo256Ansi,
|
|
31
|
+
hexColorDistance,
|
|
32
|
+
} from "./utils/colors";
|
|
33
|
+
import { getColorSupport } from "./utils/color-support";
|
|
34
|
+
import { getTheme } from "./themes";
|
|
35
|
+
import {
|
|
36
|
+
UsageProvider,
|
|
37
|
+
ContextProvider,
|
|
38
|
+
GitService,
|
|
39
|
+
TmuxService,
|
|
40
|
+
MetricsProvider,
|
|
41
|
+
SegmentRenderer,
|
|
42
|
+
} from "./segments";
|
|
43
|
+
import { BlockProvider } from "./segments/block";
|
|
44
|
+
import { TodayProvider } from "./segments/today";
|
|
45
|
+
import {
|
|
46
|
+
SYMBOLS,
|
|
47
|
+
TEXT_SYMBOLS,
|
|
48
|
+
RESET_CODE,
|
|
49
|
+
BOX_CHARS,
|
|
50
|
+
BOX_CHARS_TEXT,
|
|
51
|
+
} from "./utils/constants";
|
|
52
|
+
import { visibleLength } from "./utils/terminal";
|
|
53
|
+
import { getTerminalWidth, getRawTerminalWidth } from "./utils/terminal-width";
|
|
54
|
+
import { renderTuiPanel } from "./tui";
|
|
55
|
+
|
|
56
|
+
interface RenderedSegment {
|
|
57
|
+
type: string;
|
|
58
|
+
text: string;
|
|
59
|
+
bgColor: string;
|
|
60
|
+
fgColor: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class PowerlineRenderer {
|
|
64
|
+
private readonly symbols: PowerlineSymbols;
|
|
65
|
+
private _usageProvider?: UsageProvider;
|
|
66
|
+
private _blockProvider?: BlockProvider;
|
|
67
|
+
private _todayProvider?: TodayProvider;
|
|
68
|
+
private _contextProvider?: ContextProvider;
|
|
69
|
+
private _gitService?: GitService;
|
|
70
|
+
private _tmuxService?: TmuxService;
|
|
71
|
+
private _metricsProvider?: MetricsProvider;
|
|
72
|
+
private _segmentRenderer?: SegmentRenderer;
|
|
73
|
+
|
|
74
|
+
constructor(private readonly config: PowerlineConfig) {
|
|
75
|
+
this.symbols = this.initializeSymbols();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private get usageProvider(): UsageProvider {
|
|
79
|
+
if (!this._usageProvider) {
|
|
80
|
+
this._usageProvider = new UsageProvider();
|
|
81
|
+
}
|
|
82
|
+
return this._usageProvider;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private get blockProvider(): BlockProvider {
|
|
86
|
+
if (!this._blockProvider) {
|
|
87
|
+
this._blockProvider = new BlockProvider();
|
|
88
|
+
}
|
|
89
|
+
return this._blockProvider;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private get todayProvider(): TodayProvider {
|
|
93
|
+
if (!this._todayProvider) {
|
|
94
|
+
this._todayProvider = new TodayProvider();
|
|
95
|
+
}
|
|
96
|
+
return this._todayProvider;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private get contextProvider(): ContextProvider {
|
|
100
|
+
if (!this._contextProvider) {
|
|
101
|
+
this._contextProvider = new ContextProvider(this.config);
|
|
102
|
+
}
|
|
103
|
+
return this._contextProvider;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private get gitService(): GitService {
|
|
107
|
+
if (!this._gitService) {
|
|
108
|
+
this._gitService = new GitService();
|
|
109
|
+
}
|
|
110
|
+
return this._gitService;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private get tmuxService(): TmuxService {
|
|
114
|
+
if (!this._tmuxService) {
|
|
115
|
+
this._tmuxService = new TmuxService();
|
|
116
|
+
}
|
|
117
|
+
return this._tmuxService;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private get metricsProvider(): MetricsProvider {
|
|
121
|
+
if (!this._metricsProvider) {
|
|
122
|
+
this._metricsProvider = new MetricsProvider();
|
|
123
|
+
}
|
|
124
|
+
return this._metricsProvider;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private get segmentRenderer(): SegmentRenderer {
|
|
128
|
+
if (!this._segmentRenderer) {
|
|
129
|
+
this._segmentRenderer = new SegmentRenderer(this.config, this.symbols);
|
|
130
|
+
}
|
|
131
|
+
return this._segmentRenderer;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private needsSegmentInfo(segmentType: keyof LineConfig["segments"]): boolean {
|
|
135
|
+
return this.config.display.lines.some(
|
|
136
|
+
(line) => line.segments[segmentType]?.enabled,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async generateStatusline(hookData: ClaudeHookData): Promise<string> {
|
|
141
|
+
if (this.config.display.style === "tui") {
|
|
142
|
+
return this.generateTuiStatusline(hookData);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const usageInfo = this.needsSegmentInfo("session")
|
|
146
|
+
? await this.usageProvider.getUsageInfo(hookData.session_id, hookData)
|
|
147
|
+
: null;
|
|
148
|
+
|
|
149
|
+
const blockInfo = this.needsSegmentInfo("block")
|
|
150
|
+
? await this.blockProvider.getActiveBlockInfo(hookData)
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
const todayInfo = this.needsSegmentInfo("today")
|
|
154
|
+
? await this.todayProvider.getTodayInfo()
|
|
155
|
+
: null;
|
|
156
|
+
|
|
157
|
+
const contextSegmentConfig = this.config.display.lines
|
|
158
|
+
.map((line) => line.segments.context)
|
|
159
|
+
.find((c) => c?.enabled) as ContextSegmentConfig | undefined;
|
|
160
|
+
const autocompactBuffer = contextSegmentConfig?.autocompactBuffer ?? 33000;
|
|
161
|
+
const contextInfo = this.needsSegmentInfo("context")
|
|
162
|
+
? await this.contextProvider.getContextInfo(hookData, autocompactBuffer)
|
|
163
|
+
: null;
|
|
164
|
+
|
|
165
|
+
const metricsInfo = this.needsSegmentInfo("metrics")
|
|
166
|
+
? await this.metricsProvider.getMetricsInfo(hookData.session_id, hookData)
|
|
167
|
+
: null;
|
|
168
|
+
|
|
169
|
+
if (this.config.display.autoWrap) {
|
|
170
|
+
return this.generateAutoWrapStatusline(
|
|
171
|
+
hookData,
|
|
172
|
+
usageInfo,
|
|
173
|
+
blockInfo,
|
|
174
|
+
todayInfo,
|
|
175
|
+
contextInfo,
|
|
176
|
+
metricsInfo,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const lines = await Promise.all(
|
|
181
|
+
this.config.display.lines.map((lineConfig) =>
|
|
182
|
+
this.renderLine(
|
|
183
|
+
lineConfig,
|
|
184
|
+
hookData,
|
|
185
|
+
usageInfo,
|
|
186
|
+
blockInfo,
|
|
187
|
+
todayInfo,
|
|
188
|
+
contextInfo,
|
|
189
|
+
metricsInfo,
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return lines.filter((line) => line.length > 0).join("\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async generateAutoWrapStatusline(
|
|
198
|
+
hookData: ClaudeHookData,
|
|
199
|
+
usageInfo: UsageInfo | null,
|
|
200
|
+
blockInfo: BlockInfo | null,
|
|
201
|
+
todayInfo: TodayInfo | null,
|
|
202
|
+
contextInfo: ContextInfo | null,
|
|
203
|
+
metricsInfo: MetricsInfo | null,
|
|
204
|
+
): Promise<string> {
|
|
205
|
+
const colors = this.getThemeColors();
|
|
206
|
+
const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
|
|
207
|
+
const terminalWidth = getTerminalWidth();
|
|
208
|
+
|
|
209
|
+
const outputLines: string[] = [];
|
|
210
|
+
|
|
211
|
+
for (const lineConfig of this.config.display.lines) {
|
|
212
|
+
const segments = Object.entries(lineConfig.segments)
|
|
213
|
+
.filter(
|
|
214
|
+
([_, config]: [string, AnySegmentConfig | undefined]) =>
|
|
215
|
+
config?.enabled,
|
|
216
|
+
)
|
|
217
|
+
.map(([type, config]: [string, AnySegmentConfig]) => ({
|
|
218
|
+
type,
|
|
219
|
+
config,
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
const renderedSegments: RenderedSegment[] = [];
|
|
223
|
+
for (const segment of segments) {
|
|
224
|
+
const segmentData = await this.renderSegment(
|
|
225
|
+
segment,
|
|
226
|
+
hookData,
|
|
227
|
+
usageInfo,
|
|
228
|
+
blockInfo,
|
|
229
|
+
todayInfo,
|
|
230
|
+
contextInfo,
|
|
231
|
+
metricsInfo,
|
|
232
|
+
colors,
|
|
233
|
+
currentDir,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (segmentData) {
|
|
237
|
+
renderedSegments.push({
|
|
238
|
+
type: segment.type,
|
|
239
|
+
text: segmentData.text,
|
|
240
|
+
bgColor: segmentData.bgColor,
|
|
241
|
+
fgColor: segmentData.fgColor,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (renderedSegments.length === 0) continue;
|
|
247
|
+
|
|
248
|
+
if (!terminalWidth || terminalWidth <= 0) {
|
|
249
|
+
outputLines.push(this.buildLineFromSegments(renderedSegments, colors));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let currentLineSegments: RenderedSegment[] = [];
|
|
254
|
+
let currentLineWidth = 0;
|
|
255
|
+
|
|
256
|
+
for (const segment of renderedSegments) {
|
|
257
|
+
const segmentWidth = this.calculateSegmentWidth(
|
|
258
|
+
segment,
|
|
259
|
+
currentLineSegments.length === 0,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
currentLineSegments.length > 0 &&
|
|
264
|
+
currentLineWidth + segmentWidth > terminalWidth
|
|
265
|
+
) {
|
|
266
|
+
outputLines.push(
|
|
267
|
+
this.buildLineFromSegments(currentLineSegments, colors),
|
|
268
|
+
);
|
|
269
|
+
currentLineSegments = [];
|
|
270
|
+
currentLineWidth = 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
currentLineSegments.push(segment);
|
|
274
|
+
currentLineWidth += segmentWidth;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (currentLineSegments.length > 0) {
|
|
278
|
+
outputLines.push(
|
|
279
|
+
this.buildLineFromSegments(currentLineSegments, colors),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return outputLines.join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async generateTuiStatusline(
|
|
288
|
+
hookData: ClaudeHookData,
|
|
289
|
+
): Promise<string> {
|
|
290
|
+
const colors = this.getThemeColors();
|
|
291
|
+
const terminalWidth = getTerminalWidth();
|
|
292
|
+
const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
|
|
293
|
+
const charset = this.config.display.charset || "unicode";
|
|
294
|
+
const boxChars = charset === "text" ? BOX_CHARS_TEXT : BOX_CHARS;
|
|
295
|
+
const contextSegmentConfig = this.config.display.lines
|
|
296
|
+
.map((line) => line.segments.context)
|
|
297
|
+
.find((c) => c?.enabled) as ContextSegmentConfig | undefined;
|
|
298
|
+
const autocompactBuffer = contextSegmentConfig?.autocompactBuffer ?? 33000;
|
|
299
|
+
|
|
300
|
+
const results = await Promise.allSettled([
|
|
301
|
+
this.usageProvider.getUsageInfo(hookData.session_id, hookData),
|
|
302
|
+
this.blockProvider.getActiveBlockInfo(hookData),
|
|
303
|
+
this.todayProvider.getTodayInfo(),
|
|
304
|
+
this.contextProvider.getContextInfo(hookData, autocompactBuffer),
|
|
305
|
+
this.metricsProvider.getMetricsInfo(hookData.session_id, hookData),
|
|
306
|
+
this.gitService.getGitInfo(
|
|
307
|
+
currentDir,
|
|
308
|
+
{
|
|
309
|
+
showSha: false,
|
|
310
|
+
showWorkingTree: true,
|
|
311
|
+
showOperation: false,
|
|
312
|
+
showTag: false,
|
|
313
|
+
showTimeSinceCommit: false,
|
|
314
|
+
showStashCount: false,
|
|
315
|
+
showUpstream: false,
|
|
316
|
+
showRepoName: false,
|
|
317
|
+
},
|
|
318
|
+
hookData.workspace?.project_dir,
|
|
319
|
+
),
|
|
320
|
+
this.tmuxService.getSessionId(),
|
|
321
|
+
]);
|
|
322
|
+
const val = <T>(r: PromiseSettledResult<T>) =>
|
|
323
|
+
r.status === "fulfilled" ? r.value : null;
|
|
324
|
+
const [
|
|
325
|
+
usageInfo,
|
|
326
|
+
blockInfo,
|
|
327
|
+
todayInfo,
|
|
328
|
+
contextInfo,
|
|
329
|
+
metricsInfo,
|
|
330
|
+
gitInfo,
|
|
331
|
+
tmuxSessionId,
|
|
332
|
+
] = [
|
|
333
|
+
val(results[0]!),
|
|
334
|
+
val(results[1]!),
|
|
335
|
+
val(results[2]!),
|
|
336
|
+
val(results[3]!),
|
|
337
|
+
val(results[4]!),
|
|
338
|
+
val(results[5]!),
|
|
339
|
+
val(results[6]!),
|
|
340
|
+
] as const;
|
|
341
|
+
|
|
342
|
+
const tuiData: TuiData = {
|
|
343
|
+
hookData,
|
|
344
|
+
usageInfo,
|
|
345
|
+
blockInfo,
|
|
346
|
+
todayInfo,
|
|
347
|
+
contextInfo,
|
|
348
|
+
metricsInfo,
|
|
349
|
+
gitInfo,
|
|
350
|
+
tmuxSessionId,
|
|
351
|
+
colors,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return renderTuiPanel(
|
|
355
|
+
tuiData,
|
|
356
|
+
boxChars,
|
|
357
|
+
colors.reset,
|
|
358
|
+
terminalWidth,
|
|
359
|
+
this.config,
|
|
360
|
+
{ rawTerminalWidth: getRawTerminalWidth() },
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private calculateSegmentWidth(
|
|
365
|
+
segment: RenderedSegment,
|
|
366
|
+
isFirst: boolean,
|
|
367
|
+
): number {
|
|
368
|
+
const isCapsuleStyle = this.config.display.style === "capsule";
|
|
369
|
+
const textWidth = visibleLength(segment.text);
|
|
370
|
+
const padding = this.config.display.padding ?? 1;
|
|
371
|
+
const paddingWidth = padding * 2;
|
|
372
|
+
|
|
373
|
+
if (isCapsuleStyle) {
|
|
374
|
+
const capsuleOverhead = 2 + paddingWidth + (isFirst ? 0 : 1);
|
|
375
|
+
return textWidth + capsuleOverhead;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const powerlineOverhead = 1 + paddingWidth;
|
|
379
|
+
return textWidth + powerlineOverhead;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private buildLineFromSegments(
|
|
383
|
+
segments: RenderedSegment[],
|
|
384
|
+
colors: PowerlineColors,
|
|
385
|
+
): string {
|
|
386
|
+
const isCapsuleStyle = this.config.display.style === "capsule";
|
|
387
|
+
let line = colors.reset;
|
|
388
|
+
|
|
389
|
+
for (let i = 0; i < segments.length; i++) {
|
|
390
|
+
const segment = segments[i];
|
|
391
|
+
if (!segment) continue;
|
|
392
|
+
|
|
393
|
+
const isFirst = i === 0;
|
|
394
|
+
const isLast = i === segments.length - 1;
|
|
395
|
+
const nextSegment = !isLast ? segments[i + 1] : null;
|
|
396
|
+
|
|
397
|
+
if (isCapsuleStyle && !isFirst) {
|
|
398
|
+
line += " ";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
line += this.formatSegment(
|
|
402
|
+
segment.bgColor,
|
|
403
|
+
segment.fgColor,
|
|
404
|
+
segment.text,
|
|
405
|
+
nextSegment?.bgColor,
|
|
406
|
+
colors,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return line;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async renderLine(
|
|
414
|
+
lineConfig: LineConfig,
|
|
415
|
+
hookData: ClaudeHookData,
|
|
416
|
+
usageInfo: UsageInfo | null,
|
|
417
|
+
blockInfo: BlockInfo | null,
|
|
418
|
+
todayInfo: TodayInfo | null,
|
|
419
|
+
contextInfo: ContextInfo | null,
|
|
420
|
+
metricsInfo: MetricsInfo | null,
|
|
421
|
+
): Promise<string> {
|
|
422
|
+
const colors = this.getThemeColors();
|
|
423
|
+
const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
|
|
424
|
+
|
|
425
|
+
const segments = Object.entries(lineConfig.segments)
|
|
426
|
+
.filter(
|
|
427
|
+
([_, config]: [string, AnySegmentConfig | undefined]) =>
|
|
428
|
+
config?.enabled,
|
|
429
|
+
)
|
|
430
|
+
.map(([type, config]: [string, AnySegmentConfig]) => ({ type, config }));
|
|
431
|
+
|
|
432
|
+
const renderedSegments: RenderedSegment[] = [];
|
|
433
|
+
for (const segment of segments) {
|
|
434
|
+
const segmentData = await this.renderSegment(
|
|
435
|
+
segment,
|
|
436
|
+
hookData,
|
|
437
|
+
usageInfo,
|
|
438
|
+
blockInfo,
|
|
439
|
+
todayInfo,
|
|
440
|
+
contextInfo,
|
|
441
|
+
metricsInfo,
|
|
442
|
+
colors,
|
|
443
|
+
currentDir,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (segmentData) {
|
|
447
|
+
renderedSegments.push({
|
|
448
|
+
type: segment.type,
|
|
449
|
+
text: segmentData.text,
|
|
450
|
+
bgColor: segmentData.bgColor,
|
|
451
|
+
fgColor: segmentData.fgColor,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return this.buildLineFromSegments(renderedSegments, colors);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private async renderSegment(
|
|
460
|
+
segment: { type: string; config: AnySegmentConfig },
|
|
461
|
+
hookData: ClaudeHookData,
|
|
462
|
+
usageInfo: UsageInfo | null,
|
|
463
|
+
blockInfo: BlockInfo | null,
|
|
464
|
+
todayInfo: TodayInfo | null,
|
|
465
|
+
contextInfo: ContextInfo | null,
|
|
466
|
+
metricsInfo: MetricsInfo | null,
|
|
467
|
+
colors: PowerlineColors,
|
|
468
|
+
currentDir: string,
|
|
469
|
+
) {
|
|
470
|
+
if (segment.type === "directory") {
|
|
471
|
+
return this.segmentRenderer.renderDirectory(
|
|
472
|
+
hookData,
|
|
473
|
+
colors,
|
|
474
|
+
segment.config as DirectorySegmentConfig,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
if (segment.type === "model") {
|
|
478
|
+
return this.segmentRenderer.renderModel(hookData, colors);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (segment.type === "git") {
|
|
482
|
+
return await this.renderGitSegment(
|
|
483
|
+
segment.config as GitSegmentConfig,
|
|
484
|
+
hookData,
|
|
485
|
+
colors,
|
|
486
|
+
currentDir,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (segment.type === "session") {
|
|
491
|
+
return this.renderSessionSegment(
|
|
492
|
+
segment.config as UsageSegmentConfig,
|
|
493
|
+
usageInfo,
|
|
494
|
+
colors,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (segment.type === "sessionId") {
|
|
499
|
+
return hookData.session_id
|
|
500
|
+
? this.segmentRenderer.renderSessionId(
|
|
501
|
+
hookData.session_id,
|
|
502
|
+
colors,
|
|
503
|
+
segment.config as SessionIdSegmentConfig,
|
|
504
|
+
)
|
|
505
|
+
: null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (segment.type === "tmux") {
|
|
509
|
+
return await this.renderTmuxSegment(colors);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (segment.type === "context") {
|
|
513
|
+
return this.renderContextSegment(
|
|
514
|
+
segment.config as ContextSegmentConfig,
|
|
515
|
+
contextInfo,
|
|
516
|
+
colors,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (segment.type === "metrics") {
|
|
521
|
+
return this.renderMetricsSegment(
|
|
522
|
+
segment.config as MetricsSegmentConfig,
|
|
523
|
+
metricsInfo,
|
|
524
|
+
blockInfo,
|
|
525
|
+
colors,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (segment.type === "block") {
|
|
530
|
+
return this.renderBlockSegment(
|
|
531
|
+
segment.config as BlockSegmentConfig,
|
|
532
|
+
blockInfo,
|
|
533
|
+
colors,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (segment.type === "today") {
|
|
538
|
+
return this.renderTodaySegment(
|
|
539
|
+
segment.config as TodaySegmentConfig,
|
|
540
|
+
todayInfo,
|
|
541
|
+
colors,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (segment.type === "version") {
|
|
546
|
+
return this.renderVersionSegment(
|
|
547
|
+
segment.config as VersionSegmentConfig,
|
|
548
|
+
hookData,
|
|
549
|
+
colors,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (segment.type === "env") {
|
|
554
|
+
return this.segmentRenderer.renderEnv(
|
|
555
|
+
colors,
|
|
556
|
+
segment.config as EnvSegmentConfig,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (segment.type === "weekly") {
|
|
561
|
+
return this.segmentRenderer.renderWeekly(
|
|
562
|
+
hookData,
|
|
563
|
+
colors,
|
|
564
|
+
segment.config as WeeklySegmentConfig,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private async renderGitSegment(
|
|
572
|
+
config: GitSegmentConfig,
|
|
573
|
+
hookData: ClaudeHookData,
|
|
574
|
+
colors: PowerlineColors,
|
|
575
|
+
currentDir: string,
|
|
576
|
+
) {
|
|
577
|
+
if (!this.needsSegmentInfo("git")) return null;
|
|
578
|
+
|
|
579
|
+
const gitInfo = await this.gitService.getGitInfo(
|
|
580
|
+
currentDir,
|
|
581
|
+
{
|
|
582
|
+
showSha: config?.showSha,
|
|
583
|
+
showWorkingTree: config?.showWorkingTree,
|
|
584
|
+
showOperation: config?.showOperation,
|
|
585
|
+
showTag: config?.showTag,
|
|
586
|
+
showTimeSinceCommit: config?.showTimeSinceCommit,
|
|
587
|
+
showStashCount: config?.showStashCount,
|
|
588
|
+
showUpstream: config?.showUpstream,
|
|
589
|
+
showRepoName: config?.showRepoName,
|
|
590
|
+
},
|
|
591
|
+
hookData.workspace?.project_dir,
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
return gitInfo
|
|
595
|
+
? this.segmentRenderer.renderGit(gitInfo, colors, config)
|
|
596
|
+
: null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private renderSessionSegment(
|
|
600
|
+
config: UsageSegmentConfig,
|
|
601
|
+
usageInfo: UsageInfo | null,
|
|
602
|
+
colors: PowerlineColors,
|
|
603
|
+
) {
|
|
604
|
+
if (!usageInfo) return null;
|
|
605
|
+
return this.segmentRenderer.renderSession(usageInfo, colors, config);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private async renderTmuxSegment(colors: PowerlineColors) {
|
|
609
|
+
if (!this.needsSegmentInfo("tmux")) return null;
|
|
610
|
+
const tmuxSessionId = await this.tmuxService.getSessionId();
|
|
611
|
+
return this.segmentRenderer.renderTmux(tmuxSessionId, colors);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private renderContextSegment(
|
|
615
|
+
config: ContextSegmentConfig,
|
|
616
|
+
contextInfo: ContextInfo | null,
|
|
617
|
+
colors: PowerlineColors,
|
|
618
|
+
) {
|
|
619
|
+
if (!this.needsSegmentInfo("context")) return null;
|
|
620
|
+
return this.segmentRenderer.renderContext(contextInfo, colors, config);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private renderMetricsSegment(
|
|
624
|
+
config: MetricsSegmentConfig,
|
|
625
|
+
metricsInfo: MetricsInfo | null,
|
|
626
|
+
_blockInfo: BlockInfo | null,
|
|
627
|
+
colors: PowerlineColors,
|
|
628
|
+
) {
|
|
629
|
+
return this.segmentRenderer.renderMetrics(metricsInfo, colors, config);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private renderBlockSegment(
|
|
633
|
+
config: BlockSegmentConfig,
|
|
634
|
+
blockInfo: BlockInfo | null,
|
|
635
|
+
colors: PowerlineColors,
|
|
636
|
+
) {
|
|
637
|
+
if (!blockInfo) return null;
|
|
638
|
+
return this.segmentRenderer.renderBlock(blockInfo, colors, config);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private renderTodaySegment(
|
|
642
|
+
config: TodaySegmentConfig,
|
|
643
|
+
todayInfo: TodayInfo | null,
|
|
644
|
+
colors: PowerlineColors,
|
|
645
|
+
) {
|
|
646
|
+
if (!todayInfo) return null;
|
|
647
|
+
const todayType = config?.type || "cost";
|
|
648
|
+
return this.segmentRenderer.renderToday(todayInfo, colors, todayType);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private renderVersionSegment(
|
|
652
|
+
config: VersionSegmentConfig,
|
|
653
|
+
hookData: ClaudeHookData,
|
|
654
|
+
colors: PowerlineColors,
|
|
655
|
+
) {
|
|
656
|
+
return this.segmentRenderer.renderVersion(hookData, colors, config);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private initializeSymbols(): PowerlineSymbols {
|
|
660
|
+
const style = this.config.display.style;
|
|
661
|
+
const charset = this.config.display.charset || "unicode";
|
|
662
|
+
const isMinimalStyle = style === "minimal";
|
|
663
|
+
const isCapsuleStyle = style === "capsule";
|
|
664
|
+
const symbolSet = charset === "text" ? TEXT_SYMBOLS : SYMBOLS;
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
right: isMinimalStyle
|
|
668
|
+
? ""
|
|
669
|
+
: isCapsuleStyle
|
|
670
|
+
? symbolSet.right_rounded
|
|
671
|
+
: symbolSet.right,
|
|
672
|
+
left: isCapsuleStyle ? symbolSet.left_rounded : "",
|
|
673
|
+
branch: symbolSet.branch,
|
|
674
|
+
model: symbolSet.model,
|
|
675
|
+
git_clean: symbolSet.git_clean,
|
|
676
|
+
git_dirty: symbolSet.git_dirty,
|
|
677
|
+
git_conflicts: symbolSet.git_conflicts,
|
|
678
|
+
git_ahead: symbolSet.git_ahead,
|
|
679
|
+
git_behind: symbolSet.git_behind,
|
|
680
|
+
git_worktree: symbolSet.git_worktree,
|
|
681
|
+
git_tag: symbolSet.git_tag,
|
|
682
|
+
git_sha: symbolSet.git_sha,
|
|
683
|
+
git_upstream: symbolSet.git_upstream,
|
|
684
|
+
git_stash: symbolSet.git_stash,
|
|
685
|
+
git_time: symbolSet.git_time,
|
|
686
|
+
session_cost: symbolSet.session_cost,
|
|
687
|
+
block_cost: symbolSet.block_cost,
|
|
688
|
+
today_cost: symbolSet.today_cost,
|
|
689
|
+
context_time: symbolSet.context_time,
|
|
690
|
+
metrics_response: symbolSet.metrics_response,
|
|
691
|
+
metrics_last_response: symbolSet.metrics_last_response,
|
|
692
|
+
metrics_duration: symbolSet.metrics_duration,
|
|
693
|
+
metrics_messages: symbolSet.metrics_messages,
|
|
694
|
+
metrics_lines_added: symbolSet.metrics_lines_added,
|
|
695
|
+
metrics_lines_removed: symbolSet.metrics_lines_removed,
|
|
696
|
+
metrics_burn: symbolSet.metrics_burn,
|
|
697
|
+
version: symbolSet.version,
|
|
698
|
+
bar_filled: symbolSet.bar_filled,
|
|
699
|
+
bar_empty: symbolSet.bar_empty,
|
|
700
|
+
env: symbolSet.env,
|
|
701
|
+
session_id: symbolSet.session_id,
|
|
702
|
+
weekly_cost: symbolSet.weekly_cost,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private getThemeColors(): PowerlineColors {
|
|
707
|
+
const theme = this.config.theme;
|
|
708
|
+
let colorTheme;
|
|
709
|
+
|
|
710
|
+
const colorMode = this.config.display.colorCompatibility || "auto";
|
|
711
|
+
const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode;
|
|
712
|
+
|
|
713
|
+
if (theme === "custom") {
|
|
714
|
+
colorTheme = this.config.colors?.custom;
|
|
715
|
+
if (!colorTheme) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
"Custom theme selected but no colors provided in configuration",
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
colorTheme = getTheme(theme, colorSupport);
|
|
722
|
+
if (!colorTheme) {
|
|
723
|
+
console.warn(
|
|
724
|
+
`Built-in theme '${theme}' not found, falling back to 'dark' theme`,
|
|
725
|
+
);
|
|
726
|
+
colorTheme = getTheme("dark", colorSupport)!;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const convertHex = (hex: string, isBg: boolean): string => {
|
|
731
|
+
if (colorSupport === "none") return "";
|
|
732
|
+
if (colorSupport === "ansi") return hexToBasicAnsi(hex, isBg);
|
|
733
|
+
if (colorSupport === "ansi256") return hexTo256Ansi(hex, isBg);
|
|
734
|
+
return hexToAnsi(hex, isBg);
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const fallbackTheme = getTheme("dark", colorSupport)!;
|
|
738
|
+
|
|
739
|
+
const isTui = this.config.display.style === "tui";
|
|
740
|
+
const isLightTheme = theme === "light";
|
|
741
|
+
const terminalRef = isLightTheme ? "#f0f0f0" : "#1e1e1e";
|
|
742
|
+
|
|
743
|
+
const getSegmentColors = (segment: Exclude<keyof ColorTheme, "tui">) => {
|
|
744
|
+
const fallback = fallbackTheme[segment];
|
|
745
|
+
const custom = colorTheme[segment];
|
|
746
|
+
const colors = {
|
|
747
|
+
fg: custom?.fg || fallback.fg,
|
|
748
|
+
bg: custom?.bg || fallback.bg,
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
let fgHex = colors.fg;
|
|
752
|
+
if (isTui && hexColorDistance(fgHex, terminalRef) < 60) {
|
|
753
|
+
fgHex = colors.bg;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
bg: convertHex(colors.bg, true),
|
|
758
|
+
fg: convertHex(fgHex, false),
|
|
759
|
+
};
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const directory = getSegmentColors("directory");
|
|
763
|
+
const git = getSegmentColors("git");
|
|
764
|
+
const model = getSegmentColors("model");
|
|
765
|
+
const session = getSegmentColors("session");
|
|
766
|
+
const block = getSegmentColors("block");
|
|
767
|
+
const today = getSegmentColors("today");
|
|
768
|
+
const tmux = getSegmentColors("tmux");
|
|
769
|
+
const context = getSegmentColors("context");
|
|
770
|
+
const contextWarning = getSegmentColors("contextWarning");
|
|
771
|
+
const contextCritical = getSegmentColors("contextCritical");
|
|
772
|
+
const metrics = getSegmentColors("metrics");
|
|
773
|
+
const version = getSegmentColors("version");
|
|
774
|
+
const env = getSegmentColors("env");
|
|
775
|
+
const weekly = getSegmentColors("weekly");
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
reset: colorSupport === "none" ? "" : RESET_CODE,
|
|
779
|
+
modeBg: directory.bg,
|
|
780
|
+
modeFg: directory.fg,
|
|
781
|
+
gitBg: git.bg,
|
|
782
|
+
gitFg: git.fg,
|
|
783
|
+
modelBg: model.bg,
|
|
784
|
+
modelFg: model.fg,
|
|
785
|
+
sessionBg: session.bg,
|
|
786
|
+
sessionFg: session.fg,
|
|
787
|
+
blockBg: block.bg,
|
|
788
|
+
blockFg: block.fg,
|
|
789
|
+
todayBg: today.bg,
|
|
790
|
+
todayFg: today.fg,
|
|
791
|
+
tmuxBg: tmux.bg,
|
|
792
|
+
tmuxFg: tmux.fg,
|
|
793
|
+
contextBg: context.bg,
|
|
794
|
+
contextFg: context.fg,
|
|
795
|
+
contextWarningBg: contextWarning.bg,
|
|
796
|
+
contextWarningFg: contextWarning.fg,
|
|
797
|
+
contextCriticalBg: contextCritical.bg,
|
|
798
|
+
contextCriticalFg: contextCritical.fg,
|
|
799
|
+
metricsBg: metrics.bg,
|
|
800
|
+
metricsFg: metrics.fg,
|
|
801
|
+
versionBg: version.bg,
|
|
802
|
+
versionFg: version.fg,
|
|
803
|
+
envBg: env.bg,
|
|
804
|
+
envFg: env.fg,
|
|
805
|
+
weeklyBg: weekly.bg,
|
|
806
|
+
weeklyFg: weekly.fg,
|
|
807
|
+
partFg: theme === "custom" ? this.resolvePartColors(convertHex) : {},
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private resolvePartColors(
|
|
812
|
+
convertHex: (hex: string, isBg: boolean) => string,
|
|
813
|
+
): Record<string, string> {
|
|
814
|
+
const custom = this.config.colors?.custom as
|
|
815
|
+
| Record<string, { fg?: string }>
|
|
816
|
+
| undefined;
|
|
817
|
+
if (!custom) return {};
|
|
818
|
+
|
|
819
|
+
const result: Record<string, string> = {};
|
|
820
|
+
for (const key of Object.keys(custom)) {
|
|
821
|
+
const entry = custom[key];
|
|
822
|
+
if (!entry?.fg) continue;
|
|
823
|
+
result[key] = convertHex(entry.fg, false);
|
|
824
|
+
}
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private getSegmentBgColor(
|
|
829
|
+
segmentType: string,
|
|
830
|
+
colors: PowerlineColors,
|
|
831
|
+
): string {
|
|
832
|
+
switch (segmentType) {
|
|
833
|
+
case "directory":
|
|
834
|
+
return colors.modeBg;
|
|
835
|
+
case "git":
|
|
836
|
+
return colors.gitBg;
|
|
837
|
+
case "model":
|
|
838
|
+
return colors.modelBg;
|
|
839
|
+
case "session":
|
|
840
|
+
case "sessionId":
|
|
841
|
+
return colors.sessionBg;
|
|
842
|
+
case "block":
|
|
843
|
+
return colors.blockBg;
|
|
844
|
+
case "today":
|
|
845
|
+
return colors.todayBg;
|
|
846
|
+
case "tmux":
|
|
847
|
+
return colors.tmuxBg;
|
|
848
|
+
case "context":
|
|
849
|
+
return colors.contextBg;
|
|
850
|
+
case "metrics":
|
|
851
|
+
return colors.metricsBg;
|
|
852
|
+
case "version":
|
|
853
|
+
return colors.versionBg;
|
|
854
|
+
case "env":
|
|
855
|
+
return colors.envBg;
|
|
856
|
+
case "weekly":
|
|
857
|
+
return colors.weeklyBg;
|
|
858
|
+
default:
|
|
859
|
+
return colors.modeBg;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private formatSegment(
|
|
864
|
+
bgColor: string,
|
|
865
|
+
fgColor: string,
|
|
866
|
+
text: string,
|
|
867
|
+
nextBgColor: string | undefined,
|
|
868
|
+
colors: PowerlineColors,
|
|
869
|
+
): string {
|
|
870
|
+
const isCapsuleStyle = this.config.display.style === "capsule";
|
|
871
|
+
const padding = " ".repeat(this.config.display.padding ?? 1);
|
|
872
|
+
|
|
873
|
+
if (isCapsuleStyle) {
|
|
874
|
+
const colorMode = this.config.display.colorCompatibility || "auto";
|
|
875
|
+
const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode;
|
|
876
|
+
const isBasicMode = colorSupport === "ansi";
|
|
877
|
+
|
|
878
|
+
const capFgColor = extractBgToFg(bgColor, isBasicMode);
|
|
879
|
+
|
|
880
|
+
const leftCap = `${capFgColor}${this.symbols.left}${colors.reset}`;
|
|
881
|
+
|
|
882
|
+
const content = `${bgColor}${fgColor}${padding}${text}${padding}${colors.reset}`;
|
|
883
|
+
|
|
884
|
+
const rightCap = `${capFgColor}${this.symbols.right}${colors.reset}`;
|
|
885
|
+
|
|
886
|
+
return `${leftCap}${content}${rightCap}`;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
let output = `${bgColor}${fgColor}${padding}${text}${padding}`;
|
|
890
|
+
|
|
891
|
+
const colorMode = this.config.display.colorCompatibility || "auto";
|
|
892
|
+
const colorSupport = colorMode === "auto" ? getColorSupport() : colorMode;
|
|
893
|
+
const isBasicMode = colorSupport === "ansi";
|
|
894
|
+
|
|
895
|
+
if (nextBgColor) {
|
|
896
|
+
const arrowFgColor = extractBgToFg(bgColor, isBasicMode);
|
|
897
|
+
output += `${colors.reset}${nextBgColor}${arrowFgColor}${this.symbols.right}`;
|
|
898
|
+
} else {
|
|
899
|
+
output += `${colors.reset}${extractBgToFg(bgColor, isBasicMode)}${this.symbols.right}${colors.reset}`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return output;
|
|
903
|
+
}
|
|
904
|
+
}
|