@quandev104/pi-style 0.1.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +193 -0
  4. package/dist/extensions/pi-style.js +7897 -0
  5. package/dist/extensions/pi-style.js.map +1 -0
  6. package/extension-src/pi-style/app/command-service.ts +244 -0
  7. package/extension-src/pi-style/app/commands.ts +12 -0
  8. package/extension-src/pi-style/app/config-storage.ts +98 -0
  9. package/extension-src/pi-style/app/doctor.ts +53 -0
  10. package/extension-src/pi-style/app/index.ts +322 -0
  11. package/extension-src/pi-style/app/providers.ts +268 -0
  12. package/extension-src/pi-style/app/render-scheduler.ts +48 -0
  13. package/extension-src/pi-style/app/runtime.ts +404 -0
  14. package/extension-src/pi-style/app/snapshot.ts +15 -0
  15. package/extension-src/pi-style/domain/capabilities.ts +34 -0
  16. package/extension-src/pi-style/domain/config-authorization.ts +20 -0
  17. package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
  18. package/extension-src/pi-style/domain/config-diff.ts +30 -0
  19. package/extension-src/pi-style/domain/config-migrations.ts +52 -0
  20. package/extension-src/pi-style/domain/config-normalization.ts +567 -0
  21. package/extension-src/pi-style/domain/config-presets.ts +51 -0
  22. package/extension-src/pi-style/domain/config-types.ts +115 -0
  23. package/extension-src/pi-style/domain/providers.ts +37 -0
  24. package/extension-src/pi-style/domain/status-presets.ts +60 -0
  25. package/extension-src/pi-style/domain/status-renderer.ts +142 -0
  26. package/extension-src/pi-style/domain/status.ts +430 -0
  27. package/extension-src/pi-style/domain/theme.ts +232 -0
  28. package/extension-src/pi-style/features/.gitkeep +0 -0
  29. package/extension-src/pi-style/features/editor/index.ts +304 -0
  30. package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
  31. package/extension-src/pi-style/features/messages/index.ts +145 -0
  32. package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
  33. package/extension-src/pi-style/features/startup/index.ts +564 -0
  34. package/extension-src/pi-style/features/startup/logo.ts +151 -0
  35. package/extension-src/pi-style/features/status-line/index.ts +319 -0
  36. package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
  37. package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
  38. package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
  39. package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
  40. package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
  41. package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
  42. package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
  43. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
  44. package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
  45. package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
  46. package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
  47. package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
  48. package/extension-src/pi-style/features/tools/index.ts +480 -0
  49. package/extension-src/pi-style/pi/commands.ts +17 -0
  50. package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
  51. package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
  52. package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
  53. package/extension-src/pi-style/pi/config-host.ts +52 -0
  54. package/extension-src/pi-style/pi/config-session.ts +105 -0
  55. package/extension-src/pi-style/pi/index.ts +77 -0
  56. package/extension-src/pi-style/pi/operational-state.ts +29 -0
  57. package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
  58. package/extension-src/pi-style/pi/session-usage.ts +62 -0
  59. package/extension-src/pi-style/pi/startup-resources.ts +70 -0
  60. package/extension-src/pi-style/shared/.gitkeep +0 -0
  61. package/extension-src/pi-style/shared/ansi.ts +386 -0
  62. package/extension-src/pi-style/shared/box.ts +805 -0
  63. package/extension-src/pi-style/shared/disposable-store.ts +28 -0
  64. package/extension-src/pi-style/shared/elapsed.ts +88 -0
  65. package/extension-src/pi-style/shared/render-budget.ts +367 -0
  66. package/extension-src/pi-style/shared/split-diff.ts +692 -0
  67. package/extension-src/pi-style/shared/theme-extras.ts +292 -0
  68. package/package.json +68 -0
  69. package/themes/.gitkeep +0 -0
