pi-cc-extensions 0.3.0 → 0.5.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/README.md +1 -18
- package/extensions/claude-code-style.ts +194 -41
- package/package.json +2 -3
- package/assets/readme/showcase.webp +0 -0
- package/assets/readme/welcome.webp +0 -0
- package/extensions/ccstyle-powerline/bash-mode/completion.ts +0 -556
- package/extensions/ccstyle-powerline/bash-mode/editor.ts +0 -442
- package/extensions/ccstyle-powerline/bash-mode/history.ts +0 -151
- package/extensions/ccstyle-powerline/bash-mode/shell-session.ts +0 -286
- package/extensions/ccstyle-powerline/bash-mode/transcript.ts +0 -108
- package/extensions/ccstyle-powerline/bash-mode/types.ts +0 -59
- package/extensions/ccstyle-powerline/colors.ts +0 -69
- package/extensions/ccstyle-powerline/context-usage.ts +0 -46
- package/extensions/ccstyle-powerline/fixed-editor/cluster.ts +0 -107
- package/extensions/ccstyle-powerline/fixed-editor/terminal-split.ts +0 -1389
- package/extensions/ccstyle-powerline/git-status.ts +0 -283
- package/extensions/ccstyle-powerline/icons.ts +0 -181
- package/extensions/ccstyle-powerline/powerline-config.ts +0 -234
- package/extensions/ccstyle-powerline/powerline.ts +0 -3218
- package/extensions/ccstyle-powerline/presets.ts +0 -109
- package/extensions/ccstyle-powerline/render-scheduler.ts +0 -24
- package/extensions/ccstyle-powerline/segments.ts +0 -486
- package/extensions/ccstyle-powerline/separators.ts +0 -41
- package/extensions/ccstyle-powerline/shortcuts.ts +0 -57
- package/extensions/ccstyle-powerline/theme.json +0 -26
- package/extensions/ccstyle-powerline/theme.ts +0 -241
- package/extensions/ccstyle-powerline/types.ts +0 -200
- package/extensions/ccstyle-powerline/welcome-dismiss.ts +0 -34
- package/extensions/ccstyle-powerline/welcome.ts +0 -611
- package/extensions/ccstyle-powerline/working-vibes.ts +0 -695
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import type { GitStatus } from "./types.ts";
|
|
3
|
-
|
|
4
|
-
interface CachedGitStatus {
|
|
5
|
-
staged: number;
|
|
6
|
-
unstaged: number;
|
|
7
|
-
untracked: number;
|
|
8
|
-
added: number;
|
|
9
|
-
deleted: number;
|
|
10
|
-
timestamp: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface CachedBranch {
|
|
14
|
-
branch: string | null;
|
|
15
|
-
timestamp: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type GitPollingMode = "full" | "branch" | "off";
|
|
19
|
-
|
|
20
|
-
const CACHE_TTL_MS = 1000; // 1 second for file status
|
|
21
|
-
const BRANCH_TTL_MS = 500; // Shorter TTL so branch updates quickly after invalidation
|
|
22
|
-
let cachedStatus: CachedGitStatus | null = null;
|
|
23
|
-
let cachedBranch: CachedBranch | null = null;
|
|
24
|
-
let statusCwd: string | null = null;
|
|
25
|
-
let branchCwd: string | null = null;
|
|
26
|
-
let pendingFetch: Promise<void> | null = null;
|
|
27
|
-
let pendingBranchFetch: Promise<void> | null = null;
|
|
28
|
-
let fetchCwd: string | null = null;
|
|
29
|
-
let branchFetchCwd: string | null = null;
|
|
30
|
-
let invalidationCounter = 0; // Track invalidations to prevent stale updates
|
|
31
|
-
let branchInvalidationCounter = 0;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parse git status --porcelain output.
|
|
35
|
-
*
|
|
36
|
-
* Format: XY filename
|
|
37
|
-
* X = index status, Y = working tree status
|
|
38
|
-
* ?? = untracked
|
|
39
|
-
*
|
|
40
|
-
* Each file is counted exactly once:
|
|
41
|
-
* - Y ≠ ' ' → unstaged (working tree has changes; if X also shows staged, Y wins)
|
|
42
|
-
* - X ≠ ' ' && X ≠ '?' → staged only (index has changes, working tree is clean)
|
|
43
|
-
*/
|
|
44
|
-
function parseGitStatusOutput(output: string): { staged: number; unstaged: number; untracked: number } {
|
|
45
|
-
let staged = 0;
|
|
46
|
-
let unstaged = 0;
|
|
47
|
-
let untracked = 0;
|
|
48
|
-
|
|
49
|
-
for (const line of output.split("\n")) {
|
|
50
|
-
if (!line) continue;
|
|
51
|
-
const x = line[0];
|
|
52
|
-
const y = line[1];
|
|
53
|
-
|
|
54
|
-
if (x === "?" && y === "?") {
|
|
55
|
-
untracked++;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Y position (working tree) takes priority — if the file has working-tree
|
|
60
|
-
// changes at all, count it as unstaged.
|
|
61
|
-
if (y && y !== " ") {
|
|
62
|
-
unstaged++;
|
|
63
|
-
} else if (x && x !== " " && x !== "?") {
|
|
64
|
-
// X position (index) only counted when working tree is clean.
|
|
65
|
-
staged++;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return { staged, unstaged, untracked };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function runGit(args: string[], cwd: string, timeoutMs = 200): Promise<string | null> {
|
|
73
|
-
return new Promise((resolve) => {
|
|
74
|
-
const proc = spawn("git", args, {
|
|
75
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
76
|
-
cwd,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
let stdout = "";
|
|
80
|
-
let resolved = false;
|
|
81
|
-
|
|
82
|
-
const finish = (result: string | null) => {
|
|
83
|
-
if (resolved) return;
|
|
84
|
-
resolved = true;
|
|
85
|
-
clearTimeout(timeoutId);
|
|
86
|
-
resolve(result);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
proc.stdout.on("data", (data) => {
|
|
90
|
-
stdout += data.toString();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
proc.on("close", (code) => {
|
|
94
|
-
finish(code === 0 ? stdout.replace(/\n+$/, "") : null);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
proc.on("error", () => {
|
|
98
|
-
finish(null);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const timeoutId = setTimeout(() => {
|
|
102
|
-
proc.kill();
|
|
103
|
-
finish(null);
|
|
104
|
-
}, timeoutMs);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Fetch current git branch asynchronously.
|
|
110
|
-
* For detached HEAD, returns the short commit SHA (matches provider's "detached" behavior).
|
|
111
|
-
*/
|
|
112
|
-
async function fetchGitBranch(cwd: string): Promise<string | null> {
|
|
113
|
-
const branch = await runGit(["branch", "--show-current"], cwd);
|
|
114
|
-
if (branch === null) return null;
|
|
115
|
-
if (branch) return branch;
|
|
116
|
-
|
|
117
|
-
const sha = await runGit(["rev-parse", "--short", "HEAD"], cwd);
|
|
118
|
-
return sha ? `${sha} (detached)` : "detached";
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Parse git diff --shortstat HEAD output.
|
|
123
|
-
* Format: " N files changed, M insertions(+), K deletions(-)"
|
|
124
|
-
*/
|
|
125
|
-
function parseDiffStat(output: string): { added: number; deleted: number } {
|
|
126
|
-
const added = (output.match(/(\d+) insertion/) ?? [])[1];
|
|
127
|
-
const deleted = (output.match(/(\d+) deletion/) ?? [])[1];
|
|
128
|
-
return {
|
|
129
|
-
added: added ? Number(added) : 0,
|
|
130
|
-
deleted: deleted ? Number(deleted) : 0,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Fetch line-level diff stats (additions / deletions) from HEAD.
|
|
136
|
-
*/
|
|
137
|
-
async function fetchDiffStat(cwd: string): Promise<{ added: number; deleted: number } | null> {
|
|
138
|
-
const output = await runGit(["diff", "--shortstat", "HEAD"], cwd, 1000);
|
|
139
|
-
if (output === null) return null;
|
|
140
|
-
return parseDiffStat(output);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Fetch git status asynchronously.
|
|
145
|
-
*/
|
|
146
|
-
async function fetchGitStatus(cwd: string): Promise<{ staged: number; unstaged: number; untracked: number } | null> {
|
|
147
|
-
const output = await runGit(["status", "--porcelain"], cwd, 500);
|
|
148
|
-
if (output === null) return null;
|
|
149
|
-
return parseGitStatusOutput(output);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get the current git branch with caching.
|
|
154
|
-
* Falls back to provider branch if our cache is empty.
|
|
155
|
-
*/
|
|
156
|
-
export function getCurrentBranch(providerBranch: string | null, cwd: string): string | null {
|
|
157
|
-
const now = Date.now();
|
|
158
|
-
|
|
159
|
-
// Invalidate cache if cwd changed (different repo)
|
|
160
|
-
if (branchCwd !== cwd) {
|
|
161
|
-
cachedBranch = null;
|
|
162
|
-
branchCwd = cwd;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Return cached if fresh
|
|
166
|
-
if (cachedBranch && now - cachedBranch.timestamp < BRANCH_TTL_MS) {
|
|
167
|
-
return cachedBranch.branch;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Trigger background fetch if not already pending
|
|
171
|
-
if (!pendingBranchFetch) {
|
|
172
|
-
const fetchId = branchInvalidationCounter;
|
|
173
|
-
branchFetchCwd = cwd;
|
|
174
|
-
pendingBranchFetch = fetchGitBranch(cwd).then((result) => {
|
|
175
|
-
// Cache result if no invalidation happened AND cwd hasn't changed
|
|
176
|
-
if (fetchId === branchInvalidationCounter && branchFetchCwd === cwd) {
|
|
177
|
-
cachedBranch = {
|
|
178
|
-
branch: result,
|
|
179
|
-
timestamp: Date.now(),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
pendingBranchFetch = null;
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Return stale cache while refreshing; only use provider before first fetch
|
|
187
|
-
return cachedBranch ? cachedBranch.branch : providerBranch;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Get git status with caching.
|
|
192
|
-
* Returns cached value if within TTL, otherwise triggers async fetch.
|
|
193
|
-
* This is designed for synchronous render() calls - returns last known value
|
|
194
|
-
* while refreshing in background.
|
|
195
|
-
*/
|
|
196
|
-
export function getGitStatus(providerBranch: string | null, cwd: string, pollingMode: GitPollingMode = "full"): GitStatus {
|
|
197
|
-
const now = Date.now();
|
|
198
|
-
const branch = pollingMode === "off" ? providerBranch : getCurrentBranch(providerBranch, cwd);
|
|
199
|
-
|
|
200
|
-
if (pollingMode !== "full") {
|
|
201
|
-
return { branch, staged: 0, unstaged: 0, untracked: 0, added: 0, deleted: 0 };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Invalidate cache if cwd changed (different repo)
|
|
205
|
-
if (statusCwd !== cwd) {
|
|
206
|
-
cachedStatus = null;
|
|
207
|
-
statusCwd = cwd;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Return cached if fresh
|
|
211
|
-
if (cachedStatus && now - cachedStatus.timestamp < CACHE_TTL_MS) {
|
|
212
|
-
return {
|
|
213
|
-
branch,
|
|
214
|
-
staged: cachedStatus.staged,
|
|
215
|
-
unstaged: cachedStatus.unstaged,
|
|
216
|
-
untracked: cachedStatus.untracked,
|
|
217
|
-
added: cachedStatus.added,
|
|
218
|
-
deleted: cachedStatus.deleted,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Trigger background fetch if not already pending
|
|
223
|
-
if (!pendingFetch) {
|
|
224
|
-
const fetchId = invalidationCounter; // Capture current counter
|
|
225
|
-
fetchCwd = cwd;
|
|
226
|
-
pendingFetch = Promise.all([
|
|
227
|
-
fetchGitStatus(cwd),
|
|
228
|
-
fetchDiffStat(cwd),
|
|
229
|
-
]).then(([statusResult, diffResult]) => {
|
|
230
|
-
// Cache result if no invalidation happened AND cwd hasn't changed
|
|
231
|
-
if (fetchId === invalidationCounter && fetchCwd === cwd) {
|
|
232
|
-
cachedStatus = statusResult
|
|
233
|
-
? {
|
|
234
|
-
staged: statusResult.staged,
|
|
235
|
-
unstaged: statusResult.unstaged,
|
|
236
|
-
untracked: statusResult.untracked,
|
|
237
|
-
added: diffResult?.added ?? 0,
|
|
238
|
-
deleted: diffResult?.deleted ?? 0,
|
|
239
|
-
timestamp: Date.now(),
|
|
240
|
-
}
|
|
241
|
-
: {
|
|
242
|
-
staged: 0,
|
|
243
|
-
unstaged: 0,
|
|
244
|
-
untracked: 0,
|
|
245
|
-
added: 0,
|
|
246
|
-
deleted: 0,
|
|
247
|
-
timestamp: Date.now(),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
pendingFetch = null;
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Return last cached or empty
|
|
255
|
-
if (cachedStatus) {
|
|
256
|
-
return {
|
|
257
|
-
branch,
|
|
258
|
-
staged: cachedStatus.staged,
|
|
259
|
-
unstaged: cachedStatus.unstaged,
|
|
260
|
-
untracked: cachedStatus.untracked,
|
|
261
|
-
added: cachedStatus.added,
|
|
262
|
-
deleted: cachedStatus.deleted,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return { branch, staged: 0, unstaged: 0, untracked: 0, added: 0, deleted: 0 };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Force refresh git status (call when you know files changed)
|
|
271
|
-
*/
|
|
272
|
-
export function invalidateGitStatus(): void {
|
|
273
|
-
cachedStatus = null;
|
|
274
|
-
invalidationCounter++; // Increment to invalidate any pending fetches
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Force refresh git branch (call when you know branch might have changed)
|
|
279
|
-
*/
|
|
280
|
-
export function invalidateGitBranch(): void {
|
|
281
|
-
cachedBranch = null;
|
|
282
|
-
branchInvalidationCounter++;
|
|
283
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { loadThemeConfig } from "./theme.ts";
|
|
2
|
-
|
|
3
|
-
export interface IconSet {
|
|
4
|
-
pi: string;
|
|
5
|
-
model: string;
|
|
6
|
-
folder: string;
|
|
7
|
-
branch: string;
|
|
8
|
-
git: string;
|
|
9
|
-
tokens: string;
|
|
10
|
-
context: string;
|
|
11
|
-
cost: string;
|
|
12
|
-
time: string;
|
|
13
|
-
agents: string;
|
|
14
|
-
cache: string;
|
|
15
|
-
input: string;
|
|
16
|
-
output: string;
|
|
17
|
-
host: string;
|
|
18
|
-
session: string;
|
|
19
|
-
auto: string;
|
|
20
|
-
warning: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Separator characters
|
|
24
|
-
export const SEP_DOT = " · ";
|
|
25
|
-
|
|
26
|
-
// Thinking level display text (Unicode/ASCII)
|
|
27
|
-
export const THINKING_TEXT_UNICODE: Record<string, string> = {
|
|
28
|
-
minimal: "[min]",
|
|
29
|
-
low: "[low]",
|
|
30
|
-
medium: "[med]",
|
|
31
|
-
high: "[high]",
|
|
32
|
-
xhigh: "[xhi]",
|
|
33
|
-
max: "[max]",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Thinking level display text (Nerd Fonts - with icons)
|
|
37
|
-
export const THINKING_TEXT_NERD: Record<string, string> = {
|
|
38
|
-
minimal: "\u{F0E7} min", // lightning bolt
|
|
39
|
-
low: "\u{F10C} low", // circle outline
|
|
40
|
-
medium: "\u{F192} med", // dot circle
|
|
41
|
-
high: "\u{F111} high", // circle
|
|
42
|
-
xhigh: "\u{F06D} xhi", // fire
|
|
43
|
-
max: "\u{F06D} max", // fire (maximum reasoning)
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Get thinking text based on font support
|
|
47
|
-
export function getThinkingText(level: string): string | undefined {
|
|
48
|
-
if (hasNerdFonts()) {
|
|
49
|
-
return THINKING_TEXT_NERD[level];
|
|
50
|
-
}
|
|
51
|
-
return THINKING_TEXT_UNICODE[level];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Nerd Font icons (matching oh-my-pi exactly)
|
|
55
|
-
export const NERD_ICONS: IconSet = {
|
|
56
|
-
pi: "\uE22C", // nf-oct-pi (stylized pi icon)
|
|
57
|
-
model: "\uEC19", // nf-md-chip (model/AI chip)
|
|
58
|
-
folder: "\uF115", // nf-fa-folder_open
|
|
59
|
-
branch: "\uF126", // nf-fa-code_fork (git branch)
|
|
60
|
-
git: "\uF1D3", // nf-fa-git (git logo)
|
|
61
|
-
tokens: "\uE26B", // nf-seti-html (tokens symbol)
|
|
62
|
-
context: "\uE70F", // nf-dev-database (database)
|
|
63
|
-
cost: "\uF155", // nf-fa-dollar
|
|
64
|
-
time: "\uF017", // nf-fa-clock_o
|
|
65
|
-
agents: "\uF0C0", // nf-fa-users
|
|
66
|
-
cache: "\uF1C0", // nf-fa-database (cache)
|
|
67
|
-
input: "\uF090", // nf-fa-sign_in (input arrow)
|
|
68
|
-
output: "\uF08B", // nf-fa-sign_out (output arrow)
|
|
69
|
-
host: "\uF109", // nf-fa-laptop (host)
|
|
70
|
-
session: "\uF550", // nf-md-identifier (session id)
|
|
71
|
-
auto: "\u{F0068}", // nf-md-lightning_bolt (auto-compact)
|
|
72
|
-
warning: "\uF071", // nf-fa-warning
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// ASCII/Unicode fallback icons — pure ASCII only, no obscure Unicode that may render as □
|
|
76
|
-
export const ASCII_ICONS: IconSet = {
|
|
77
|
-
pi: "pi",
|
|
78
|
-
model: "",
|
|
79
|
-
folder: "dir",
|
|
80
|
-
branch: "@",
|
|
81
|
-
git: "@",
|
|
82
|
-
tokens: "~",
|
|
83
|
-
context: "ctx",
|
|
84
|
-
cost: "$",
|
|
85
|
-
time: "~",
|
|
86
|
-
agents: "AG",
|
|
87
|
-
cache: "cache",
|
|
88
|
-
input: "in:",
|
|
89
|
-
output: "out:",
|
|
90
|
-
host: "host",
|
|
91
|
-
session: "id",
|
|
92
|
-
auto: "AC",
|
|
93
|
-
warning: "!",
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
type PartialIconSet = Partial<IconSet>;
|
|
97
|
-
|
|
98
|
-
function sanitizeUserIconOverrides(value: unknown): PartialIconSet {
|
|
99
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const sanitized: PartialIconSet = {};
|
|
104
|
-
const validKeys = Object.keys(NERD_ICONS) as Array<keyof IconSet>;
|
|
105
|
-
for (const key of validKeys) {
|
|
106
|
-
const icon = value[key];
|
|
107
|
-
if (typeof icon === "string") {
|
|
108
|
-
sanitized[key] = icon;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return sanitized;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Separator characters
|
|
116
|
-
export interface SeparatorChars {
|
|
117
|
-
powerlineLeft: string;
|
|
118
|
-
powerlineRight: string;
|
|
119
|
-
powerlineThinLeft: string;
|
|
120
|
-
powerlineThinRight: string;
|
|
121
|
-
slash: string;
|
|
122
|
-
pipe: string;
|
|
123
|
-
block: string;
|
|
124
|
-
space: string;
|
|
125
|
-
asciiLeft: string;
|
|
126
|
-
asciiRight: string;
|
|
127
|
-
dot: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export const NERD_SEPARATORS: SeparatorChars = {
|
|
131
|
-
powerlineLeft: "\uE0B0", //
|
|
132
|
-
powerlineRight: "\uE0B2", //
|
|
133
|
-
powerlineThinLeft: "\uE0B1", //
|
|
134
|
-
powerlineThinRight: "\uE0B3", //
|
|
135
|
-
slash: "/",
|
|
136
|
-
pipe: "|",
|
|
137
|
-
block: "█",
|
|
138
|
-
space: " ",
|
|
139
|
-
asciiLeft: ">",
|
|
140
|
-
asciiRight: "<",
|
|
141
|
-
dot: "·",
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export const ASCII_SEPARATORS: SeparatorChars = {
|
|
145
|
-
powerlineLeft: ">",
|
|
146
|
-
powerlineRight: "<",
|
|
147
|
-
powerlineThinLeft: "|",
|
|
148
|
-
powerlineThinRight: "|",
|
|
149
|
-
slash: "/",
|
|
150
|
-
pipe: "|",
|
|
151
|
-
block: "#",
|
|
152
|
-
space: " ",
|
|
153
|
-
asciiLeft: ">",
|
|
154
|
-
asciiRight: "<",
|
|
155
|
-
dot: ".",
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
/* Detect Nerd Font support via env var, WT_SESSION, or TERM_PROGRAM.
|
|
159
|
-
* Windows Terminal (WT_SESSION) often ships with Nerd Fonts or Cascadia Code PL. */
|
|
160
|
-
export function hasNerdFonts(): boolean {
|
|
161
|
-
if (process.env.POWERLINE_NERD_FONTS === "1") return true;
|
|
162
|
-
if (process.env.POWERLINE_NERD_FONTS === "0") return false;
|
|
163
|
-
if (process.env.GHOSTTY_RESOURCES_DIR) return true;
|
|
164
|
-
// Windows Terminal — many users have Nerd Fonts installed
|
|
165
|
-
if (process.env.WT_SESSION) return true;
|
|
166
|
-
const term = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
167
|
-
const nerdTerms = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
|
|
168
|
-
return nerdTerms.some(t => term.includes(t));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function getIcons(): IconSet {
|
|
172
|
-
const baseIcons = hasNerdFonts() ? NERD_ICONS : ASCII_ICONS;
|
|
173
|
-
return {
|
|
174
|
-
...baseIcons,
|
|
175
|
-
...sanitizeUserIconOverrides(loadThemeConfig().icons),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function getSeparatorChars(): SeparatorChars {
|
|
180
|
-
return hasNerdFonts() ? NERD_SEPARATORS : ASCII_SEPARATORS;
|
|
181
|
-
}
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { ColorValue, CustomItemPosition, CustomStatusItem, PresetDef, StatusLinePreset, StatusLineSegmentId, StatusLineSegmentOptions } from "./types.ts";
|
|
3
|
-
|
|
4
|
-
export interface PowerlineConfig {
|
|
5
|
-
preset: StatusLinePreset;
|
|
6
|
-
customItems: CustomStatusItem[];
|
|
7
|
-
segmentOptions: StatusLineSegmentOptions;
|
|
8
|
-
mouseScroll: boolean;
|
|
9
|
-
fixedEditor: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function normalizePreset(value: unknown, presets: readonly StatusLinePreset[]): StatusLinePreset | null {
|
|
17
|
-
if (typeof value !== "string") return null;
|
|
18
|
-
const normalized = value.trim().toLowerCase();
|
|
19
|
-
return (presets as readonly string[]).includes(normalized) ? (normalized as StatusLinePreset) : null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function normalizeCustomItemId(value: unknown): string | null {
|
|
23
|
-
if (typeof value !== "string") return null;
|
|
24
|
-
const normalized = value.trim();
|
|
25
|
-
if (!normalized) return null;
|
|
26
|
-
return /^[a-zA-Z0-9_-]+$/.test(normalized) ? normalized : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function normalizeCustomItemPosition(value: unknown): CustomItemPosition {
|
|
30
|
-
if (value === "left" || value === "right" || value === "secondary") return value;
|
|
31
|
-
return "right";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function normalizeCustomColor(value: unknown): ColorValue | undefined {
|
|
35
|
-
if (typeof value !== "string") return undefined;
|
|
36
|
-
const normalized = value.trim();
|
|
37
|
-
return normalized ? (normalized as ColorValue) : undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function normalizeCustomPrefix(value: unknown): string | undefined {
|
|
41
|
-
if (typeof value !== "string") return undefined;
|
|
42
|
-
const normalized = value.trim();
|
|
43
|
-
return normalized ? normalized : undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function normalizeCustomStatusItem(raw: unknown, idOverride?: string): CustomStatusItem | null {
|
|
47
|
-
if (!isRecord(raw)) return null;
|
|
48
|
-
const id = normalizeCustomItemId(idOverride ?? raw.id);
|
|
49
|
-
if (!id) return null;
|
|
50
|
-
|
|
51
|
-
const statusKey = typeof raw.statusKey === "string" && raw.statusKey.trim() ? raw.statusKey.trim() : id;
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
id,
|
|
55
|
-
statusKey,
|
|
56
|
-
position: normalizeCustomItemPosition(raw.position),
|
|
57
|
-
color: normalizeCustomColor(raw.color),
|
|
58
|
-
prefix: normalizeCustomPrefix(raw.prefix),
|
|
59
|
-
hideWhenMissing: raw.hideWhenMissing !== false,
|
|
60
|
-
excludeFromExtensionStatuses: raw.excludeFromExtensionStatuses !== false,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeCustomItems(raw: unknown): CustomStatusItem[] {
|
|
65
|
-
const normalized: CustomStatusItem[] = [];
|
|
66
|
-
|
|
67
|
-
if (Array.isArray(raw)) {
|
|
68
|
-
for (const entry of raw) {
|
|
69
|
-
const item = normalizeCustomStatusItem(entry);
|
|
70
|
-
if (item) normalized.push(item);
|
|
71
|
-
}
|
|
72
|
-
} else if (isRecord(raw)) {
|
|
73
|
-
for (const [id, entry] of Object.entries(raw)) {
|
|
74
|
-
const item = normalizeCustomStatusItem(entry, id);
|
|
75
|
-
if (item) normalized.push(item);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const deduped = new Map<string, CustomStatusItem>();
|
|
80
|
-
for (const item of normalized) {
|
|
81
|
-
deduped.set(item.id, item);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return [...deduped.values()];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function normalizeSegmentOptions(raw: Record<string, unknown>): StatusLineSegmentOptions {
|
|
88
|
-
const options: StatusLineSegmentOptions = {};
|
|
89
|
-
|
|
90
|
-
if (isRecord(raw.model)) {
|
|
91
|
-
options.model = {
|
|
92
|
-
...(typeof raw.model.showThinkingLevel === "boolean" ? { showThinkingLevel: raw.model.showThinkingLevel } : {}),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (isRecord(raw.path)) {
|
|
97
|
-
options.path = {
|
|
98
|
-
...(raw.path.mode === "basename" || raw.path.mode === "abbreviated" || raw.path.mode === "full" ? { mode: raw.path.mode } : {}),
|
|
99
|
-
...(typeof raw.path.maxLength === "number" && Number.isFinite(raw.path.maxLength) && raw.path.maxLength > 0
|
|
100
|
-
? { maxLength: Math.floor(raw.path.maxLength) }
|
|
101
|
-
: {}),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (isRecord(raw.git)) {
|
|
106
|
-
options.git = {
|
|
107
|
-
...(typeof raw.git.showBranch === "boolean" ? { showBranch: raw.git.showBranch } : {}),
|
|
108
|
-
...(typeof raw.git.showStaged === "boolean" ? { showStaged: raw.git.showStaged } : {}),
|
|
109
|
-
...(typeof raw.git.showUnstaged === "boolean" ? { showUnstaged: raw.git.showUnstaged } : {}),
|
|
110
|
-
...(typeof raw.git.showUntracked === "boolean" ? { showUntracked: raw.git.showUntracked } : {}),
|
|
111
|
-
...(raw.git.polling === "full" || raw.git.polling === "branch" || raw.git.polling === "off" ? { polling: raw.git.polling } : {}),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (isRecord(raw.time)) {
|
|
116
|
-
options.time = {
|
|
117
|
-
...(raw.time.format === "12h" || raw.time.format === "24h" ? { format: raw.time.format } : {}),
|
|
118
|
-
...(typeof raw.time.showSeconds === "boolean" ? { showSeconds: raw.time.showSeconds } : {}),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return options;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function mergeSegmentOptions(
|
|
126
|
-
defaults: StatusLineSegmentOptions = {},
|
|
127
|
-
overrides: StatusLineSegmentOptions = {},
|
|
128
|
-
): StatusLineSegmentOptions {
|
|
129
|
-
return {
|
|
130
|
-
...defaults,
|
|
131
|
-
...overrides,
|
|
132
|
-
model: { ...defaults.model, ...overrides.model },
|
|
133
|
-
path: { ...defaults.path, ...overrides.path },
|
|
134
|
-
git: { ...defaults.git, ...overrides.git },
|
|
135
|
-
time: { ...defaults.time, ...overrides.time },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function parsePowerlineConfig(value: unknown, presets: readonly StatusLinePreset[]): PowerlineConfig {
|
|
140
|
-
const defaultConfig: PowerlineConfig = { preset: "default", customItems: [], segmentOptions: {}, mouseScroll: true, fixedEditor: true };
|
|
141
|
-
|
|
142
|
-
const directPreset = normalizePreset(value, presets);
|
|
143
|
-
if (directPreset) return { ...defaultConfig, preset: directPreset };
|
|
144
|
-
|
|
145
|
-
if (!isRecord(value)) return defaultConfig;
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
preset: normalizePreset(value.preset, presets) ?? defaultConfig.preset,
|
|
149
|
-
customItems: normalizeCustomItems(value.customItems),
|
|
150
|
-
segmentOptions: normalizeSegmentOptions(value),
|
|
151
|
-
mouseScroll: value.mouseScroll !== false,
|
|
152
|
-
fixedEditor: value.fixedEditor !== false,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function mergeSegmentsWithCustomItems(presetDef: PresetDef, customItems: readonly CustomStatusItem[]): {
|
|
157
|
-
leftSegments: StatusLineSegmentId[];
|
|
158
|
-
rightSegments: StatusLineSegmentId[];
|
|
159
|
-
secondarySegments: StatusLineSegmentId[];
|
|
160
|
-
} {
|
|
161
|
-
const left: StatusLineSegmentId[] = [...presetDef.leftSegments];
|
|
162
|
-
const right: StatusLineSegmentId[] = [...presetDef.rightSegments];
|
|
163
|
-
const secondary: StatusLineSegmentId[] = [...(presetDef.secondarySegments ?? [])];
|
|
164
|
-
|
|
165
|
-
for (const item of customItems) {
|
|
166
|
-
const segmentId: StatusLineSegmentId = `custom:${item.id}`;
|
|
167
|
-
if (item.position === "left") left.push(segmentId);
|
|
168
|
-
else if (item.position === "secondary") secondary.push(segmentId);
|
|
169
|
-
else right.push(segmentId);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return { leftSegments: left, rightSegments: right, secondarySegments: secondary };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function nextPowerlineSettingWithPreset(existingPowerlineSetting: unknown, preset: StatusLinePreset): unknown {
|
|
176
|
-
if (!isRecord(existingPowerlineSetting)) {
|
|
177
|
-
return preset;
|
|
178
|
-
}
|
|
179
|
-
return { ...existingPowerlineSetting, preset };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function nextPowerlineSettingWithOptions(
|
|
183
|
-
existingPowerlineSetting: unknown,
|
|
184
|
-
updates: Partial<Pick<PowerlineConfig, "mouseScroll" | "fixedEditor">>,
|
|
185
|
-
currentPreset: StatusLinePreset,
|
|
186
|
-
): unknown {
|
|
187
|
-
if (!isRecord(existingPowerlineSetting)) {
|
|
188
|
-
return { preset: currentPreset, ...updates };
|
|
189
|
-
}
|
|
190
|
-
return { ...existingPowerlineSetting, ...updates };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function collectHiddenExtensionStatusKeys(customItems: readonly CustomStatusItem[]): Set<string> {
|
|
194
|
-
const hidden = new Set<string>();
|
|
195
|
-
for (const item of customItems) {
|
|
196
|
-
if (item.excludeFromExtensionStatuses) hidden.add(item.statusKey);
|
|
197
|
-
}
|
|
198
|
-
return hidden;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function isNotificationExtensionStatus(value: string): boolean {
|
|
202
|
-
return value.trimStart().startsWith("[");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function getNotificationExtensionStatuses(
|
|
206
|
-
statuses: ReadonlyMap<string, string>,
|
|
207
|
-
hiddenKeys: ReadonlySet<string>,
|
|
208
|
-
): string[] {
|
|
209
|
-
const notifications: string[] = [];
|
|
210
|
-
for (const [statusKey, value] of statuses.entries()) {
|
|
211
|
-
if (hiddenKeys.has(statusKey) || !value || !isNotificationExtensionStatus(value)) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
notifications.push(value);
|
|
215
|
-
}
|
|
216
|
-
return notifications;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function normalizeExtensionStatusValue(value: string): string | null {
|
|
220
|
-
if (!value || visibleWidth(value) <= 0) {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const stripped = value.replace(/(\x1b\[[0-9;]*m|\s|·|[|])+$/, "");
|
|
225
|
-
return visibleWidth(stripped) > 0 ? stripped : null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function normalizeCompactExtensionStatus(value: string): string | null {
|
|
229
|
-
if (isNotificationExtensionStatus(value)) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return normalizeExtensionStatusValue(value);
|
|
234
|
-
}
|