pi-powerline-footer 0.2.2
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/CHANGELOG.md +66 -0
- package/README.md +75 -0
- package/colors.ts +126 -0
- package/git-status.ts +242 -0
- package/icons.ts +156 -0
- package/index.ts +412 -0
- package/install.mjs +30 -0
- package/package.json +27 -0
- package/presets.ts +80 -0
- package/segments.ts +440 -0
- package/separators.ts +57 -0
- package/types.ts +129 -0
package/segments.ts
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { hostname as osHostname } from "node:os";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types.js";
|
|
4
|
+
import { fgOnly, rainbow } from "./colors.js";
|
|
5
|
+
import { getIcons, SEP_DOT, getThinkingText } from "./icons.js";
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
// Helpers
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
function withIcon(icon: string, text: string): string {
|
|
12
|
+
return icon ? `${icon} ${text}` : text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatTokens(n: number): string {
|
|
16
|
+
if (n < 1000) return n.toString();
|
|
17
|
+
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
18
|
+
if (n < 1000000) return `${Math.round(n / 1000)}k`;
|
|
19
|
+
if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
20
|
+
return `${Math.round(n / 1000000)}M`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatDuration(ms: number): string {
|
|
24
|
+
const seconds = Math.floor(ms / 1000);
|
|
25
|
+
const minutes = Math.floor(seconds / 60);
|
|
26
|
+
const hours = Math.floor(minutes / 60);
|
|
27
|
+
|
|
28
|
+
if (hours > 0) return `${hours}h${minutes % 60}m`;
|
|
29
|
+
if (minutes > 0) return `${minutes}m${seconds % 60}s`;
|
|
30
|
+
return `${seconds}s`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// Segment Implementations
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
const piSegment: StatusLineSegment = {
|
|
38
|
+
id: "pi",
|
|
39
|
+
render(_ctx) {
|
|
40
|
+
const icons = getIcons();
|
|
41
|
+
if (!icons.pi) return { content: "", visible: false };
|
|
42
|
+
const content = `${icons.pi} `;
|
|
43
|
+
return { content: fgOnly("accent", content), visible: true };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const modelSegment: StatusLineSegment = {
|
|
48
|
+
id: "model",
|
|
49
|
+
render(ctx) {
|
|
50
|
+
const icons = getIcons();
|
|
51
|
+
const opts = ctx.options.model ?? {};
|
|
52
|
+
|
|
53
|
+
let modelName = ctx.model?.name || ctx.model?.id || "no-model";
|
|
54
|
+
// Strip "Claude " prefix for brevity
|
|
55
|
+
if (modelName.startsWith("Claude ")) {
|
|
56
|
+
modelName = modelName.slice(7);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let content = withIcon(icons.model, modelName);
|
|
60
|
+
|
|
61
|
+
// Add thinking level with dot separator
|
|
62
|
+
if (opts.showThinkingLevel !== false && ctx.model?.reasoning) {
|
|
63
|
+
const level = ctx.thinkingLevel || "off";
|
|
64
|
+
if (level !== "off") {
|
|
65
|
+
const thinkingText = getThinkingText(level);
|
|
66
|
+
if (thinkingText) {
|
|
67
|
+
content += `${SEP_DOT}${thinkingText}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { content: fgOnly("model", content), visible: true };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const pathSegment: StatusLineSegment = {
|
|
77
|
+
id: "path",
|
|
78
|
+
render(ctx) {
|
|
79
|
+
const icons = getIcons();
|
|
80
|
+
const opts = ctx.options.path ?? {};
|
|
81
|
+
const mode = opts.mode ?? "basename";
|
|
82
|
+
|
|
83
|
+
let pwd = process.cwd();
|
|
84
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
85
|
+
|
|
86
|
+
if (mode === "basename") {
|
|
87
|
+
// Just the last directory component (cross-platform)
|
|
88
|
+
pwd = basename(pwd) || pwd;
|
|
89
|
+
} else {
|
|
90
|
+
// Abbreviate home directory for abbreviated/full modes
|
|
91
|
+
if (home && pwd.startsWith(home)) {
|
|
92
|
+
pwd = `~${pwd.slice(home.length)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Strip /work/ prefix (common in containers)
|
|
96
|
+
if (pwd.startsWith("/work/")) {
|
|
97
|
+
pwd = pwd.slice(6);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Truncate if too long (only for abbreviated mode)
|
|
101
|
+
if (mode === "abbreviated") {
|
|
102
|
+
const maxLen = opts.maxLength ?? 40;
|
|
103
|
+
if (pwd.length > maxLen) {
|
|
104
|
+
pwd = `…${pwd.slice(-(maxLen - 1))}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const content = withIcon(icons.folder, pwd);
|
|
110
|
+
return { content: fgOnly("path", content), visible: true };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const gitSegment: StatusLineSegment = {
|
|
115
|
+
id: "git",
|
|
116
|
+
render(ctx) {
|
|
117
|
+
const icons = getIcons();
|
|
118
|
+
const opts = ctx.options.git ?? {};
|
|
119
|
+
const { branch, staged, unstaged, untracked } = ctx.git;
|
|
120
|
+
const gitStatus = (staged > 0 || unstaged > 0 || untracked > 0)
|
|
121
|
+
? { staged, unstaged, untracked }
|
|
122
|
+
: null;
|
|
123
|
+
|
|
124
|
+
if (!branch && !gitStatus) return { content: "", visible: false };
|
|
125
|
+
|
|
126
|
+
const isDirty = gitStatus && (gitStatus.staged > 0 || gitStatus.unstaged > 0 || gitStatus.untracked > 0);
|
|
127
|
+
const showBranch = opts.showBranch !== false;
|
|
128
|
+
|
|
129
|
+
// Build content
|
|
130
|
+
let content = "";
|
|
131
|
+
if (showBranch && branch) {
|
|
132
|
+
content = withIcon(icons.branch, branch);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add status indicators
|
|
136
|
+
if (gitStatus) {
|
|
137
|
+
const indicators: string[] = [];
|
|
138
|
+
if (opts.showUnstaged !== false && gitStatus.unstaged > 0) {
|
|
139
|
+
indicators.push(fgOnly("unstaged", `*${gitStatus.unstaged}`));
|
|
140
|
+
}
|
|
141
|
+
if (opts.showStaged !== false && gitStatus.staged > 0) {
|
|
142
|
+
indicators.push(fgOnly("staged", `+${gitStatus.staged}`));
|
|
143
|
+
}
|
|
144
|
+
if (opts.showUntracked !== false && gitStatus.untracked > 0) {
|
|
145
|
+
indicators.push(fgOnly("untracked", `?${gitStatus.untracked}`));
|
|
146
|
+
}
|
|
147
|
+
if (indicators.length > 0) {
|
|
148
|
+
const indicatorText = indicators.join(" ");
|
|
149
|
+
if (!content && showBranch === false) {
|
|
150
|
+
content = withIcon(icons.git, indicatorText);
|
|
151
|
+
} else {
|
|
152
|
+
content += content ? ` ${indicatorText}` : indicatorText;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!content) return { content: "", visible: false };
|
|
158
|
+
|
|
159
|
+
// Wrap entire content in branch color
|
|
160
|
+
const colorName = isDirty ? "gitDirty" : "gitClean";
|
|
161
|
+
return { content: fgOnly(colorName, content), visible: true };
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const thinkingSegment: StatusLineSegment = {
|
|
166
|
+
id: "thinking",
|
|
167
|
+
render(ctx) {
|
|
168
|
+
const level = ctx.thinkingLevel || "off";
|
|
169
|
+
|
|
170
|
+
// Text label for each level
|
|
171
|
+
const levelText: Record<string, string> = {
|
|
172
|
+
off: "off",
|
|
173
|
+
minimal: "min",
|
|
174
|
+
low: "low",
|
|
175
|
+
medium: "med",
|
|
176
|
+
high: "high",
|
|
177
|
+
xhigh: "xhigh",
|
|
178
|
+
};
|
|
179
|
+
const label = levelText[level] || level;
|
|
180
|
+
const content = `thinking:${label}`;
|
|
181
|
+
|
|
182
|
+
// Use rainbow effect for high/xhigh (like Claude Code ultrathink)
|
|
183
|
+
if (level === "high" || level === "xhigh") {
|
|
184
|
+
return { content: rainbow(content), visible: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Use dedicated thinking colors (gradient: gray → purple → blue → teal)
|
|
188
|
+
const colorMap: Record<string, "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium"> = {
|
|
189
|
+
off: "thinkingOff",
|
|
190
|
+
minimal: "thinkingMinimal",
|
|
191
|
+
low: "thinkingLow",
|
|
192
|
+
medium: "thinkingMedium",
|
|
193
|
+
};
|
|
194
|
+
const color = colorMap[level] || "thinkingOff";
|
|
195
|
+
|
|
196
|
+
return { content: fgOnly(color, content), visible: true };
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const subagentsSegment: StatusLineSegment = {
|
|
201
|
+
id: "subagents",
|
|
202
|
+
render(_ctx) {
|
|
203
|
+
// Note: pi-mono doesn't have subagent tracking built-in
|
|
204
|
+
// This would require extension state management
|
|
205
|
+
// For now, return not visible
|
|
206
|
+
return { content: "", visible: false };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const tokenInSegment: StatusLineSegment = {
|
|
211
|
+
id: "token_in",
|
|
212
|
+
render(ctx) {
|
|
213
|
+
const icons = getIcons();
|
|
214
|
+
const { input } = ctx.usageStats;
|
|
215
|
+
if (!input) return { content: "", visible: false };
|
|
216
|
+
|
|
217
|
+
const content = withIcon(icons.input, formatTokens(input));
|
|
218
|
+
return { content: fgOnly("spend", content), visible: true };
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const tokenOutSegment: StatusLineSegment = {
|
|
223
|
+
id: "token_out",
|
|
224
|
+
render(ctx) {
|
|
225
|
+
const icons = getIcons();
|
|
226
|
+
const { output } = ctx.usageStats;
|
|
227
|
+
if (!output) return { content: "", visible: false };
|
|
228
|
+
|
|
229
|
+
const content = withIcon(icons.output, formatTokens(output));
|
|
230
|
+
return { content: fgOnly("output", content), visible: true };
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const tokenTotalSegment: StatusLineSegment = {
|
|
235
|
+
id: "token_total",
|
|
236
|
+
render(ctx) {
|
|
237
|
+
const icons = getIcons();
|
|
238
|
+
const { input, output, cacheRead, cacheWrite } = ctx.usageStats;
|
|
239
|
+
const total = input + output + cacheRead + cacheWrite;
|
|
240
|
+
if (!total) return { content: "", visible: false };
|
|
241
|
+
|
|
242
|
+
const content = withIcon(icons.tokens, formatTokens(total));
|
|
243
|
+
return { content: fgOnly("spend", content), visible: true };
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const costSegment: StatusLineSegment = {
|
|
248
|
+
id: "cost",
|
|
249
|
+
render(ctx) {
|
|
250
|
+
const { cost } = ctx.usageStats;
|
|
251
|
+
const usingSubscription = ctx.usingSubscription;
|
|
252
|
+
|
|
253
|
+
if (!cost && !usingSubscription) {
|
|
254
|
+
return { content: "", visible: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const costDisplay = usingSubscription ? "(sub)" : `$${cost.toFixed(2)}`;
|
|
258
|
+
return { content: fgOnly("cost", costDisplay), visible: true };
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const contextPctSegment: StatusLineSegment = {
|
|
263
|
+
id: "context_pct",
|
|
264
|
+
render(ctx) {
|
|
265
|
+
const icons = getIcons();
|
|
266
|
+
const pct = ctx.contextPercent;
|
|
267
|
+
const window = ctx.contextWindow;
|
|
268
|
+
|
|
269
|
+
const autoIcon = ctx.autoCompactEnabled && icons.auto ? ` ${icons.auto}` : "";
|
|
270
|
+
const text = `${pct.toFixed(1)}%/${formatTokens(window)}${autoIcon}`;
|
|
271
|
+
|
|
272
|
+
// Icon outside color, text inside
|
|
273
|
+
let content: string;
|
|
274
|
+
if (pct > 90) {
|
|
275
|
+
content = withIcon(icons.context, fgOnly("error", text));
|
|
276
|
+
} else if (pct > 70) {
|
|
277
|
+
content = withIcon(icons.context, fgOnly("warning", text));
|
|
278
|
+
} else {
|
|
279
|
+
content = withIcon(icons.context, fgOnly("context", text));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { content, visible: true };
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const contextTotalSegment: StatusLineSegment = {
|
|
287
|
+
id: "context_total",
|
|
288
|
+
render(ctx) {
|
|
289
|
+
const icons = getIcons();
|
|
290
|
+
const window = ctx.contextWindow;
|
|
291
|
+
if (!window) return { content: "", visible: false };
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
content: fgOnly("context", withIcon(icons.context, formatTokens(window))),
|
|
295
|
+
visible: true,
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const timeSpentSegment: StatusLineSegment = {
|
|
301
|
+
id: "time_spent",
|
|
302
|
+
render(ctx) {
|
|
303
|
+
const icons = getIcons();
|
|
304
|
+
const elapsed = Date.now() - ctx.sessionStartTime;
|
|
305
|
+
if (elapsed < 1000) return { content: "", visible: false };
|
|
306
|
+
|
|
307
|
+
// No explicit color
|
|
308
|
+
return { content: withIcon(icons.time, formatDuration(elapsed)), visible: true };
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const timeSegment: StatusLineSegment = {
|
|
313
|
+
id: "time",
|
|
314
|
+
render(ctx) {
|
|
315
|
+
const icons = getIcons();
|
|
316
|
+
const opts = ctx.options.time ?? {};
|
|
317
|
+
const now = new Date();
|
|
318
|
+
|
|
319
|
+
let hours = now.getHours();
|
|
320
|
+
let suffix = "";
|
|
321
|
+
if (opts.format === "12h") {
|
|
322
|
+
suffix = hours >= 12 ? "pm" : "am";
|
|
323
|
+
hours = hours % 12 || 12;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const mins = now.getMinutes().toString().padStart(2, "0");
|
|
327
|
+
let timeStr = `${hours}:${mins}`;
|
|
328
|
+
if (opts.showSeconds) {
|
|
329
|
+
timeStr += `:${now.getSeconds().toString().padStart(2, "0")}`;
|
|
330
|
+
}
|
|
331
|
+
timeStr += suffix;
|
|
332
|
+
|
|
333
|
+
// No explicit color
|
|
334
|
+
return { content: withIcon(icons.time, timeStr), visible: true };
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const sessionSegment: StatusLineSegment = {
|
|
339
|
+
id: "session",
|
|
340
|
+
render(ctx) {
|
|
341
|
+
const icons = getIcons();
|
|
342
|
+
const sessionId = ctx.sessionId;
|
|
343
|
+
const display = sessionId?.slice(0, 8) || "new";
|
|
344
|
+
|
|
345
|
+
// No explicit color
|
|
346
|
+
return { content: withIcon(icons.session, display), visible: true };
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const hostnameSegment: StatusLineSegment = {
|
|
351
|
+
id: "hostname",
|
|
352
|
+
render(_ctx) {
|
|
353
|
+
const icons = getIcons();
|
|
354
|
+
const name = osHostname().split(".")[0];
|
|
355
|
+
// No explicit color
|
|
356
|
+
return { content: withIcon(icons.host, name), visible: true };
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const cacheReadSegment: StatusLineSegment = {
|
|
361
|
+
id: "cache_read",
|
|
362
|
+
render(ctx) {
|
|
363
|
+
const icons = getIcons();
|
|
364
|
+
const { cacheRead } = ctx.usageStats;
|
|
365
|
+
if (!cacheRead) return { content: "", visible: false };
|
|
366
|
+
|
|
367
|
+
// Space-separated parts
|
|
368
|
+
const parts = [icons.cache, icons.input, formatTokens(cacheRead)].filter(Boolean);
|
|
369
|
+
const content = parts.join(" ");
|
|
370
|
+
return { content: fgOnly("spend", content), visible: true };
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const cacheWriteSegment: StatusLineSegment = {
|
|
375
|
+
id: "cache_write",
|
|
376
|
+
render(ctx) {
|
|
377
|
+
const icons = getIcons();
|
|
378
|
+
const { cacheWrite } = ctx.usageStats;
|
|
379
|
+
if (!cacheWrite) return { content: "", visible: false };
|
|
380
|
+
|
|
381
|
+
// Space-separated parts
|
|
382
|
+
const parts = [icons.cache, icons.output, formatTokens(cacheWrite)].filter(Boolean);
|
|
383
|
+
const content = parts.join(" ");
|
|
384
|
+
return { content: fgOnly("output", content), visible: true };
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const extensionStatusesSegment: StatusLineSegment = {
|
|
389
|
+
id: "extension_statuses",
|
|
390
|
+
render(ctx) {
|
|
391
|
+
const statuses = ctx.extensionStatuses;
|
|
392
|
+
if (!statuses || statuses.size === 0) return { content: "", visible: false };
|
|
393
|
+
|
|
394
|
+
// Join all extension statuses with a separator
|
|
395
|
+
const parts: string[] = [];
|
|
396
|
+
for (const [_key, value] of statuses) {
|
|
397
|
+
if (value) parts.push(value);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (parts.length === 0) return { content: "", visible: false };
|
|
401
|
+
|
|
402
|
+
// Statuses already have their own styling applied by the extensions
|
|
403
|
+
const content = parts.join(` ${SEP_DOT} `);
|
|
404
|
+
return { content, visible: true };
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
409
|
+
// Segment Registry
|
|
410
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
411
|
+
|
|
412
|
+
export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
|
|
413
|
+
pi: piSegment,
|
|
414
|
+
model: modelSegment,
|
|
415
|
+
path: pathSegment,
|
|
416
|
+
git: gitSegment,
|
|
417
|
+
thinking: thinkingSegment,
|
|
418
|
+
subagents: subagentsSegment,
|
|
419
|
+
token_in: tokenInSegment,
|
|
420
|
+
token_out: tokenOutSegment,
|
|
421
|
+
token_total: tokenTotalSegment,
|
|
422
|
+
cost: costSegment,
|
|
423
|
+
context_pct: contextPctSegment,
|
|
424
|
+
context_total: contextTotalSegment,
|
|
425
|
+
time_spent: timeSpentSegment,
|
|
426
|
+
time: timeSegment,
|
|
427
|
+
session: sessionSegment,
|
|
428
|
+
hostname: hostnameSegment,
|
|
429
|
+
cache_read: cacheReadSegment,
|
|
430
|
+
cache_write: cacheWriteSegment,
|
|
431
|
+
extension_statuses: extensionStatusesSegment,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export function renderSegment(id: StatusLineSegmentId, ctx: SegmentContext): RenderedSegment {
|
|
435
|
+
const segment = SEGMENTS[id];
|
|
436
|
+
if (!segment) {
|
|
437
|
+
return { content: "", visible: false };
|
|
438
|
+
}
|
|
439
|
+
return segment.render(ctx);
|
|
440
|
+
}
|
package/separators.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SeparatorDef, StatusLineSeparatorStyle } from "./types.js";
|
|
2
|
+
import { getSeparatorChars } from "./icons.js";
|
|
3
|
+
|
|
4
|
+
export function getSeparator(style: StatusLineSeparatorStyle): SeparatorDef {
|
|
5
|
+
const chars = getSeparatorChars();
|
|
6
|
+
|
|
7
|
+
switch (style) {
|
|
8
|
+
case "powerline":
|
|
9
|
+
return {
|
|
10
|
+
left: chars.powerlineLeft,
|
|
11
|
+
right: chars.powerlineRight,
|
|
12
|
+
endCaps: {
|
|
13
|
+
left: chars.powerlineRight,
|
|
14
|
+
right: chars.powerlineLeft,
|
|
15
|
+
useBgAsFg: true,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
case "powerline-thin":
|
|
20
|
+
return {
|
|
21
|
+
left: chars.powerlineThinLeft,
|
|
22
|
+
right: chars.powerlineThinRight,
|
|
23
|
+
endCaps: {
|
|
24
|
+
left: chars.powerlineRight,
|
|
25
|
+
right: chars.powerlineLeft,
|
|
26
|
+
useBgAsFg: true,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
case "slash":
|
|
31
|
+
return { left: ` ${chars.slash} `, right: ` ${chars.slash} ` };
|
|
32
|
+
|
|
33
|
+
case "pipe":
|
|
34
|
+
return { left: ` ${chars.pipe} `, right: ` ${chars.pipe} ` };
|
|
35
|
+
|
|
36
|
+
case "block":
|
|
37
|
+
return { left: chars.block, right: chars.block };
|
|
38
|
+
|
|
39
|
+
case "none":
|
|
40
|
+
return { left: chars.space, right: chars.space };
|
|
41
|
+
|
|
42
|
+
case "ascii":
|
|
43
|
+
return { left: chars.asciiLeft, right: chars.asciiRight };
|
|
44
|
+
|
|
45
|
+
case "dot":
|
|
46
|
+
return { left: chars.dot, right: chars.dot };
|
|
47
|
+
|
|
48
|
+
case "chevron":
|
|
49
|
+
return { left: "›", right: "‹" };
|
|
50
|
+
|
|
51
|
+
case "star":
|
|
52
|
+
return { left: "✦", right: "✦" };
|
|
53
|
+
|
|
54
|
+
default:
|
|
55
|
+
return getSeparator("powerline-thin");
|
|
56
|
+
}
|
|
57
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Segment identifiers
|
|
2
|
+
export type StatusLineSegmentId =
|
|
3
|
+
| "pi"
|
|
4
|
+
| "model"
|
|
5
|
+
| "path"
|
|
6
|
+
| "git"
|
|
7
|
+
| "subagents"
|
|
8
|
+
| "token_in"
|
|
9
|
+
| "token_out"
|
|
10
|
+
| "token_total"
|
|
11
|
+
| "cost"
|
|
12
|
+
| "context_pct"
|
|
13
|
+
| "context_total"
|
|
14
|
+
| "time_spent"
|
|
15
|
+
| "time"
|
|
16
|
+
| "session"
|
|
17
|
+
| "hostname"
|
|
18
|
+
| "cache_read"
|
|
19
|
+
| "cache_write"
|
|
20
|
+
| "thinking"
|
|
21
|
+
| "extension_statuses";
|
|
22
|
+
|
|
23
|
+
// Separator styles
|
|
24
|
+
export type StatusLineSeparatorStyle =
|
|
25
|
+
| "powerline"
|
|
26
|
+
| "powerline-thin"
|
|
27
|
+
| "slash"
|
|
28
|
+
| "pipe"
|
|
29
|
+
| "block"
|
|
30
|
+
| "none"
|
|
31
|
+
| "ascii"
|
|
32
|
+
| "dot"
|
|
33
|
+
| "chevron"
|
|
34
|
+
| "star";
|
|
35
|
+
|
|
36
|
+
// Preset names
|
|
37
|
+
export type StatusLinePreset =
|
|
38
|
+
| "default"
|
|
39
|
+
| "minimal"
|
|
40
|
+
| "compact"
|
|
41
|
+
| "full"
|
|
42
|
+
| "nerd"
|
|
43
|
+
| "ascii"
|
|
44
|
+
| "custom";
|
|
45
|
+
|
|
46
|
+
// Per-segment options
|
|
47
|
+
export interface StatusLineSegmentOptions {
|
|
48
|
+
model?: { showThinkingLevel?: boolean };
|
|
49
|
+
path?: {
|
|
50
|
+
mode?: "basename" | "abbreviated" | "full";
|
|
51
|
+
maxLength?: number;
|
|
52
|
+
};
|
|
53
|
+
git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
|
|
54
|
+
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Preset definition
|
|
58
|
+
export interface PresetDef {
|
|
59
|
+
leftSegments: StatusLineSegmentId[];
|
|
60
|
+
rightSegments: StatusLineSegmentId[];
|
|
61
|
+
separator: StatusLineSeparatorStyle;
|
|
62
|
+
segmentOptions?: StatusLineSegmentOptions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Separator definition
|
|
66
|
+
export interface SeparatorDef {
|
|
67
|
+
left: string;
|
|
68
|
+
right: string;
|
|
69
|
+
endCaps?: {
|
|
70
|
+
left: string;
|
|
71
|
+
right: string;
|
|
72
|
+
useBgAsFg: boolean;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Git status data
|
|
77
|
+
export interface GitStatus {
|
|
78
|
+
branch: string | null;
|
|
79
|
+
staged: number;
|
|
80
|
+
unstaged: number;
|
|
81
|
+
untracked: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Usage statistics
|
|
85
|
+
export interface UsageStats {
|
|
86
|
+
input: number;
|
|
87
|
+
output: number;
|
|
88
|
+
cacheRead: number;
|
|
89
|
+
cacheWrite: number;
|
|
90
|
+
cost: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Context passed to segment render functions
|
|
94
|
+
export interface SegmentContext {
|
|
95
|
+
// From pi-mono
|
|
96
|
+
model: { id: string; name?: string; reasoning?: boolean; contextWindow?: number } | undefined;
|
|
97
|
+
thinkingLevel: string;
|
|
98
|
+
sessionId: string | undefined;
|
|
99
|
+
|
|
100
|
+
// Computed
|
|
101
|
+
usageStats: UsageStats;
|
|
102
|
+
contextPercent: number;
|
|
103
|
+
contextWindow: number;
|
|
104
|
+
autoCompactEnabled: boolean;
|
|
105
|
+
usingSubscription: boolean;
|
|
106
|
+
sessionStartTime: number;
|
|
107
|
+
|
|
108
|
+
// Git
|
|
109
|
+
git: GitStatus;
|
|
110
|
+
|
|
111
|
+
// Extension statuses
|
|
112
|
+
extensionStatuses: ReadonlyMap<string, string>;
|
|
113
|
+
|
|
114
|
+
// Options
|
|
115
|
+
options: StatusLineSegmentOptions;
|
|
116
|
+
width: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Rendered segment output
|
|
120
|
+
export interface RenderedSegment {
|
|
121
|
+
content: string;
|
|
122
|
+
visible: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Segment definition
|
|
126
|
+
export interface StatusLineSegment {
|
|
127
|
+
id: StatusLineSegmentId;
|
|
128
|
+
render(ctx: SegmentContext): RenderedSegment;
|
|
129
|
+
}
|