@@ -0,0 +1,386 @@
1
+ function isFinal(byte: string): boolean {
2
+ return byte >= "@" && byte <= "~";
3
+ }
4
+
5
+ const CUBE_STEPS = [0, 95, 135, 175, 215, 255] as const;
6
+
7
+ function xterm256ToRgb(index: number): { r: number; g: number; b: number } | undefined {
8
+ if (index >= 0 && index < 16) {
9
+ // Base 16 palette approximation used by the gradient logic.
10
+ const base = [
11
+ [0, 0, 0],
12
+ [128, 0, 0],
13
+ [0, 128, 0],
14
+ [128, 128, 0],
15
+ [0, 0, 128],
16
+ [128, 0, 128],
17
+ [0, 128, 128],
18
+ [192, 192, 192],
19
+ [128, 128, 128],
20
+ [255, 0, 0],
21
+ [0, 255, 0],
22
+ [255, 255, 0],
23
+ [0, 0, 255],
24
+ [255, 0, 255],
25
+ [0, 255, 255],
26
+ [255, 255, 255],
27
+ ] as const;
28
+ const entry = base[index] ?? [0, 0, 0];
29
+ const [r, g, b] = entry;
30
+ return { r, g, b };
31
+ }
32
+ if (index >= 16 && index < 232) {
33
+ const cube = index - 16;
34
+ return {
35
+ r: CUBE_STEPS[Math.floor(cube / 36)] ?? 0,
36
+ g: CUBE_STEPS[Math.floor((cube % 36) / 6)] ?? 0,
37
+ b: CUBE_STEPS[cube % 6] ?? 0,
38
+ };
39
+ }
40
+ if (index >= 232 && index < 256) {
41
+ const gray = 8 + 10 * (index - 232);
42
+ return { r: gray, g: gray, b: gray };
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ /**
48
+ * Parse an ANSI foreground prefix into RGB. Supports 24-bit `38;2;r;g;b` and
49
+ * 256-color `38;5;N` forms; returns undefined when the prefix is empty or not
50
+ * a foreground color (e.g. `38;5;Nm` reset fragments).
51
+ */
52
+ export function parseAnsiFgToRgb(prefix: string): { r: number; g: number; b: number } | undefined {
53
+ const esc = "\u001b";
54
+ const direct = new RegExp(`^${esc}\\[38;2;(\\d{1,3});(\\d{1,3});(\\d{1,3})m$`).exec(prefix);
55
+ if (direct) {
56
+ const r = Number.parseInt(direct[1] ?? "0", 10);
57
+ const g = Number.parseInt(direct[2] ?? "0", 10);
58
+ const b = Number.parseInt(direct[3] ?? "0", 10);
59
+ if (r <= 255 && g <= 255 && b <= 255) return { r, g, b };
60
+ }
61
+ const indexed = new RegExp(`^${esc}\\[38;5;(\\d{1,3})m$`).exec(prefix);
62
+ if (indexed) {
63
+ const index = Number.parseInt(indexed[1] ?? "0", 10);
64
+ if (index <= 255) return xterm256ToRgb(index);
65
+ }
66
+ return undefined;
67
+ }
68
+ export function stripAnsi(value: string): string {
69
+ let output = "";
70
+ for (let i = 0; i < value.length; i++) {
71
+ if (value.charCodeAt(i) !== 27) {
72
+ output += value[i];
73
+ continue;
74
+ }
75
+ i++;
76
+ if (value[i] === "[") {
77
+ while (i + 1 < value.length && !isFinal(value[i + 1] ?? "")) i++;
78
+ i++;
79
+ } else if (value[i] === "]") {
80
+ while (i + 1 < value.length && value.charCodeAt(i + 1) !== 7) {
81
+ i++;
82
+ if (value.charCodeAt(i) === 27 && value[i + 1] === "\\") {
83
+ i++;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ return output;
90
+ }
91
+ export function visibleWidth(value: string): number {
92
+ return [...stripAnsi(value)].length;
93
+ }
94
+ export function resetAnsi(value: string): string {
95
+ return `${value}\x1b[0m`;
96
+ }
97
+
98
+ /** Return the value unchanged when it fits the width; truncate it otherwise. */
99
+ export function fitAnsiWidth(value: string, width: number, ellipsis = "…"): string {
100
+ return visibleWidth(value) <= width ? value : truncateAnsi(value, width, ellipsis);
101
+ }
102
+ export function truncateAnsi(value: string, width: number, ellipsis = "…"): string {
103
+ if (width <= 0) return "";
104
+ if (visibleWidth(value) <= width) return resetAnsi(value);
105
+ let output = "";
106
+ let visible = 0;
107
+ for (let i = 0; i < value.length && visible < width - visibleWidth(ellipsis); i++) {
108
+ if (value.charCodeAt(i) === 27) {
109
+ const start = i;
110
+ i++;
111
+ while (i + 1 < value.length && !isFinal(value[i + 1] ?? "")) i++;
112
+ i++;
113
+ output += value.slice(start, i + 1);
114
+ continue;
115
+ }
116
+ output += value[i];
117
+ visible++;
118
+ }
119
+ return resetAnsi(output + ellipsis);
120
+ }
121
+ export function wrapAnsi(value: string, width: number): string[] {
122
+ if (width <= 0) return [""];
123
+ const lines: string[] = [];
124
+ let line = "";
125
+ for (const word of value.split(/\s+/)) {
126
+ const next = line ? `${line} ${word}` : word;
127
+ if (visibleWidth(next) <= width) line = next;
128
+ else {
129
+ if (line) lines.push(resetAnsi(line));
130
+ line = truncateAnsi(word, width);
131
+ }
132
+ }
133
+ if (line || lines.length === 0) lines.push(resetAnsi(line));
134
+ return lines;
135
+ }
136
+
137
+ // ── Hex color + background helpers ──
138
+
139
+ export const RESET_BACKGROUND = "\x1b[49m";
140
+ export const ERASE_TO_END_OF_LINE = "\x1b[K";
141
+ const ESC = "\x1b";
142
+
143
+ export function isHexColor(value: string): boolean {
144
+ const cleaned = value.replace("#", "");
145
+ return cleaned.length === 3
146
+ ? /^[0-9a-fA-F]{3}$/.test(cleaned)
147
+ : (cleaned.length === 6 || cleaned.length === 8) && /^[0-9a-fA-F]+$/.test(cleaned);
148
+ }
149
+
150
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } {
151
+ const cleaned = hex.replace("#", "");
152
+ if (cleaned.length === 3) {
153
+ const r = Number.parseInt((cleaned[0] ?? "") + (cleaned[0] ?? ""), 16);
154
+ const g = Number.parseInt((cleaned[1] ?? "") + (cleaned[1] ?? ""), 16);
155
+ const b = Number.parseInt((cleaned[2] ?? "") + (cleaned[2] ?? ""), 16);
156
+ return { r, g, b };
157
+ }
158
+ if ((cleaned.length !== 6 && cleaned.length !== 8) || !/^[0-9a-fA-F]+$/.test(cleaned)) {
159
+ return { r: 0, g: 0, b: 0 };
160
+ }
161
+ const r = Number.parseInt(cleaned.slice(0, 2), 16);
162
+ const g = Number.parseInt(cleaned.slice(2, 4), 16);
163
+ const b = Number.parseInt(cleaned.slice(4, 6), 16);
164
+ return { r, g, b };
165
+ }
166
+
167
+ function channelToHex(value: number): string {
168
+ return Math.max(0, Math.min(255, Math.round(value)))
169
+ .toString(16)
170
+ .padStart(2, "0");
171
+ }
172
+
173
+ export function rgbToHex(rgb: { r: number; g: number; b: number }): string {
174
+ return `#${channelToHex(rgb.r)}${channelToHex(rgb.g)}${channelToHex(rgb.b)}`;
175
+ }
176
+
177
+ const CUBE_VALUES = [0, 95, 135, 175, 215, 255] as const;
178
+ const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);
179
+
180
+ function _ansi256ToRgb(index: number): { r: number; g: number; b: number } {
181
+ if (index >= 232) {
182
+ const gray = 8 + (index - 232) * 10;
183
+ return { r: gray, g: gray, b: gray };
184
+ }
185
+ const cubeIndex = Math.max(0, index - 16);
186
+ const redIndex = Math.floor(cubeIndex / 36);
187
+ const greenIndex = Math.floor((cubeIndex % 36) / 6);
188
+ const blueIndex = cubeIndex % 6;
189
+ return { r: CUBE_VALUES[redIndex] ?? 0, g: CUBE_VALUES[greenIndex] ?? 0, b: CUBE_VALUES[blueIndex] ?? 0 };
190
+ }
191
+
192
+ const _ANSI_16_RGB: ReadonlyArray<readonly [number, number, number]> = [
193
+ [0, 0, 0],
194
+ [128, 0, 0],
195
+ [0, 128, 0],
196
+ [128, 128, 0],
197
+ [0, 0, 128],
198
+ [128, 0, 128],
199
+ [0, 128, 128],
200
+ [192, 192, 192],
201
+ [128, 128, 128],
202
+ [255, 0, 0],
203
+ [0, 255, 0],
204
+ [255, 255, 0],
205
+ [0, 0, 255],
206
+ [255, 0, 255],
207
+ [0, 255, 255],
208
+ [255, 255, 255],
209
+ ] as const;
210
+
211
+ function colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
212
+ const dr = r1 - r2;
213
+ const dg = g1 - g2;
214
+ const db = b1 - b2;
215
+ return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;
216
+ }
217
+
218
+ function findClosestCubeIndex(value: number): number {
219
+ let minDist = Number.POSITIVE_INFINITY;
220
+ let minIdx = 0;
221
+ for (let i = 0; i < CUBE_VALUES.length; i++) {
222
+ const dist = Math.abs(value - (CUBE_VALUES[i] ?? 0));
223
+ if (dist < minDist) {
224
+ minDist = dist;
225
+ minIdx = i;
226
+ }
227
+ }
228
+ return minIdx;
229
+ }
230
+
231
+ function findClosestGrayIndex(gray: number): number {
232
+ let minDist = Number.POSITIVE_INFINITY;
233
+ let minIdx = 0;
234
+ for (let i = 0; i < GRAY_VALUES.length; i++) {
235
+ const dist = Math.abs(gray - (GRAY_VALUES[i] ?? 0));
236
+ if (dist < minDist) {
237
+ minDist = dist;
238
+ minIdx = i;
239
+ }
240
+ }
241
+ return minIdx;
242
+ }
243
+
244
+ function rgbTo256(r: number, g: number, b: number): number {
245
+ const rIdx = findClosestCubeIndex(r);
246
+ const gIdx = findClosestCubeIndex(g);
247
+ const bIdx = findClosestCubeIndex(b);
248
+ const cubeR = CUBE_VALUES[rIdx] ?? 0;
249
+ const cubeG = CUBE_VALUES[gIdx] ?? 0;
250
+ const cubeB = CUBE_VALUES[bIdx] ?? 0;
251
+ const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;
252
+ const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);
253
+
254
+ const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
255
+ const grayIdx = findClosestGrayIndex(gray);
256
+ const grayValue = GRAY_VALUES[grayIdx] ?? 0;
257
+ const grayIndex = 232 + grayIdx;
258
+ const grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue);
259
+
260
+ const spread = Math.max(r, g, b) - Math.min(r, g, b);
261
+ if (spread < 10 && grayDist < cubeDist) return grayIndex;
262
+ return cubeIndex;
263
+ }
264
+
265
+ /** Color-mode-aware escape prefix/suffix pairs for a hex color (cached per mode+hex). */
266
+ function colorEscapes(mode: string, hex: string, kind: "fg" | "bg"): { prefix: string; suffix: string } | undefined {
267
+ if (!isHexColor(hex)) return undefined;
268
+ const { r, g, b } = hexToRgb(hex);
269
+ const cacheKey = `${mode}:${kind}:${hex}`;
270
+ const cache = kind === "fg" ? fgEscapeCache : bgEscapeCache;
271
+ let cached = cache.get(cacheKey);
272
+ if (!cached) {
273
+ if (mode === "256color") {
274
+ const idx = rgbTo256(r, g, b);
275
+ cached = {
276
+ prefix: `\x1b[${kind === "fg" ? 38 : 48};5;${idx}m`,
277
+ suffix: kind === "fg" ? "\x1b[39m" : RESET_BACKGROUND,
278
+ };
279
+ } else {
280
+ cached = {
281
+ prefix: `\x1b[${kind === "fg" ? 38 : 48};2;${r};${g};${b}m`,
282
+ suffix: kind === "fg" ? "\x1b[39m" : RESET_BACKGROUND,
283
+ };
284
+ }
285
+ cache.set(cacheKey, cached);
286
+ }
287
+ return cached;
288
+ }
289
+
290
+ const fgEscapeCache = new Map<string, { prefix: string; suffix: string }>();
291
+ const bgEscapeCache = new Map<string, { prefix: string; suffix: string }>();
292
+
293
+ /** Foreground-styling helper: accepts a theme (for getColorMode) or "truecolor". */
294
+ export function fgHex(theme: { getColorMode?: () => string } | undefined, hex: string, text: string): string {
295
+ const mode = typeof theme?.getColorMode === "function" ? theme.getColorMode() : "truecolor";
296
+ const escapes = colorEscapes(mode, hex, "fg");
297
+ if (!escapes) return text;
298
+ return `${escapes.prefix}${text}${escapes.suffix}`;
299
+ }
300
+
301
+ /** Background ANSI prefix for a hex color, or "" when not a hex color. */
302
+ export function bgHexAnsi(theme: { getColorMode?: () => string } | undefined, hex: string): string {
303
+ const mode = typeof theme?.getColorMode === "function" ? theme.getColorMode() : "truecolor";
304
+ return colorEscapes(mode, hex, "bg")?.prefix ?? "";
305
+ }
306
+
307
+ export function bgHex(theme: { getColorMode?: () => string } | undefined, hex: string, text: string): string {
308
+ const mode = typeof theme?.getColorMode === "function" ? theme.getColorMode() : "truecolor";
309
+ const escapes = colorEscapes(mode, hex, "bg");
310
+ if (!escapes) return text;
311
+ return `${escapes.prefix}${text}${escapes.suffix}`;
312
+ }
313
+
314
+ /** Whether an SGR code sequence contains a background-affecting action (set/reset). */
315
+ function finalBackgroundAction(rawCodes: string): "none" | "reset" | "set" {
316
+ const codes = rawCodes.split(";").filter(Boolean);
317
+ if (codes.length === 0) return "reset";
318
+ let action: "none" | "reset" | "set" = "none";
319
+ const sgrColorParameterEnd = (index: number): number => {
320
+ const code = Number(codes[index]);
321
+ if (code !== 38 && code !== 48) return index;
322
+ const mode = Number(codes[index + 1]);
323
+ if (mode === 2) return Math.min(codes.length - 1, index + 4);
324
+ if (mode === 5) return Math.min(codes.length - 1, index + 2);
325
+ return index;
326
+ };
327
+ for (let i = 0; i < codes.length; i++) {
328
+ const code = Number(codes[i]);
329
+ if (code === 0 || code === 49) {
330
+ action = "reset";
331
+ continue;
332
+ }
333
+ if (code === 48) {
334
+ action = "set";
335
+ i = sgrColorParameterEnd(i);
336
+ continue;
337
+ }
338
+ if (code === 38) {
339
+ i = sgrColorParameterEnd(i);
340
+ continue;
341
+ }
342
+ if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) action = "set";
343
+ }
344
+ return action;
345
+ }
346
+
347
+ function removeStandaloneBackgroundReset(rawCodes: string): string {
348
+ const codes = rawCodes.split(";").filter(Boolean);
349
+ if (codes.length === 0) return "0";
350
+ const rebuilt: string[] = [];
351
+ const sgrColorParameterEnd = (index: number): number => {
352
+ const code = Number(codes[index]);
353
+ if (code !== 38 && code !== 48) return index;
354
+ const mode = Number(codes[index + 1]);
355
+ if (mode === 2) return Math.min(codes.length - 1, index + 4);
356
+ if (mode === 5) return Math.min(codes.length - 1, index + 2);
357
+ return index;
358
+ };
359
+ for (let i = 0; i < codes.length; i++) {
360
+ const code = Number(codes[i]);
361
+ if (code === 49) continue;
362
+ const end = sgrColorParameterEnd(i);
363
+ for (let j = i; j <= end; j++) rebuilt.push(codes[j] ?? "");
364
+ i = end;
365
+ }
366
+ return rebuilt.join(";");
367
+ }
368
+
369
+ /** Re-apply a background after reset sequences so a fill survives nested resets. */
370
+ export function keepAnsiBackgroundAcrossResets(text: string, bgAnsi: string): string {
371
+ if (!text) return text;
372
+ return text.replace(new RegExp(`${ESC}\\[([0-9;]*)m`, "g"), (sequence, rawCodes) => {
373
+ const codes = String(rawCodes ?? "");
374
+ if (finalBackgroundAction(codes) !== "reset") return sequence;
375
+ const rebuilt = removeStandaloneBackgroundReset(codes);
376
+ return `${rebuilt ? `\x1b[${rebuilt}m` : ""}${bgAnsi}`;
377
+ });
378
+ }
379
+
380
+ /** Wrap text in a background fill that survives nested resets; optional fill-to-end-of-line. */
381
+ export function wrapAnsiBackground(text: string, bgAnsi: string, options: { fillToEnd?: boolean } = {}): string {
382
+ if (!bgAnsi || bgAnsi === RESET_BACKGROUND) return text;
383
+ const body = keepAnsiBackgroundAcrossResets(text, bgAnsi);
384
+ const fill = options.fillToEnd ? `${bgAnsi}${ERASE_TO_END_OF_LINE}` : "";
385
+ return `${bgAnsi}${body}${fill}${RESET_BACKGROUND}`;
386
+ }