@underactive/pi-topping-statusline 0.2.4 → 0.2.6
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 +3 -0
- package/package.json +4 -4
- package/src/nvidia-green.ts +1 -3
- package/src/rainbow.ts +7 -3
- package/src/tests/color-mode.test.ts +109 -0
- package/src/theme.ts +24 -11
package/README.md
CHANGED
|
@@ -21,6 +21,9 @@ Then `/reload` (or restart pi). Requirements: a [Nerd Font](https://www.nerdfont
|
|
|
21
21
|
truecolor terminal for the default look — the Symbols setting in
|
|
22
22
|
`/topping-statusline-settings` (`unicode` or `ascii`) drops the font requirement.
|
|
23
23
|
|
|
24
|
+
The bar's color depth follows pi's terminal detection, so `PI_TRUE_COLOR` and the
|
|
25
|
+
`terminal.trueColor` setting pick 24-bit or 256-color output for it too.
|
|
26
|
+
|
|
24
27
|
## Configuration — `/topping-statusline-settings`
|
|
25
28
|
|
|
26
29
|
`/topping-statusline-settings` opens a settings TUI with a live preview of the box's top and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping-statusline",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "oh-my-pi's powerline statusline as a pi extension with some customizations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"@earendil-works/pi-tui": "*"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-ai": "0.
|
|
49
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
50
|
-
"@earendil-works/pi-tui": "0.
|
|
48
|
+
"@earendil-works/pi-ai": "1.0.3",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "1.0.3",
|
|
50
|
+
"@earendil-works/pi-tui": "1.0.3",
|
|
51
51
|
"@types/node": "^22.0.0",
|
|
52
52
|
"typescript": "^5.7.0"
|
|
53
53
|
}
|
package/src/nvidia-green.ts
CHANGED
|
@@ -41,11 +41,9 @@ function buildAnsiByShade(mode: ColorMode): string[] {
|
|
|
41
41
|
|
|
42
42
|
export class NvidiaGreenBorder implements BorderColorizer {
|
|
43
43
|
#phaseDeg: number;
|
|
44
|
-
readonly #ansiByShade: string[];
|
|
45
44
|
|
|
46
45
|
constructor(phaseDeg = 0) {
|
|
47
46
|
this.#phaseDeg = wrapDeg(phaseDeg);
|
|
48
|
-
this.#ansiByShade = buildAnsiByShade(detectColorMode());
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
step(deltaDeg: number): void {
|
|
@@ -61,6 +59,6 @@ export class NvidiaGreenBorder implements BorderColorizer {
|
|
|
61
59
|
const u = (1 - Math.cos(2 * Math.PI * t)) / 2;
|
|
62
60
|
const eased = u ** GREEN_BIAS;
|
|
63
61
|
const shadeIndex = Math.round((1 - eased) * (GREEN_SHADE_COUNT - 1));
|
|
64
|
-
return
|
|
62
|
+
return buildAnsiByShade(detectColorMode())[shadeIndex];
|
|
65
63
|
}
|
|
66
64
|
}
|
package/src/rainbow.ts
CHANGED
|
@@ -80,12 +80,11 @@ export interface BorderColorizer {
|
|
|
80
80
|
*/
|
|
81
81
|
export class RainbowBorder implements BorderColorizer {
|
|
82
82
|
#phaseDeg: number;
|
|
83
|
-
|
|
83
|
+
#mode: ColorMode | undefined;
|
|
84
84
|
readonly #ansiByHue: string[] = [];
|
|
85
85
|
|
|
86
86
|
constructor(phaseDeg = 0) {
|
|
87
87
|
this.#phaseDeg = wrapDeg(phaseDeg);
|
|
88
|
-
this.#mode = detectColorMode();
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
/** Advance the whole spectrum by `deltaDeg` (mod 360). */
|
|
@@ -99,12 +98,17 @@ export class RainbowBorder implements BorderColorizer {
|
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
prefix(perimeterPos: number, perimeter: number): string {
|
|
101
|
+
const mode = detectColorMode();
|
|
102
|
+
if (mode !== this.#mode) {
|
|
103
|
+
this.#mode = mode;
|
|
104
|
+
this.#ansiByHue.length = 0;
|
|
105
|
+
}
|
|
102
106
|
const hue = (this.#phaseDeg + (360 * perimeterPos) / perimeter) % 360;
|
|
103
107
|
const bucket = Math.round(hue) % 360;
|
|
104
108
|
let ansi = this.#ansiByHue[bucket];
|
|
105
109
|
if (ansi === undefined) {
|
|
106
110
|
const hex = rgbToHex(hslToRgb(bucket, SATURATION, LIGHTNESS));
|
|
107
|
-
ansi = hexToFgAnsi(hex,
|
|
111
|
+
ansi = hexToFgAnsi(hex, mode);
|
|
108
112
|
this.#ansiByHue[bucket] = ansi;
|
|
109
113
|
}
|
|
110
114
|
return ansi;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { resetCapabilitiesCache, setCapabilities, setCapabilityOverrides } from "@earendil-works/pi-tui";
|
|
4
|
+
import { NvidiaGreenBorder } from "../nvidia-green.ts";
|
|
5
|
+
import { RainbowBorder } from "../rainbow.ts";
|
|
6
|
+
import { detectColorMode, hexToFgAnsi, theme } from "../theme.ts";
|
|
7
|
+
|
|
8
|
+
function forceTrueColor(trueColor: boolean): void {
|
|
9
|
+
setCapabilities({ images: null, trueColor, hyperlinks: false });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resetCapabilities(): void {
|
|
13
|
+
setCapabilityOverrides({});
|
|
14
|
+
resetCapabilitiesCache();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("forced 256-color capabilities select 256-color escapes", () => {
|
|
18
|
+
try {
|
|
19
|
+
forceTrueColor(false);
|
|
20
|
+
assert.equal(detectColorMode(), "256color");
|
|
21
|
+
assert.equal(hexToFgAnsi("#ff0000"), "\x1b[38;5;196m");
|
|
22
|
+
} finally {
|
|
23
|
+
resetCapabilities();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("forced truecolor capabilities select 24-bit escapes", () => {
|
|
28
|
+
try {
|
|
29
|
+
forceTrueColor(true);
|
|
30
|
+
assert.equal(detectColorMode(), "truecolor");
|
|
31
|
+
assert.equal(hexToFgAnsi("#ff0000"), "\x1b[38;2;255;0;0m");
|
|
32
|
+
} finally {
|
|
33
|
+
resetCapabilities();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("the shared theme invalidates cached escapes when the mode changes", () => {
|
|
38
|
+
try {
|
|
39
|
+
forceTrueColor(true);
|
|
40
|
+
assert.match(theme.getFgAnsi("statusLineBg"), /^\x1b\[38;2;/);
|
|
41
|
+
assert.match(theme.getBgAnsi(), /^\x1b\[48;2;/);
|
|
42
|
+
|
|
43
|
+
forceTrueColor(false);
|
|
44
|
+
assert.match(theme.getFgAnsi("statusLineBg"), /^\x1b\[38;5;/);
|
|
45
|
+
assert.match(theme.getBgAnsi(), /^\x1b\[48;5;/);
|
|
46
|
+
assert.match(theme.fadeFg("warning", "statusLineBg", 2, "x"), /^\x1b\[38;5;/);
|
|
47
|
+
assert.match(theme.fadeAnsi("\x1b[38;2;254;188;56mA\x1b[39m", 0.5), /^\x1b\[38;5;/);
|
|
48
|
+
|
|
49
|
+
forceTrueColor(true);
|
|
50
|
+
assert.match(theme.getFgAnsi("statusLineBg"), /^\x1b\[38;2;/);
|
|
51
|
+
} finally {
|
|
52
|
+
resetCapabilities();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("session-long border colorizers follow mode changes", () => {
|
|
57
|
+
try {
|
|
58
|
+
forceTrueColor(true);
|
|
59
|
+
const rainbow = new RainbowBorder(0);
|
|
60
|
+
const nvidia = new NvidiaGreenBorder(0);
|
|
61
|
+
assert.match(rainbow.colorChar("─", 0, 28), /^\x1b\[38;2;/);
|
|
62
|
+
assert.match(nvidia.colorChar("─", 0, 28), /^\x1b\[38;2;/);
|
|
63
|
+
|
|
64
|
+
forceTrueColor(false);
|
|
65
|
+
assert.match(rainbow.colorChar("─", 0, 28), /^\x1b\[38;5;/);
|
|
66
|
+
assert.match(nvidia.colorChar("─", 0, 28), /^\x1b\[38;5;/);
|
|
67
|
+
|
|
68
|
+
forceTrueColor(true);
|
|
69
|
+
assert.match(rainbow.colorChar("─", 0, 28), /^\x1b\[38;2;/);
|
|
70
|
+
assert.match(nvidia.colorChar("─", 0, 28), /^\x1b\[38;2;/);
|
|
71
|
+
} finally {
|
|
72
|
+
resetCapabilities();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("pi capability overrides take precedence over terminal hints", () => {
|
|
77
|
+
const previousColorTerm = process.env.COLORTERM;
|
|
78
|
+
try {
|
|
79
|
+
process.env.COLORTERM = "truecolor";
|
|
80
|
+
setCapabilityOverrides({ trueColor: false });
|
|
81
|
+
assert.equal(detectColorMode(), "256color");
|
|
82
|
+
assert.match(theme.getFgAnsi("statusLineBg"), /^\x1b\[38;5;/);
|
|
83
|
+
} finally {
|
|
84
|
+
if (previousColorTerm === undefined) delete process.env.COLORTERM;
|
|
85
|
+
else process.env.COLORTERM = previousColorTerm;
|
|
86
|
+
resetCapabilities();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("PI_TRUE_COLOR controls capability detection", () => {
|
|
91
|
+
const previousTrueColor = process.env.PI_TRUE_COLOR;
|
|
92
|
+
const previousHyperlinks = process.env.PI_HYPERLINKS;
|
|
93
|
+
try {
|
|
94
|
+
process.env.PI_HYPERLINKS = "0";
|
|
95
|
+
process.env.PI_TRUE_COLOR = "0";
|
|
96
|
+
resetCapabilitiesCache();
|
|
97
|
+
assert.equal(detectColorMode(), "256color");
|
|
98
|
+
|
|
99
|
+
process.env.PI_TRUE_COLOR = "1";
|
|
100
|
+
resetCapabilitiesCache();
|
|
101
|
+
assert.equal(detectColorMode(), "truecolor");
|
|
102
|
+
} finally {
|
|
103
|
+
if (previousTrueColor === undefined) delete process.env.PI_TRUE_COLOR;
|
|
104
|
+
else process.env.PI_TRUE_COLOR = previousTrueColor;
|
|
105
|
+
if (previousHyperlinks === undefined) delete process.env.PI_HYPERLINKS;
|
|
106
|
+
else process.env.PI_HYPERLINKS = previousHyperlinks;
|
|
107
|
+
resetCapabilities();
|
|
108
|
+
}
|
|
109
|
+
});
|
package/src/theme.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* embedded here. Values are hex strings (truecolor), 256-palette indices
|
|
9
9
|
* (numbers), or "" for the terminal default color.
|
|
10
10
|
*/
|
|
11
|
+
import { getTerminalColorMode } from "@earendil-works/pi-tui";
|
|
11
12
|
|
|
12
13
|
export type SymbolPreset = "unicode" | "nerd" | "ascii";
|
|
13
14
|
|
|
@@ -230,13 +231,9 @@ export const MAJOR_COLOR_HEXES = [
|
|
|
230
231
|
|
|
231
232
|
export type ColorMode = "truecolor" | "256color";
|
|
232
233
|
|
|
234
|
+
/** Defer to pi's cached terminal detection, which honors PI_TRUE_COLOR, terminal.trueColor, and capability overrides. */
|
|
233
235
|
export function detectColorMode(): ColorMode {
|
|
234
|
-
|
|
235
|
-
if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor";
|
|
236
|
-
if (process.env.WT_SESSION) return "truecolor";
|
|
237
|
-
const term = process.env.TERM || "";
|
|
238
|
-
if (term === "dumb" || term === "" || term === "linux") return "256color";
|
|
239
|
-
return "truecolor";
|
|
236
|
+
return getTerminalColorMode();
|
|
240
237
|
}
|
|
241
238
|
|
|
242
239
|
export function parseHex(hex: string): [number, number, number] | undefined {
|
|
@@ -314,12 +311,26 @@ function bgAnsi(color: ColorValue, mode: ColorMode): string {
|
|
|
314
311
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
315
312
|
|
|
316
313
|
class StatusTheme {
|
|
317
|
-
#mode: ColorMode
|
|
314
|
+
#mode: ColorMode | undefined;
|
|
318
315
|
#preset: SymbolPreset = "nerd";
|
|
319
316
|
#symbols: StatusSymbols = NERD_SYMBOLS;
|
|
320
317
|
#fgCache = new Map<StatusColor, string>();
|
|
321
318
|
#bgAnsi: string | undefined;
|
|
322
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Pi imports extensions before it applies terminal.trueColor and reapplies
|
|
322
|
+
* overrides when settings change, so resolve the mode on use and drop stale ANSI.
|
|
323
|
+
*/
|
|
324
|
+
#currentMode(): ColorMode {
|
|
325
|
+
const mode = detectColorMode();
|
|
326
|
+
if (mode !== this.#mode) {
|
|
327
|
+
this.#mode = mode;
|
|
328
|
+
this.#fgCache.clear();
|
|
329
|
+
this.#bgAnsi = undefined;
|
|
330
|
+
}
|
|
331
|
+
return mode;
|
|
332
|
+
}
|
|
333
|
+
|
|
323
334
|
setSymbolPreset(preset: SymbolPreset): SymbolPreset {
|
|
324
335
|
const previous = this.#preset;
|
|
325
336
|
this.#preset = preset;
|
|
@@ -345,16 +356,18 @@ class StatusTheme {
|
|
|
345
356
|
}
|
|
346
357
|
|
|
347
358
|
getFgAnsi(color: StatusColor): string {
|
|
359
|
+
const mode = this.#currentMode();
|
|
348
360
|
let ansi = this.#fgCache.get(color);
|
|
349
361
|
if (ansi === undefined) {
|
|
350
|
-
ansi = fgAnsi(FG_COLORS[color],
|
|
362
|
+
ansi = fgAnsi(FG_COLORS[color], mode);
|
|
351
363
|
this.#fgCache.set(color, ansi);
|
|
352
364
|
}
|
|
353
365
|
return ansi;
|
|
354
366
|
}
|
|
355
367
|
|
|
356
368
|
getBgAnsi(): string {
|
|
357
|
-
|
|
369
|
+
const mode = this.#currentMode();
|
|
370
|
+
if (this.#bgAnsi === undefined) this.#bgAnsi = bgAnsi(STATUS_LINE_BG, mode);
|
|
358
371
|
return this.#bgAnsi;
|
|
359
372
|
}
|
|
360
373
|
|
|
@@ -376,7 +389,7 @@ class StatusTheme {
|
|
|
376
389
|
const r = Math.round(a[0] * (1 - eased) + b[0] * eased);
|
|
377
390
|
const g = Math.round(a[1] * (1 - eased) + b[1] * eased);
|
|
378
391
|
const bl = Math.round(a[2] * (1 - eased) + b[2] * eased);
|
|
379
|
-
const ansi = rgbToFgAnsi(r, g, bl, this.#
|
|
392
|
+
const ansi = rgbToFgAnsi(r, g, bl, this.#currentMode());
|
|
380
393
|
return `${ansi}${text}\x1b[39m`;
|
|
381
394
|
}
|
|
382
395
|
|
|
@@ -392,7 +405,7 @@ class StatusTheme {
|
|
|
392
405
|
const alpha = Math.max(0, Math.min(1, opacity));
|
|
393
406
|
if (alpha >= 1) return text;
|
|
394
407
|
const target = colorValueToRgb(STATUS_LINE_BG);
|
|
395
|
-
const mode = this.#
|
|
408
|
+
const mode = this.#currentMode();
|
|
396
409
|
return text.replace(SGR_COLOR_RE, (_m, tcPlane, r, g, b, ixPlane, index) => {
|
|
397
410
|
let plane = "38";
|
|
398
411
|
let rgb: [number, number, number] = DEFAULT_FG_RGB;
|