@pi-unipi/unipi 2.0.4 → 2.0.5
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 +2 -2
- package/package.json +1 -1
- package/packages/autocomplete/README.md +1 -0
- package/packages/autocomplete/src/provider.ts +17 -2
- package/packages/footer/README.md +12 -0
- package/packages/footer/src/config.ts +9 -1
- package/packages/footer/src/rendering/renderer.ts +14 -1
- package/packages/footer/src/rendering/theme.ts +187 -6
- package/packages/footer/src/types.ts +5 -0
- package/packages/utility/README.md +3 -0
- package/packages/utility/src/display/capabilities.ts +29 -15
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ pi install npm:@pi-unipi/unipi
|
|
|
27
27
|
|
|
28
28
|
**[Notify](./packages/notify/README.md)** — Push notifications to native OS, Gotify, Telegram, or ntfy. Per-event platform routing plus native focus suppression so alerts can stay quiet while Pi is already focused.
|
|
29
29
|
|
|
30
|
-
**[Footer](./packages/footer/README.md)** — Persistent status bar showing live stats from every package. Responsive layout, presets, per-segment toggling.
|
|
30
|
+
**[Footer](./packages/footer/README.md)** — Persistent status bar showing live stats from every package. Responsive layout, presets, per-segment toggling, and terminal-aware color fallback.
|
|
31
31
|
|
|
32
32
|
**[BTW](./packages/btw/README.md)** — Side conversations that run in parallel. Ask questions without interrupting the main agent.
|
|
33
33
|
|
|
@@ -45,7 +45,7 @@ pi install npm:@pi-unipi/unipi
|
|
|
45
45
|
|
|
46
46
|
**[Input Shortcuts](./packages/input-shortcuts/README.md)** — Keyboard shortcuts via vim-style chord overlay. Stash/restore, undo/redo, clipboard, thinking toggle.
|
|
47
47
|
|
|
48
|
-
**[Command Enchantment](./packages/autocomplete/README.md)** — Enhanced `/unipi:*` autocomplete with package
|
|
48
|
+
**[Command Enchantment](./packages/autocomplete/README.md)** — Enhanced `/unipi:*` autocomplete with full command names, package tags, descriptions, colors, and registry audits that catch stale command docs before release.
|
|
49
49
|
|
|
50
50
|
## Architecture
|
|
51
51
|
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ Command Enchantment has no user commands. It improves the editor autocomplete ex
|
|
|
9
9
|
## What It Does
|
|
10
10
|
|
|
11
11
|
- Groups `/unipi:*` commands by package so workflow, memory, web, footer, and other commands are visually distinct.
|
|
12
|
+
- Shows full command values like `unipi:brainstorm` while replacing Pi source tags with concise package tags like `[workflow]`.
|
|
12
13
|
- Sorts matches in predictable tiers: exact Unipi matches first, then other Unipi matches, then system commands.
|
|
13
14
|
- Preserves dynamic argument completions from command providers, including workflow document and worktree suggestions.
|
|
14
15
|
- Ships an audit test that checks registered Unipi commands are represented in the autocomplete registry and have descriptions.
|
|
@@ -43,6 +43,18 @@ function fuzzyMatch(text: string, query: string): boolean {
|
|
|
43
43
|
|
|
44
44
|
// ─── Namespace detection ─────────────────────────────────────────────
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Pi prefixes extension command descriptions with source tags such as
|
|
48
|
+
* `[u:npm:@pi-unipi/unipi]`. The enchanted provider replaces that with
|
|
49
|
+
* package tags (`[workflow]`, `[memory]`, …), so strip the source tag from
|
|
50
|
+
* base descriptions before reusing them.
|
|
51
|
+
*/
|
|
52
|
+
function stripPiSourceTag(description: string): string {
|
|
53
|
+
return description
|
|
54
|
+
.replace(/^\[(?:[upt])(?::[^\]]+)?\]\s*/, "")
|
|
55
|
+
.trimStart();
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
/**
|
|
47
59
|
* If the query looks like a package namespace (e.g. "workflow", "memory",
|
|
48
60
|
* "utility"), return that package name so its commands sort to the top.
|
|
@@ -174,7 +186,7 @@ function getEnhancedUnipiItems(
|
|
|
174
186
|
|
|
175
187
|
return {
|
|
176
188
|
value: cmd,
|
|
177
|
-
label: cmd
|
|
189
|
+
label: cmd,
|
|
178
190
|
description: desc ? `${tag} ${desc}` : tag,
|
|
179
191
|
};
|
|
180
192
|
});
|
|
@@ -261,7 +273,10 @@ export function createEnchantedProvider(
|
|
|
261
273
|
for (const item of baseSuggestions.items) {
|
|
262
274
|
if (item.value.startsWith("unipi:")) {
|
|
263
275
|
if (item.description) {
|
|
264
|
-
|
|
276
|
+
const cleanDescription = stripPiSourceTag(item.description);
|
|
277
|
+
if (cleanDescription) {
|
|
278
|
+
descriptionOverrides.set(item.value, cleanDescription);
|
|
279
|
+
}
|
|
265
280
|
}
|
|
266
281
|
} else {
|
|
267
282
|
nonUnipiItems.push(item);
|
|
@@ -69,6 +69,7 @@ Settings in `~/.pi/agent/settings.json` under `unipi.footer`:
|
|
|
69
69
|
"preset": "default",
|
|
70
70
|
"separator": "powerline-thin",
|
|
71
71
|
"iconStyle": "nerd",
|
|
72
|
+
"colorMode": "auto",
|
|
72
73
|
"groups": {
|
|
73
74
|
"compactor": {
|
|
74
75
|
"show": true,
|
|
@@ -105,6 +106,17 @@ Settings in `~/.pi/agent/settings.json` under `unipi.footer`:
|
|
|
105
106
|
|
|
106
107
|
When `iconStyle` is not set, footer auto-detects Nerd Font support and defaults to `nerd` if available, `emoji` otherwise.
|
|
107
108
|
|
|
109
|
+
### Color Mode
|
|
110
|
+
|
|
111
|
+
| Mode | Description |
|
|
112
|
+
|------|-------------|
|
|
113
|
+
| `auto` | Detect terminal support from environment (default) |
|
|
114
|
+
| `truecolor` | Force 24-bit ANSI color |
|
|
115
|
+
| `256` | Force xterm-256 color fallback |
|
|
116
|
+
| `none` | Disable footer color escapes |
|
|
117
|
+
|
|
118
|
+
`auto` uses truecolor where supported and downgrades to xterm-256 colors for terminals such as Apple Terminal that do not reliably render 24-bit color escapes.
|
|
119
|
+
|
|
108
120
|
### Responsive Layout
|
|
109
121
|
|
|
110
122
|
```
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import * as os from "node:os";
|
|
11
|
-
import type { FooterSettings, FooterGroupSettings, SeparatorStyle, IconStyle } from "./types.js";
|
|
11
|
+
import type { FooterSettings, FooterGroupSettings, SeparatorStyle, IconStyle, ColorMode } from "./types.js";
|
|
12
12
|
import { UNIPI_SETTINGS_KEY } from "@pi-unipi/core";
|
|
13
13
|
|
|
14
14
|
/** Default footer settings */
|
|
@@ -19,6 +19,7 @@ export const DEFAULT_FOOTER_SETTINGS: FooterSettings = {
|
|
|
19
19
|
iconStyle: "nerd",
|
|
20
20
|
zoneSeparator: "\u2502", // │
|
|
21
21
|
showFullLabels: false,
|
|
22
|
+
colorMode: "auto",
|
|
22
23
|
groups: {
|
|
23
24
|
core: { show: true, segments: {} },
|
|
24
25
|
compactor: { show: true, segments: {} },
|
|
@@ -96,6 +97,7 @@ export function loadFooterSettings(): FooterSettings {
|
|
|
96
97
|
iconStyle: isValidIconStyle(footer.iconStyle) ? footer.iconStyle as IconStyle : DEFAULT_FOOTER_SETTINGS.iconStyle,
|
|
97
98
|
zoneSeparator: typeof footer.zoneSeparator === "string" ? footer.zoneSeparator : DEFAULT_FOOTER_SETTINGS.zoneSeparator,
|
|
98
99
|
showFullLabels: typeof footer.showFullLabels === "boolean" ? footer.showFullLabels : DEFAULT_FOOTER_SETTINGS.showFullLabels,
|
|
100
|
+
colorMode: isValidColorMode(footer.colorMode) ? footer.colorMode as ColorMode : DEFAULT_FOOTER_SETTINGS.colorMode,
|
|
99
101
|
groups: mergeGroupSettings(
|
|
100
102
|
DEFAULT_FOOTER_SETTINGS.groups,
|
|
101
103
|
footer.groups as Record<string, FooterGroupSettings> | undefined,
|
|
@@ -170,6 +172,12 @@ function isValidIconStyle(value: unknown): boolean {
|
|
|
170
172
|
return valid.includes(value);
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
function isValidColorMode(value: unknown): boolean {
|
|
176
|
+
if (typeof value !== "string") return false;
|
|
177
|
+
const valid: string[] = ["auto", "truecolor", "256", "none"];
|
|
178
|
+
return valid.includes(value);
|
|
179
|
+
}
|
|
180
|
+
|
|
173
181
|
function mergeGroupSettings(
|
|
174
182
|
defaults: Record<string, FooterGroupSettings>,
|
|
175
183
|
overrides: Record<string, FooterGroupSettings> | undefined,
|
|
@@ -11,7 +11,7 @@ import type { PresetDef, FooterSegmentContext, FooterSegment, ColorScheme, Rende
|
|
|
11
11
|
import type { FooterRegistry } from "../registry/index.js";
|
|
12
12
|
import { visibleWidth as piVisibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
13
13
|
import { getSeparator, separatorVisibleWidth } from "./separators.js";
|
|
14
|
-
import { getDefaultColors } from "./theme.js";
|
|
14
|
+
import { getDefaultColors, setColorMode, refreshColorMode } from "./theme.js";
|
|
15
15
|
import { setIconStyle } from "./icons.js";
|
|
16
16
|
import { getPreset } from "../presets.js";
|
|
17
17
|
import { isSegmentEnabled, isSegmentExplicitlyEnabled, loadFooterSettings } from "../config.js";
|
|
@@ -104,6 +104,19 @@ export class FooterRenderer {
|
|
|
104
104
|
private syncIconStyle(): void {
|
|
105
105
|
const settings = loadFooterSettings();
|
|
106
106
|
setIconStyle(settings.iconStyle);
|
|
107
|
+
this.syncColorMode(settings);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Sync the colour-emission mode from settings to the theme module. */
|
|
111
|
+
private syncColorMode(settings?: ReturnType<typeof loadFooterSettings>): void {
|
|
112
|
+
const s = settings ?? loadFooterSettings();
|
|
113
|
+
const mode = s.colorMode ?? "auto";
|
|
114
|
+
if (mode === "auto") {
|
|
115
|
+
setColorMode(null);
|
|
116
|
+
refreshColorMode();
|
|
117
|
+
} else {
|
|
118
|
+
setColorMode(mode);
|
|
119
|
+
}
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/** Get the active preset name */
|
|
@@ -3,11 +3,174 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Maps semantic color names to pi theme colors. Supports
|
|
5
5
|
* hex overrides via ColorScheme.
|
|
6
|
+
*
|
|
7
|
+
* Color emission is capability-aware: hex values are emitted as 24-bit
|
|
8
|
+
* truecolor on terminals that advertise truecolor (COLORTERM or known
|
|
9
|
+
* truecolor TERM_PROGRAM), and gracefully downgraded to the nearest
|
|
10
|
+
* xterm-256 index on legacy terminals (notably Apple Terminal.app, which
|
|
11
|
+
* silently swallows 24-bit escapes and renders the text uncoloured).
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
9
15
|
import type { ColorScheme, ColorValue, SemanticColor, ThemeLike } from "../types.js";
|
|
10
16
|
|
|
17
|
+
// ─── Color mode detection ──────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detected colour-emission mode for the current terminal.
|
|
21
|
+
* - `truecolor` → emit 24-bit `\x1b[38;2;R;G;Bm`
|
|
22
|
+
* - `256` → emit 8-bit `\x1b[38;5;Nm` (nearest xterm-256 index)
|
|
23
|
+
* - `none` → emit no colour codes (plain text)
|
|
24
|
+
*/
|
|
25
|
+
export type ColorMode = "truecolor" | "256" | "none";
|
|
26
|
+
|
|
27
|
+
/** Manual override (set via `setColorMode`) — wins over env-based detection. */
|
|
28
|
+
let manualMode: ColorMode | null = null;
|
|
29
|
+
/** Cached detection result; cleared by `refreshColorMode()`. */
|
|
30
|
+
let detectedMode: ColorMode | null = null;
|
|
31
|
+
|
|
32
|
+
/** Terminals known to honour 24-bit truecolor escapes. */
|
|
33
|
+
const TRUECOLOR_TERM_PROGRAMS = [
|
|
34
|
+
"iTerm.app",
|
|
35
|
+
"WezTerm",
|
|
36
|
+
"Alacritty",
|
|
37
|
+
"vscode",
|
|
38
|
+
"Hyper",
|
|
39
|
+
"Warp",
|
|
40
|
+
"ghostty",
|
|
41
|
+
"Ghostty",
|
|
42
|
+
"zed",
|
|
43
|
+
"Zed",
|
|
44
|
+
"cursor",
|
|
45
|
+
"Cursor",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detect the colour mode of the current terminal from environment.
|
|
50
|
+
* Respects `NO_COLOR`, `FORCE_COLOR`, then sniffs `COLORTERM` / `TERM_PROGRAM`
|
|
51
|
+
* / `TERM`. Apple Terminal is explicitly capped at 256-colour.
|
|
52
|
+
*/
|
|
53
|
+
function detectColorMode(): ColorMode {
|
|
54
|
+
const env = process.env;
|
|
55
|
+
if (env.NO_COLOR || env.NODE_DISABLE_COLORS) return "none";
|
|
56
|
+
if (env.FORCE_COLOR) {
|
|
57
|
+
const lvl = parseInt(env.FORCE_COLOR, 10);
|
|
58
|
+
if (Number.isNaN(lvl)) return "truecolor"; // any non-numeric truthy value
|
|
59
|
+
if (lvl >= 3) return "truecolor";
|
|
60
|
+
if (lvl >= 1) return "256";
|
|
61
|
+
return "none";
|
|
62
|
+
}
|
|
63
|
+
const term = env.TERM ?? "";
|
|
64
|
+
const termProgram = env.TERM_PROGRAM ?? "";
|
|
65
|
+
// Apple Terminal.app does NOT support 24-bit colour. Force 256 even if
|
|
66
|
+
// some wrapper leaked COLORTERM into the env.
|
|
67
|
+
if (termProgram === "Apple_Terminal") {
|
|
68
|
+
return "256";
|
|
69
|
+
}
|
|
70
|
+
if (env.COLORTERM === "truecolor" || env.COLORTERM === "24bit") {
|
|
71
|
+
return "truecolor";
|
|
72
|
+
}
|
|
73
|
+
if (TRUECOLOR_TERM_PROGRAMS.includes(termProgram)) {
|
|
74
|
+
return "truecolor";
|
|
75
|
+
}
|
|
76
|
+
if (term === "dumb") return "none";
|
|
77
|
+
// Anything that talks ANSI but isn't known-truecolor → 256
|
|
78
|
+
if (term.includes("256color") || term.includes("color") || term.includes("xterm") || term.includes("ansi") || term.includes("screen") || term.includes("tmux")) {
|
|
79
|
+
return "256";
|
|
80
|
+
}
|
|
81
|
+
// Default conservative: 256 colour. Better than dropping colours entirely
|
|
82
|
+
// on unknown terminals that almost certainly understand the 256 palette.
|
|
83
|
+
return "256";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the active colour mode (manual override > cached detection > env probe).
|
|
88
|
+
*/
|
|
89
|
+
export function getColorMode(): ColorMode {
|
|
90
|
+
if (manualMode) return manualMode;
|
|
91
|
+
if (!detectedMode) detectedMode = detectColorMode();
|
|
92
|
+
return detectedMode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Force a specific colour mode (e.g. from user settings). Pass `null` to
|
|
97
|
+
* revert to auto-detection.
|
|
98
|
+
*/
|
|
99
|
+
export function setColorMode(mode: ColorMode | null): void {
|
|
100
|
+
manualMode = mode;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Re-run env-based detection (useful after settings or env changes). */
|
|
104
|
+
export function refreshColorMode(): ColorMode {
|
|
105
|
+
detectedMode = null;
|
|
106
|
+
return getColorMode();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── 24-bit → 256 colour downgrade ─────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Map an RGB triple to the nearest xterm-256 index.
|
|
113
|
+
*
|
|
114
|
+
* xterm-256 layout:
|
|
115
|
+
* 0–15 → system / bright colours (skipped; we route through the cube
|
|
116
|
+
* for predictability)
|
|
117
|
+
* 16–231 → 6×6×6 colour cube
|
|
118
|
+
* 232–255 → 24-step grayscale
|
|
119
|
+
*
|
|
120
|
+
* We pick the better of (cube nearest) and (grayscale nearest) by squared
|
|
121
|
+
* RGB distance, which matches the heuristic used by chalk / ansi-styles.
|
|
122
|
+
*/
|
|
123
|
+
function rgbTo256(r: number, g: number, b: number): number {
|
|
124
|
+
// Grayscale candidate
|
|
125
|
+
const grayAvg = Math.round((r + g + b) / 3);
|
|
126
|
+
let grayIdx: number;
|
|
127
|
+
let grayR: number;
|
|
128
|
+
if (grayAvg < 8) {
|
|
129
|
+
grayIdx = 16;
|
|
130
|
+
grayR = 0;
|
|
131
|
+
} else if (grayAvg > 248) {
|
|
132
|
+
grayIdx = 231;
|
|
133
|
+
grayR = 255;
|
|
134
|
+
} else {
|
|
135
|
+
const step = Math.round(((grayAvg - 8) / 247) * 24);
|
|
136
|
+
grayIdx = 232 + step;
|
|
137
|
+
grayR = 8 + step * 10;
|
|
138
|
+
}
|
|
139
|
+
const grayDist = sqDist(r, g, b, grayR, grayR, grayR);
|
|
140
|
+
|
|
141
|
+
// Cube candidate (6 steps: 0, 95, 135, 175, 215, 255)
|
|
142
|
+
const cubeR = cubeStep(r);
|
|
143
|
+
const cubeG = cubeStep(g);
|
|
144
|
+
const cubeB = cubeStep(b);
|
|
145
|
+
const cubeIdx = 16 + 36 * cubeR.idx + 6 * cubeG.idx + cubeB.idx;
|
|
146
|
+
const cubeDist = sqDist(r, g, b, cubeR.value, cubeG.value, cubeB.value);
|
|
147
|
+
|
|
148
|
+
return cubeDist <= grayDist ? cubeIdx : grayIdx;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const CUBE_STEPS = [0, 95, 135, 175, 215, 255];
|
|
152
|
+
function cubeStep(v: number): { idx: number; value: number } {
|
|
153
|
+
let bestIdx = 0;
|
|
154
|
+
let bestDist = Infinity;
|
|
155
|
+
for (let i = 0; i < CUBE_STEPS.length; i++) {
|
|
156
|
+
const d = Math.abs(v - CUBE_STEPS[i]);
|
|
157
|
+
if (d < bestDist) {
|
|
158
|
+
bestDist = d;
|
|
159
|
+
bestIdx = i;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { idx: bestIdx, value: CUBE_STEPS[bestIdx] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sqDist(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
|
|
166
|
+
const dr = r1 - r2;
|
|
167
|
+
const dg = g1 - g2;
|
|
168
|
+
const db = b1 - b2;
|
|
169
|
+
return dr * dr + dg * dg + db * db;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
11
174
|
/** Wrap text in dim ANSI codes for muted placeholder display */
|
|
12
175
|
export function mutedPlaceholder(text: string): string {
|
|
13
176
|
return `\x1b[2m${text}\x1b[0m`;
|
|
@@ -95,6 +258,26 @@ export function resolveColor(color: ColorValue, theme: ThemeLike): string {
|
|
|
95
258
|
return theme.fg(color as ThemeColor, "").replace(/\x1b\[0m$/, "");
|
|
96
259
|
}
|
|
97
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Wrap `text` in an ANSI escape for the given hex value, chosen to match
|
|
263
|
+
* the active colour mode (truecolor / 256 / none).
|
|
264
|
+
*/
|
|
265
|
+
function wrapHex(hex: string, text: string): string {
|
|
266
|
+
const mode = getColorMode();
|
|
267
|
+
if (mode === "none") return text;
|
|
268
|
+
const h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
269
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
270
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
271
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
272
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return text;
|
|
273
|
+
if (mode === "truecolor") {
|
|
274
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
275
|
+
}
|
|
276
|
+
// 256-color fallback
|
|
277
|
+
const idx = rgbTo256(r, g, b);
|
|
278
|
+
return `\x1b[38;5;${idx}m${text}\x1b[0m`;
|
|
279
|
+
}
|
|
280
|
+
|
|
98
281
|
/**
|
|
99
282
|
* Apply a semantic color to text using the theme.
|
|
100
283
|
* Falls back to the default theme color if no override is provided.
|
|
@@ -109,16 +292,14 @@ export function applyColor(
|
|
|
109
292
|
if (!colorValue) {
|
|
110
293
|
// Use default from the map
|
|
111
294
|
const defaultColor = DEFAULT_COLOR_MAP[semantic] || "text";
|
|
295
|
+
if (typeof defaultColor === "string" && defaultColor.startsWith("#")) {
|
|
296
|
+
return wrapHex(defaultColor, text);
|
|
297
|
+
}
|
|
112
298
|
return theme.fg(defaultColor as ThemeColor, text);
|
|
113
299
|
}
|
|
114
300
|
|
|
115
301
|
if (colorValue.startsWith("#")) {
|
|
116
|
-
|
|
117
|
-
const hex = colorValue.slice(1);
|
|
118
|
-
const r = Number.parseInt(hex.slice(0, 2), 16);
|
|
119
|
-
const g = Number.parseInt(hex.slice(2, 4), 16);
|
|
120
|
-
const b = Number.parseInt(hex.slice(4, 6), 16);
|
|
121
|
-
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
302
|
+
return wrapHex(colorValue, text);
|
|
122
303
|
}
|
|
123
304
|
|
|
124
305
|
return theme.fg(colorValue as ThemeColor, text);
|
|
@@ -82,6 +82,9 @@ export type ColorScheme = Partial<Record<SemanticColor, ColorValue>>;
|
|
|
82
82
|
/** Icon style — determines which icon set is used for segments */
|
|
83
83
|
export type IconStyle = "nerd" | "emoji" | "text";
|
|
84
84
|
|
|
85
|
+
/** Colour-emission mode for terminal output. */
|
|
86
|
+
export type ColorMode = "auto" | "truecolor" | "256" | "none";
|
|
87
|
+
|
|
85
88
|
/** Separator styles for segment dividers */
|
|
86
89
|
export type SeparatorStyle =
|
|
87
90
|
| "powerline"
|
|
@@ -190,6 +193,8 @@ export interface FooterSettings {
|
|
|
190
193
|
zoneSeparator?: string;
|
|
191
194
|
/** Show full labels instead of compact short labels */
|
|
192
195
|
showFullLabels?: boolean;
|
|
196
|
+
/** Terminal colour mode override (default: "auto" — env-detected). */
|
|
197
|
+
colorMode?: ColorMode;
|
|
193
198
|
/** Per-group settings */
|
|
194
199
|
groups: Record<string, FooterGroupSettings>;
|
|
195
200
|
}
|
|
@@ -128,9 +128,12 @@ import { detectCapabilities, getIcon } from "@pi-unipi/utility/display/capabilit
|
|
|
128
128
|
|
|
129
129
|
const caps = detectCapabilities();
|
|
130
130
|
console.log("Nerd Font:", caps.nerdFont);
|
|
131
|
+
console.log("Truecolor:", caps.truecolor);
|
|
131
132
|
console.log(getIcon("", "[OK]")); // Uses Nerd Font if available
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
Capability detection distinguishes basic color, xterm-256-compatible terminals, and truecolor-capable terminals. Apple Terminal is treated as color-capable but not truecolor-capable so renderers can fall back safely.
|
|
136
|
+
|
|
134
137
|
## Privacy
|
|
135
138
|
|
|
136
139
|
The analytics collector is privacy-respecting:
|
|
@@ -42,24 +42,38 @@ function detectColorSupport(): { color: boolean; truecolor: boolean } {
|
|
|
42
42
|
const term = env.TERM || "";
|
|
43
43
|
const termProgram = env.TERM_PROGRAM || "";
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
45
|
+
// Apple Terminal.app does NOT support 24-bit truecolor (longstanding
|
|
46
|
+
// limitation as of macOS 26). Force 256-colour even if some wrapper
|
|
47
|
+
// leaked COLORTERM=truecolor into the environment.
|
|
48
|
+
const isAppleTerminal = termProgram === "Apple_Terminal";
|
|
49
|
+
|
|
50
|
+
// Truecolor TERM_PROGRAM allow-list. Note: terminfo names like
|
|
51
|
+
// "xterm-256color" / "screen-256color" / "tmux-256color" advertise
|
|
52
|
+
// 256-colour, NOT truecolor — they must not appear here.
|
|
53
|
+
const truecolorTermPrograms = [
|
|
54
|
+
"iTerm.app",
|
|
55
|
+
"WezTerm",
|
|
56
|
+
"Alacritty",
|
|
57
|
+
"vscode",
|
|
58
|
+
"Hyper",
|
|
59
|
+
"Warp",
|
|
56
60
|
"ghostty",
|
|
61
|
+
"Ghostty",
|
|
62
|
+
"zed",
|
|
63
|
+
"Zed",
|
|
64
|
+
"cursor",
|
|
65
|
+
"Cursor",
|
|
57
66
|
];
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
// Truecolor TERM tokens (the rare TERM that *does* advertise truecolor).
|
|
69
|
+
const truecolorTermTokens = ["truecolor", "24bit", "alacritty", "kitty", "wezterm"];
|
|
70
|
+
|
|
71
|
+
const hasTruecolor = isAppleTerminal
|
|
72
|
+
? false
|
|
73
|
+
: env.COLORTERM === "truecolor" ||
|
|
74
|
+
env.COLORTERM === "24bit" ||
|
|
75
|
+
truecolorTermPrograms.includes(termProgram) ||
|
|
76
|
+
truecolorTermTokens.some((t) => term.includes(t));
|
|
63
77
|
|
|
64
78
|
// Basic color support
|
|
65
79
|
const hasColor =
|