@owloops/claude-powerline 1.24.4 → 1.25.0
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 +3 -3
- package/plugin/templates/config-tui-full.json +4 -4
- package/plugin/templates/config-tui-standard.json +4 -4
- 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
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { DEFAULT_CONFIG } from "./defaults";
|
|
5
|
+
import type { ColorTheme } from "../themes";
|
|
6
|
+
import type { TuiGridConfig } from "../tui/types";
|
|
7
|
+
import { isValidSegmentRef } from "../tui/types";
|
|
8
|
+
import { BOX_PRESETS } from "../utils/constants";
|
|
9
|
+
import type {
|
|
10
|
+
SegmentConfig,
|
|
11
|
+
DirectorySegmentConfig,
|
|
12
|
+
GitSegmentConfig,
|
|
13
|
+
UsageSegmentConfig,
|
|
14
|
+
TmuxSegmentConfig,
|
|
15
|
+
ContextSegmentConfig,
|
|
16
|
+
MetricsSegmentConfig,
|
|
17
|
+
BlockSegmentConfig,
|
|
18
|
+
TodaySegmentConfig,
|
|
19
|
+
VersionSegmentConfig,
|
|
20
|
+
SessionIdSegmentConfig,
|
|
21
|
+
EnvSegmentConfig,
|
|
22
|
+
WeeklySegmentConfig,
|
|
23
|
+
} from "../segments/renderer";
|
|
24
|
+
|
|
25
|
+
export interface LineConfig {
|
|
26
|
+
segments: {
|
|
27
|
+
directory?: DirectorySegmentConfig;
|
|
28
|
+
git?: GitSegmentConfig;
|
|
29
|
+
model?: SegmentConfig;
|
|
30
|
+
session?: UsageSegmentConfig;
|
|
31
|
+
block?: BlockSegmentConfig;
|
|
32
|
+
today?: TodaySegmentConfig;
|
|
33
|
+
tmux?: TmuxSegmentConfig;
|
|
34
|
+
context?: ContextSegmentConfig;
|
|
35
|
+
metrics?: MetricsSegmentConfig;
|
|
36
|
+
version?: VersionSegmentConfig;
|
|
37
|
+
sessionId?: SessionIdSegmentConfig;
|
|
38
|
+
env?: EnvSegmentConfig;
|
|
39
|
+
weekly?: WeeklySegmentConfig;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DisplayConfig {
|
|
44
|
+
lines: LineConfig[];
|
|
45
|
+
style?: "minimal" | "powerline" | "capsule" | "tui";
|
|
46
|
+
charset?: "unicode" | "text";
|
|
47
|
+
colorCompatibility?: "auto" | "ansi" | "ansi256" | "truecolor";
|
|
48
|
+
autoWrap?: boolean;
|
|
49
|
+
padding?: number;
|
|
50
|
+
tui?: TuiGridConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BudgetItemConfig {
|
|
54
|
+
amount?: number;
|
|
55
|
+
warningThreshold?: number;
|
|
56
|
+
type?: "cost" | "tokens";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BudgetConfig {
|
|
60
|
+
session?: BudgetItemConfig;
|
|
61
|
+
today?: BudgetItemConfig;
|
|
62
|
+
block?: BudgetItemConfig;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PowerlineConfig {
|
|
66
|
+
theme:
|
|
67
|
+
| "light"
|
|
68
|
+
| "dark"
|
|
69
|
+
| "nord"
|
|
70
|
+
| "tokyo-night"
|
|
71
|
+
| "rose-pine"
|
|
72
|
+
| "gruvbox"
|
|
73
|
+
| "custom";
|
|
74
|
+
display: DisplayConfig;
|
|
75
|
+
colors?: {
|
|
76
|
+
custom: ColorTheme;
|
|
77
|
+
};
|
|
78
|
+
budget?: BudgetConfig;
|
|
79
|
+
modelContextLimits?: Record<string, number>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isValidTheme(theme: string): theme is PowerlineConfig["theme"] {
|
|
83
|
+
return [
|
|
84
|
+
"light",
|
|
85
|
+
"dark",
|
|
86
|
+
"nord",
|
|
87
|
+
"tokyo-night",
|
|
88
|
+
"rose-pine",
|
|
89
|
+
"gruvbox",
|
|
90
|
+
"custom",
|
|
91
|
+
].includes(theme);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isValidStyle(
|
|
95
|
+
style: string,
|
|
96
|
+
): style is "minimal" | "powerline" | "capsule" | "tui" {
|
|
97
|
+
return (
|
|
98
|
+
style === "minimal" ||
|
|
99
|
+
style === "powerline" ||
|
|
100
|
+
style === "capsule" ||
|
|
101
|
+
style === "tui"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isValidCharset(charset: string): charset is "unicode" | "text" {
|
|
106
|
+
return charset === "unicode" || charset === "text";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getArgValue(args: string[], argName: string): string | undefined {
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
const arg = args[i];
|
|
112
|
+
if (arg === argName && i + 1 < args.length) {
|
|
113
|
+
return args[i + 1];
|
|
114
|
+
}
|
|
115
|
+
if (arg?.startsWith(`${argName}=`)) {
|
|
116
|
+
return arg.split("=")[1];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
function deepMerge<T extends Record<string, any>>(
|
|
124
|
+
target: T,
|
|
125
|
+
source: Partial<T>,
|
|
126
|
+
): T {
|
|
127
|
+
const result = { ...target };
|
|
128
|
+
|
|
129
|
+
for (const key in source) {
|
|
130
|
+
const sourceValue = source[key];
|
|
131
|
+
if (sourceValue !== undefined) {
|
|
132
|
+
if (
|
|
133
|
+
typeof sourceValue === "object" &&
|
|
134
|
+
sourceValue !== null &&
|
|
135
|
+
!Array.isArray(sourceValue)
|
|
136
|
+
) {
|
|
137
|
+
const targetValue = result[key] || {};
|
|
138
|
+
result[key] = deepMerge(
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
targetValue as Record<string, any>,
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
sourceValue as Record<string, any>,
|
|
143
|
+
) as T[Extract<keyof T, string>];
|
|
144
|
+
} else {
|
|
145
|
+
result[key] = sourceValue as T[Extract<keyof T, string>];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findConfigFile(
|
|
154
|
+
customPath?: string,
|
|
155
|
+
projectDir?: string,
|
|
156
|
+
): string | null {
|
|
157
|
+
if (customPath) {
|
|
158
|
+
return fs.existsSync(customPath) ? customPath : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const locations = [
|
|
162
|
+
...(projectDir ? [path.join(projectDir, ".claude-powerline.json")] : []),
|
|
163
|
+
path.join(process.cwd(), ".claude-powerline.json"),
|
|
164
|
+
path.join(os.homedir(), ".claude", "claude-powerline.json"),
|
|
165
|
+
path.join(os.homedir(), ".config", "claude-powerline", "config.json"),
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
return locations.find(fs.existsSync) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function loadConfigFile(filePath: string): Partial<PowerlineConfig> {
|
|
172
|
+
try {
|
|
173
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
174
|
+
return JSON.parse(content);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Failed to load config file ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function loadEnvConfig(): Partial<PowerlineConfig> {
|
|
183
|
+
const config: Partial<PowerlineConfig> = {};
|
|
184
|
+
const display: Partial<DisplayConfig> = {};
|
|
185
|
+
|
|
186
|
+
const theme = process.env.CLAUDE_POWERLINE_THEME;
|
|
187
|
+
if (theme && isValidTheme(theme)) {
|
|
188
|
+
config.theme = theme;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const style = process.env.CLAUDE_POWERLINE_STYLE;
|
|
192
|
+
if (style) {
|
|
193
|
+
if (isValidStyle(style)) {
|
|
194
|
+
display.style = style;
|
|
195
|
+
} else {
|
|
196
|
+
console.warn(
|
|
197
|
+
`Invalid display style '${style}' from environment variable, falling back to 'minimal'`,
|
|
198
|
+
);
|
|
199
|
+
display.style = "minimal";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (Object.keys(display).length > 0) {
|
|
204
|
+
config.display = display as DisplayConfig;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return config;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getConfigPathFromEnv(): string | undefined {
|
|
211
|
+
return process.env.CLAUDE_POWERLINE_CONFIG;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseCLIOverrides(args: string[]): Partial<PowerlineConfig> {
|
|
215
|
+
const config: Partial<PowerlineConfig> = {};
|
|
216
|
+
const display: Partial<DisplayConfig> = {};
|
|
217
|
+
|
|
218
|
+
const theme = getArgValue(args, "--theme");
|
|
219
|
+
if (theme && isValidTheme(theme)) {
|
|
220
|
+
config.theme = theme;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const style = getArgValue(args, "--style");
|
|
224
|
+
if (style) {
|
|
225
|
+
if (isValidStyle(style)) {
|
|
226
|
+
display.style = style;
|
|
227
|
+
} else {
|
|
228
|
+
console.warn(
|
|
229
|
+
`Invalid display style '${style}' from CLI argument, falling back to 'minimal'`,
|
|
230
|
+
);
|
|
231
|
+
display.style = "minimal";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const charset = getArgValue(args, "--charset");
|
|
236
|
+
if (charset) {
|
|
237
|
+
if (isValidCharset(charset)) {
|
|
238
|
+
display.charset = charset;
|
|
239
|
+
} else {
|
|
240
|
+
console.warn(
|
|
241
|
+
`Invalid charset '${charset}' from CLI argument, falling back to 'unicode'`,
|
|
242
|
+
);
|
|
243
|
+
display.charset = "unicode";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (Object.keys(display).length > 0) {
|
|
248
|
+
config.display = display as DisplayConfig;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return config;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function validateGridConfig(tui: TuiGridConfig): string | null {
|
|
255
|
+
if (typeof tui.box === "string" && !BOX_PRESETS[tui.box]) {
|
|
256
|
+
const valid = Object.keys(BOX_PRESETS).join(", ");
|
|
257
|
+
return `unknown box preset "${tui.box}" (valid: ${valid})`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (
|
|
261
|
+
!tui.breakpoints ||
|
|
262
|
+
!Array.isArray(tui.breakpoints) ||
|
|
263
|
+
tui.breakpoints.length === 0
|
|
264
|
+
) {
|
|
265
|
+
return "grid config must have at least one breakpoint";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const seenMinWidths = new Set<number>();
|
|
269
|
+
for (let bpIdx = 0; bpIdx < tui.breakpoints.length; bpIdx++) {
|
|
270
|
+
const bp = tui.breakpoints[bpIdx]!;
|
|
271
|
+
const prefix = `breakpoint[${bpIdx}]`;
|
|
272
|
+
|
|
273
|
+
if (typeof bp.minWidth !== "number" || bp.minWidth < 0) {
|
|
274
|
+
return `${prefix}: minWidth must be a non-negative number`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (seenMinWidths.has(bp.minWidth)) {
|
|
278
|
+
return `${prefix}: duplicate minWidth ${bp.minWidth} (each breakpoint must have a unique minWidth)`;
|
|
279
|
+
}
|
|
280
|
+
seenMinWidths.add(bp.minWidth);
|
|
281
|
+
|
|
282
|
+
if (!bp.areas || !Array.isArray(bp.areas) || bp.areas.length === 0) {
|
|
283
|
+
return `${prefix}: areas must be a non-empty array of strings`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!bp.columns || !Array.isArray(bp.columns) || bp.columns.length === 0) {
|
|
287
|
+
return `${prefix}: columns must be a non-empty array`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const colCount = bp.columns.length;
|
|
291
|
+
|
|
292
|
+
// Validate column definitions
|
|
293
|
+
for (const col of bp.columns) {
|
|
294
|
+
if (typeof col !== "string") {
|
|
295
|
+
return `${prefix}: column definition must be a string`;
|
|
296
|
+
}
|
|
297
|
+
if (!/^(\d+fr|\d+|auto)$/.test(col)) {
|
|
298
|
+
return `${prefix}: invalid column definition "${col}" (use "auto", "Nfr", or a fixed integer)`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate align array
|
|
303
|
+
if (bp.align !== undefined) {
|
|
304
|
+
if (!Array.isArray(bp.align)) {
|
|
305
|
+
return `${prefix}: align must be an array`;
|
|
306
|
+
}
|
|
307
|
+
if (bp.align.length !== colCount) {
|
|
308
|
+
return `${prefix}: align length (${bp.align.length}) must match columns length (${colCount})`;
|
|
309
|
+
}
|
|
310
|
+
for (const a of bp.align) {
|
|
311
|
+
if (a !== "left" && a !== "center" && a !== "right") {
|
|
312
|
+
return `${prefix}: invalid align value "${a}"`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Validate areas rows
|
|
318
|
+
const seenSegments = new Set<string>();
|
|
319
|
+
for (let rowIdx = 0; rowIdx < bp.areas.length; rowIdx++) {
|
|
320
|
+
const row = bp.areas[rowIdx]!;
|
|
321
|
+
|
|
322
|
+
// Divider row
|
|
323
|
+
if (row.trim() === "---") continue;
|
|
324
|
+
|
|
325
|
+
const cells = row.trim().split(/\s+/);
|
|
326
|
+
if (cells.length !== colCount) {
|
|
327
|
+
return `${prefix}: row "${row}" has ${cells.length} cells but expected ${colCount} columns`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check segment names and contiguity
|
|
331
|
+
const templateNames = tui.segments
|
|
332
|
+
? new Set(Object.keys(tui.segments))
|
|
333
|
+
: new Set<string>();
|
|
334
|
+
let prevCell = "";
|
|
335
|
+
let spanName = "";
|
|
336
|
+
for (const cell of cells) {
|
|
337
|
+
if (cell !== ".") {
|
|
338
|
+
if (!isValidSegmentRef(cell) && !templateNames.has(cell)) {
|
|
339
|
+
return `${prefix}: unknown segment name "${cell}"`;
|
|
340
|
+
}
|
|
341
|
+
// Check for non-contiguous spans
|
|
342
|
+
if (cell === spanName) {
|
|
343
|
+
// still in the same span, ok
|
|
344
|
+
} else if (seenSegments.has(cell)) {
|
|
345
|
+
return `${prefix}: segment "${cell}" appears on multiple rows`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Track span contiguity
|
|
350
|
+
if (cell !== prevCell) {
|
|
351
|
+
if (spanName && prevCell !== spanName && prevCell !== ".") {
|
|
352
|
+
// finished a span
|
|
353
|
+
}
|
|
354
|
+
spanName = cell;
|
|
355
|
+
}
|
|
356
|
+
prevCell = cell;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Check for non-contiguous spans within this row
|
|
360
|
+
const seen = new Map<string, number>();
|
|
361
|
+
for (let i = 0; i < cells.length; i++) {
|
|
362
|
+
const cell = cells[i]!;
|
|
363
|
+
if (cell === "." || cell === "---") continue;
|
|
364
|
+
const lastIdx = seen.get(cell);
|
|
365
|
+
if (lastIdx !== undefined && lastIdx !== i - 1) {
|
|
366
|
+
return `${prefix}: segment "${cell}" has non-contiguous span in row "${row}"`;
|
|
367
|
+
}
|
|
368
|
+
seen.set(cell, i);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Record segments from this row
|
|
372
|
+
for (const cell of cells) {
|
|
373
|
+
if (cell !== "." && cell !== "---") {
|
|
374
|
+
seenSegments.add(cell);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (tui.segments) {
|
|
381
|
+
for (const [segRef, tmpl] of Object.entries(tui.segments)) {
|
|
382
|
+
if (!tmpl.items || !Array.isArray(tmpl.items)) {
|
|
383
|
+
return `segments["${segRef}"]: items must be an array`;
|
|
384
|
+
}
|
|
385
|
+
if (
|
|
386
|
+
tmpl.justify !== undefined &&
|
|
387
|
+
tmpl.justify !== "start" &&
|
|
388
|
+
tmpl.justify !== "between"
|
|
389
|
+
) {
|
|
390
|
+
return `segments["${segRef}"]: invalid justify value "${tmpl.justify}" (use "start" or "between")`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return null; // valid
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function loadConfig(
|
|
399
|
+
args: string[] = process.argv,
|
|
400
|
+
projectDir?: string,
|
|
401
|
+
): PowerlineConfig {
|
|
402
|
+
let config: PowerlineConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
403
|
+
|
|
404
|
+
const rawConfigPath = getArgValue(args, "--config") || getConfigPathFromEnv();
|
|
405
|
+
const configPath = rawConfigPath?.startsWith("~")
|
|
406
|
+
? rawConfigPath.replace("~", os.homedir())
|
|
407
|
+
: rawConfigPath;
|
|
408
|
+
|
|
409
|
+
const configFile = findConfigFile(configPath, projectDir);
|
|
410
|
+
if (configFile) {
|
|
411
|
+
try {
|
|
412
|
+
const fileConfig = loadConfigFile(configFile);
|
|
413
|
+
config = deepMerge(config, fileConfig);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
console.warn(
|
|
416
|
+
`Warning: ${err instanceof Error ? err.message : String(err)}`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (config.display?.style && !isValidStyle(config.display.style)) {
|
|
422
|
+
console.warn(
|
|
423
|
+
`Invalid display style '${config.display.style}' in config file, falling back to 'minimal'`,
|
|
424
|
+
);
|
|
425
|
+
config.display.style = "minimal";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (config.display?.charset && !isValidCharset(config.display.charset)) {
|
|
429
|
+
console.warn(
|
|
430
|
+
`Invalid charset '${config.display.charset}' in config file, falling back to 'unicode'`,
|
|
431
|
+
);
|
|
432
|
+
config.display.charset = "unicode";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (config.theme && !isValidTheme(config.theme)) {
|
|
436
|
+
console.warn(
|
|
437
|
+
`Invalid theme '${config.theme}' in config file, falling back to 'dark'`,
|
|
438
|
+
);
|
|
439
|
+
config.theme = "dark";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const envConfig = loadEnvConfig();
|
|
443
|
+
config = deepMerge(config, envConfig);
|
|
444
|
+
|
|
445
|
+
const cliOverrides = parseCLIOverrides(args);
|
|
446
|
+
config = deepMerge(config, cliOverrides);
|
|
447
|
+
|
|
448
|
+
// Validate grid config if present
|
|
449
|
+
if (config.display?.tui) {
|
|
450
|
+
const error = validateGridConfig(config.display.tui);
|
|
451
|
+
if (error) {
|
|
452
|
+
process.stderr.write(
|
|
453
|
+
`Warning: invalid grid config: ${error}. Falling back to hardcoded layout.\n`,
|
|
454
|
+
);
|
|
455
|
+
delete config.display.tui;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return config;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export const loadConfigFromCLI = loadConfig;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import type { ClaudeHookData } from "./utils/claude";
|
|
4
|
+
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { json } from "node:stream/consumers";
|
|
7
|
+
import { PowerlineRenderer } from "./powerline";
|
|
8
|
+
import { loadConfigFromCLI } from "./config/loader";
|
|
9
|
+
import { debug } from "./utils/logger";
|
|
10
|
+
|
|
11
|
+
function showHelpText(): void {
|
|
12
|
+
console.log(`
|
|
13
|
+
claude-powerline - Beautiful powerline statusline for Claude Code
|
|
14
|
+
|
|
15
|
+
Usage: claude-powerline [options]
|
|
16
|
+
|
|
17
|
+
Standalone Commands:
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
|
|
20
|
+
Debugging:
|
|
21
|
+
CLAUDE_POWERLINE_DEBUG=1 Enable debug logging for troubleshooting
|
|
22
|
+
|
|
23
|
+
Claude Code Options (for settings.json):
|
|
24
|
+
--theme=THEME Set theme: dark, light, nord, tokyo-night, rose-pine, custom
|
|
25
|
+
--style=STYLE Set separator style: minimal, powerline, capsule, tui
|
|
26
|
+
--charset=CHARSET Set character set: unicode (default), text
|
|
27
|
+
--config=PATH Use custom config file path
|
|
28
|
+
|
|
29
|
+
See example config at: https://github.com/Owloops/claude-powerline/blob/main/.claude-powerline.json
|
|
30
|
+
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main(): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
const showHelp =
|
|
37
|
+
process.argv.includes("--help") || process.argv.includes("-h");
|
|
38
|
+
|
|
39
|
+
if (showHelp) {
|
|
40
|
+
showHelpText();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.stdin.isTTY === true) {
|
|
45
|
+
console.error(`Error: This tool requires input from Claude Code
|
|
46
|
+
|
|
47
|
+
claude-powerline is designed to be used as a Claude Code statusLine command.
|
|
48
|
+
It reads hook data from stdin and outputs formatted statusline.
|
|
49
|
+
|
|
50
|
+
Add to ~/.claude/settings.json:
|
|
51
|
+
{
|
|
52
|
+
"statusLine": {
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "claude-powerline --style=powerline"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Run with --help for more options.
|
|
59
|
+
|
|
60
|
+
To test output manually:
|
|
61
|
+
echo '{"session_id":"test-session","workspace":{"project_dir":"/path/to/project"},"model":{"id":"claude-sonnet-4-5","display_name":"Claude"}}' | claude-powerline --style=powerline`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
debug(`Working directory: ${process.cwd()}`);
|
|
66
|
+
debug(`Process args:`, process.argv);
|
|
67
|
+
|
|
68
|
+
const hookData = (await json(process.stdin)) as ClaudeHookData;
|
|
69
|
+
debug(`Received hook data:`, JSON.stringify(hookData, null, 2));
|
|
70
|
+
|
|
71
|
+
if (!hookData) {
|
|
72
|
+
console.error("Error: No input data received from stdin");
|
|
73
|
+
showHelpText();
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const projectDir = hookData.workspace?.project_dir;
|
|
78
|
+
const config = loadConfigFromCLI(process.argv, projectDir);
|
|
79
|
+
const renderer = new PowerlineRenderer(config);
|
|
80
|
+
const statusline = await renderer.generateStatusline(hookData);
|
|
81
|
+
|
|
82
|
+
console.log(statusline);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
85
|
+
console.error("Error generating statusline:", errorMessage);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main();
|