@pi-unipi/footer 2.15.0 → 2.16.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.
- package/README.md +3 -5
- package/package.json +3 -3
- package/src/glance-editor.ts +35 -66
- package/src/rendering/icons.ts +5 -0
- package/src/rendering/lolcat.ts +84 -0
- package/src/tui/settings-tui.ts +1 -1
package/README.md
CHANGED
|
@@ -26,11 +26,9 @@ An experimental input surface, on by default and toggleable in `/unipi:footer-se
|
|
|
26
26
|
| Command | Description |
|
|
27
27
|
|---------|-------------|
|
|
28
28
|
| `/unipi:footer` | Toggle footer on/off |
|
|
29
|
-
| `/unipi:footer <preset>` | Switch preset (default, minimal, compact, full, nerd, ascii) |
|
|
30
|
-
| `/unipi:footer sep:<style>` | Change separator style |
|
|
31
|
-
| `/unipi:footer icon:<style>` | Change icon style (nerd, emoji, text) |
|
|
32
29
|
| `/unipi:footer on` / `/unipi:footer off` | Enable/disable explicitly |
|
|
33
|
-
| `/unipi:footer-settings` | Open settings TUI
|
|
30
|
+
| `/unipi:footer-settings` | Open settings TUI — preset, separator, icon style, glance mode, per-segment toggles |
|
|
31
|
+
| `/unipi:footer-help` | Show footer segment guide |
|
|
34
32
|
|
|
35
33
|
## Special Triggers
|
|
36
34
|
|
|
@@ -120,7 +118,7 @@ Settings in `~/.pi/agent/settings.json` under `unipi.footer`:
|
|
|
120
118
|
|-------|-------------|
|
|
121
119
|
| `nerd` | Nerd Font glyphs (auto-detected) |
|
|
122
120
|
| `emoji` | Unicode symbols (works on most terminals) |
|
|
123
|
-
| `text` | Plain text
|
|
121
|
+
| `text` | Plain text labels (works everywhere). Glance frame drops the robot glyph: `UNIPI │ branch:main`, `workspace:unipi` |
|
|
124
122
|
|
|
125
123
|
When `iconStyle` is not set, footer auto-detects Nerd Font support and defaults to `nerd` if available, `emoji` otherwise.
|
|
126
124
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.1",
|
|
4
4
|
"description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
36
|
-
"@pi-unipi/background-tasks": "2.
|
|
35
|
+
"@pi-unipi/core": "2.16.1",
|
|
36
|
+
"@pi-unipi/background-tasks": "2.16.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
package/src/glance-editor.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
|
18
18
|
import type { EditorOptions, EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
19
19
|
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
21
|
-
import { getIcon } from "./rendering/icons.js";
|
|
21
|
+
import { getIcon, getResolvedIconStyle } from "./rendering/icons.js";
|
|
22
|
+
import { lolcatRainbow, paintLolcatGradient } from "./rendering/lolcat.js";
|
|
22
23
|
|
|
23
24
|
/** Live status injected by the footer extension. */
|
|
24
25
|
export interface GlanceStatus {
|
|
@@ -96,25 +97,35 @@ function fmtTokens(n: number): string {
|
|
|
96
97
|
|
|
97
98
|
// ─── Lolcat rainbow gradient (classic sine algorithm, animated) ─────────
|
|
98
99
|
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// blue = sin(freq·i + 4π/3 + phase) · 127 + 128
|
|
102
|
-
// i = character position; phase advances with wall time so the gradient
|
|
103
|
-
// drifts across the word (lolcat -a behavior). Rendered as truecolor SGR.
|
|
100
|
+
// Rendered per glyph as truecolor SGR — see rendering/lolcat.ts (issue #34:
|
|
101
|
+
// painting iterates code points so emoji surrogate pairs survive).
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Compose the glance frame titles for the active icon style.
|
|
105
|
+
*
|
|
106
|
+
* - emoji/nerd: glyph prefix before brand, branch and workspace.
|
|
107
|
+
* - text: no robot glyph (brand word only); literal labels —
|
|
108
|
+
* `branch:main` on the top title, `workspace:unipi` bottom-left.
|
|
109
|
+
*/
|
|
110
|
+
export function composeGlanceTitles(
|
|
111
|
+
brand: string,
|
|
112
|
+
branch: string | null,
|
|
113
|
+
workspace: string,
|
|
114
|
+
): { titleParts: string[]; leftTitle: string } {
|
|
115
|
+
const titleParts: string[] = [];
|
|
116
|
+
if (getResolvedIconStyle() === "text") {
|
|
117
|
+
titleParts.push(brand);
|
|
118
|
+
if (branch) titleParts.push(`branch:${branch}`);
|
|
119
|
+
return { titleParts, leftTitle: ` workspace:${workspace} ` };
|
|
120
|
+
}
|
|
121
|
+
const brandIcon = getIcon("model");
|
|
122
|
+
titleParts.push(`${brandIcon ? brandIcon + " " : ""}${brand}`);
|
|
123
|
+
if (branch) {
|
|
124
|
+
const gitIcon = getIcon("git");
|
|
125
|
+
titleParts.push(`${gitIcon ? gitIcon + " " : ""}${branch}`);
|
|
126
|
+
}
|
|
127
|
+
const dirIcon = getIcon("directory");
|
|
128
|
+
return { titleParts, leftTitle: ` ${dirIcon ? dirIcon + " " : ""}${workspace} ` };
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
export class GlanceEditor extends CustomEditor {
|
|
@@ -133,44 +144,11 @@ export class GlanceEditor extends CustomEditor {
|
|
|
133
144
|
|
|
134
145
|
/**
|
|
135
146
|
* Apply a flowing lolcat gradient to an already-composed frame line.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* the brand itself, which already carries its own colors).
|
|
147
|
+
* Kept as a protected hook for subclasses; painting itself lives in
|
|
148
|
+
* rendering/lolcat.ts (code-point-safe — issue #34).
|
|
139
149
|
*/
|
|
140
150
|
protected paintLolcatLine(line: string, phaseBase: number): string {
|
|
141
|
-
|
|
142
|
-
let pos = 0;
|
|
143
|
-
let i = 0;
|
|
144
|
-
while (i < line.length) {
|
|
145
|
-
const ch = line[i];
|
|
146
|
-
if (ch === "\x1b") {
|
|
147
|
-
const rest = line.slice(i);
|
|
148
|
-
// Copy the whole escape sequence verbatim.
|
|
149
|
-
const m = /^\x1b\[[0-?]*[ -/]*[@-~]/.exec(line.slice(i));
|
|
150
|
-
if (m) {
|
|
151
|
-
out += m[0];
|
|
152
|
-
i += m[0].length;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
// Zero-width string sequences: OSC 133 zone markers (\x1b]133;A
|
|
156
|
-
// BEL) and the APC CURSOR_MARKER (\x1b_pi:c BEL). Painting their
|
|
157
|
-
// bytes is what produced the "_pi:c" garbage in rainbow mode.
|
|
158
|
-
const strSeq = /^\x1b[\]_][^\x07]*(?:\x07|\x1b\\)/.exec(rest);
|
|
159
|
-
if (strSeq) {
|
|
160
|
-
out += strSeq[0];
|
|
161
|
-
i += strSeq[0].length;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const t = LOLCAT_FREQ * 0.35 * pos + phaseBase;
|
|
166
|
-
const r = Math.round(Math.sin(t) * 127 + 128);
|
|
167
|
-
const g = Math.round(Math.sin(t + (2 * Math.PI) / 3) * 127 + 128);
|
|
168
|
-
const b = Math.round(Math.sin(t + (4 * Math.PI) / 3) * 127 + 128);
|
|
169
|
-
out += `\u001b[38;2;${r};${g};${b}m${ch}`;
|
|
170
|
-
pos++;
|
|
171
|
-
i++;
|
|
172
|
-
}
|
|
173
|
-
return out + "\u001b[39m";
|
|
151
|
+
return paintLolcatGradient(line, phaseBase);
|
|
174
152
|
}
|
|
175
153
|
|
|
176
154
|
private static isThinkingHot(level: string | null | undefined): boolean {
|
|
@@ -211,14 +189,7 @@ export class GlanceEditor extends CustomEditor {
|
|
|
211
189
|
// Brand rendered as an animated lolcat gradient (phase from wall time;
|
|
212
190
|
// the footer's 1s refresh timer re-renders, so it shimmers each tick).
|
|
213
191
|
const brand = lolcatRainbow("UNIPI", Date.now() / 1000);
|
|
214
|
-
const
|
|
215
|
-
const titleParts = [
|
|
216
|
-
`${brandIcon ? brandIcon + " " : ""}${brand}`,
|
|
217
|
-
];
|
|
218
|
-
if (st.branch) {
|
|
219
|
-
const gitIcon = getIcon("git");
|
|
220
|
-
titleParts.push(`${gitIcon ? gitIcon + " " : ""}${st.branch}`);
|
|
221
|
-
}
|
|
192
|
+
const { titleParts, leftTitle } = composeGlanceTitles(brand, st.branch, st.workspace);
|
|
222
193
|
const title = titleParts.join(SEP);
|
|
223
194
|
const leadRule = `${BORDER.horizontal} `;
|
|
224
195
|
const titleText = ` ${title}${SEP}`;
|
|
@@ -249,9 +220,7 @@ export class GlanceEditor extends CustomEditor {
|
|
|
249
220
|
if (st.thinkingLevel && st.thinkingLevel !== "off") {
|
|
250
221
|
rightParts.push(`thinking:${st.thinkingLevel}`);
|
|
251
222
|
}
|
|
252
|
-
// Workspace
|
|
253
|
-
const dirIcon = getIcon("directory");
|
|
254
|
-
const leftTitle = ` ${dirIcon ? dirIcon + " " : ""}${st.workspace} `;
|
|
223
|
+
// Workspace label per icon style (glyph prefix, or workspace: in text mode).
|
|
255
224
|
let cluster = rightParts.join(SEP);
|
|
256
225
|
|
|
257
226
|
// Shrink policy for narrow terminals: thinking first, then model tail.
|
package/src/rendering/icons.ts
CHANGED
|
@@ -314,6 +314,11 @@ export function resolveIconStyle(configured?: string): "nerd" | "emoji" | "text"
|
|
|
314
314
|
return detectNerdFontSupport() ? "nerd" : "emoji";
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
/** Effective icon style in use — the current setting, or auto-detection. */
|
|
318
|
+
export function getResolvedIconStyle(): "nerd" | "emoji" | "text" {
|
|
319
|
+
return currentIconStyle ?? resolveIconStyle();
|
|
320
|
+
}
|
|
321
|
+
|
|
317
322
|
/**
|
|
318
323
|
* Get the icon for a segment by ID.
|
|
319
324
|
* Uses the configured icon style, falling back to auto-detection.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Lolcat rainbow gradient painting
|
|
3
|
+
*
|
|
4
|
+
* Classic sine algorithm (lolcat -a): red/green/blue chase each other by
|
|
5
|
+
* 2π/3 across character position; phase advances with wall time so the 1s
|
|
6
|
+
* footer refresh ticks drive visible motion.
|
|
7
|
+
*
|
|
8
|
+
* Issue #34: painting must NEVER split a glyph. Emoji (🤖 U+1F916, 🔀
|
|
9
|
+
* U+1F500) are surrogate pairs — indexing the string per UTF-16 code unit
|
|
10
|
+
* let the rainbow renderer insert an SGR between the two halves, which
|
|
11
|
+
* terminals render as replacement characters. Painting therefore advances
|
|
12
|
+
* per code point (with emoji variation selectors kept attached) and copies
|
|
13
|
+
* ANSI/OSC/APC escape sequences verbatim.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Radians per char — short words → tight sweep. */
|
|
17
|
+
const LOLCAT_FREQ = 0.9;
|
|
18
|
+
|
|
19
|
+
const ANSI_CSI_RE = /^\x1b\[[0-?]*[ -/]*[@-~]/;
|
|
20
|
+
const ANSI_STRING_RE = /^\x1b[\]_][^\x07]*(?:\x07|\x1b\\)/;
|
|
21
|
+
const VARIATION_SELECTOR_16 = 0xfe0f;
|
|
22
|
+
|
|
23
|
+
/** UTF-16 length of the glyph starting at `i`: full code point, plus a
|
|
24
|
+
* trailing VS16 (⬇️ = U+2B07 U+FE0F) so the SGR emitted after the glyph
|
|
25
|
+
* can never detach emoji presentation from its base character. */
|
|
26
|
+
function glyphLength(line: string, i: number): number {
|
|
27
|
+
const cp = line.codePointAt(i)!;
|
|
28
|
+
let len = cp > 0xffff ? 2 : 1;
|
|
29
|
+
if (line.codePointAt(i + len) === VARIATION_SELECTOR_16) len += 1;
|
|
30
|
+
return len;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sineColor(t: number, offset: number): number {
|
|
34
|
+
return Math.round(Math.sin(t + offset) * 127 + 128);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Per-character rainbow — used for the UNIPI brand word (ASCII only). */
|
|
38
|
+
export function lolcatRainbow(text: string, phase: number): string {
|
|
39
|
+
return text
|
|
40
|
+
.split("")
|
|
41
|
+
.map((ch, i) => {
|
|
42
|
+
const t = LOLCAT_FREQ * i + phase;
|
|
43
|
+
return `\x1b[38;2;${sineColor(t, 0)};${sineColor(t, (2 * Math.PI) / 3)};${sineColor(t, (4 * Math.PI) / 3)}m${ch}`;
|
|
44
|
+
})
|
|
45
|
+
.join("") + "\x1b[39m";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Apply a flowing lolcat gradient to an already-composed frame line.
|
|
50
|
+
* Printable glyphs get per-position truecolor from the sine palette;
|
|
51
|
+
* existing escape sequences pass through untouched (they mostly come from
|
|
52
|
+
* the brand itself, which already carries its own colors).
|
|
53
|
+
*/
|
|
54
|
+
export function paintLolcatGradient(line: string, phaseBase: number): string {
|
|
55
|
+
let out = "";
|
|
56
|
+
let pos = 0;
|
|
57
|
+
let i = 0;
|
|
58
|
+
while (i < line.length) {
|
|
59
|
+
if (line[i] === "\x1b") {
|
|
60
|
+
// Copy escape sequences verbatim: CSI SGR, OSC 133 zone markers
|
|
61
|
+
// (\x1b]133;A BEL) and the APC CURSOR_MARKER (\x1b_pi:c BEL).
|
|
62
|
+
// Painting their bytes produced "_pi:c" garbage in rainbow mode.
|
|
63
|
+
const csi = ANSI_CSI_RE.exec(line.slice(i));
|
|
64
|
+
if (csi) {
|
|
65
|
+
out += csi[0];
|
|
66
|
+
i += csi[0].length;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const strSeq = ANSI_STRING_RE.exec(line.slice(i));
|
|
70
|
+
if (strSeq) {
|
|
71
|
+
out += strSeq[0];
|
|
72
|
+
i += strSeq[0].length;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const len = glyphLength(line, i);
|
|
77
|
+
const glyph = line.slice(i, i + len); // whole code point — never split a surrogate pair
|
|
78
|
+
const t = LOLCAT_FREQ * 0.35 * pos + phaseBase;
|
|
79
|
+
out += `\u001b[38;2;${sineColor(t, 0)};${sineColor(t, (2 * Math.PI) / 3)};${sineColor(t, (4 * Math.PI) / 3)}m${glyph}`;
|
|
80
|
+
pos++;
|
|
81
|
+
i += len;
|
|
82
|
+
}
|
|
83
|
+
return out + "\u001b[39m";
|
|
84
|
+
}
|
package/src/tui/settings-tui.ts
CHANGED
|
@@ -244,7 +244,7 @@ class FooterSettingsOverlay {
|
|
|
244
244
|
{
|
|
245
245
|
id: "iconStyle",
|
|
246
246
|
label: "Icon Style",
|
|
247
|
-
description: "
|
|
247
|
+
description: "emoji · nerd (Nerd Font) · text (glance: branch:x, workspace:y)",
|
|
248
248
|
currentValue: this.settings.iconStyle,
|
|
249
249
|
values: ICON_STYLES,
|
|
250
250
|
},
|