@pi-archimedes/core 2.7.3 → 2.9.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.
@@ -1,330 +0,0 @@
1
- import { VERSION, type Theme } from "@earendil-works/pi-coding-agent";
2
- import { getShinedLogo, TRUECOLOR, LOGO_PAD, LOGO_SETTLE_FRAME } from "./logo.js";
3
- import { loadCoreConfig } from "../config.js";
4
- import { detectSection, parseSectionText, parseModelScope, formatColumns, buildItemWrapper, type ParsedSection, SECTION_KEYS } from "./sections.js";
5
- import { fetchLatestVersion, compareVersions } from "./version.js";
6
- import { stripAnsi } from "../text.js";
7
- import { Text, Spacer, Container, type TUI, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
8
-
9
- // Symbol keys (survive hot-reload)
10
- const LISTING_REF = Symbol.for("splashscreen:listingRef");
11
- const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
12
- const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
13
- const PATCHED_CLEAR = Symbol.for("splashscreen:clearPatched");
14
- const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
15
- const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
16
-
17
- // Animation constants
18
- const MAX_RENDER_WIDTH = 9999;
19
- const MIN_HEADER_LINES = 11;
20
- const REVEAL_DEBOUNCE_MS = 150;
21
- const RAMP_FRAMES = 22;
22
- const STAGGER_FRAMES = 0;
23
- const BASE_FADE_DELAY = 3;
24
- const MAX_STAGGER = BASE_FADE_DELAY + 5 * STAGGER_FRAMES;
25
-
26
- export interface ListingRef {
27
- sections: ParsedSection[];
28
- frame: number;
29
- revealed: boolean;
30
- revealedAt: number;
31
- scaffoldAt: number;
32
- latestVersion?: string;
33
- settled: boolean;
34
- cachedLines?: string[];
35
- cachedWidth?: number;
36
- cachedHeight?: number;
37
- maxHeaderHeight?: number;
38
- }
39
-
40
- // ── renderHeader() — composes logo + sections + padding ────────────────
41
-
42
- export function renderHeader(theme: Theme, ref: ListingRef, width: number, height: number): string[] {
43
- const dim = (t: string) => theme.fg("dim", t);
44
- const accent = (t: string) => theme.fg("accent", t);
45
- const logoLines = getShinedLogo(ref.frame, loadCoreConfig().animationStyle);
46
-
47
- // Use cached text lines if settled (no more animations)
48
- let listingLines: string[];
49
-
50
- if (ref.settled && ref.cachedLines && ref.cachedWidth === width && ref.cachedHeight === height) {
51
- listingLines = ref.cachedLines;
52
- } else {
53
- const sectionsToRender: { name: "Version" | ParsedSection["name"]; items: string[] }[] = [];
54
-
55
- if (!ref.revealed) {
56
- // Logo only — sections appear together on reveal
57
- } else {
58
- const latest = ref.latestVersion ?? VERSION;
59
- const hasUpdate = compareVersions(latest, VERSION) > 0;
60
- const latestStr = hasUpdate ? `Latest: ${accent("v" + latest)}` : `Latest: v${latest}`;
61
- sectionsToRender.push({ name: "Version", items: [`Local: v${VERSION}`, latestStr] });
62
-
63
- // Display sections in SECTION_KEYS order, skip empty
64
- const byName = new Map(ref.sections.map(s => [s.name, s]));
65
- for (const key of SECTION_KEYS) {
66
- const sec = byName.get(key);
67
- if (sec && sec.items.length > 0) sectionsToRender.push(sec);
68
- }
69
- }
70
-
71
- const listingMaxW = Math.floor(width * 0.7);
72
- listingLines = formatColumns(sectionsToRender, theme, listingMaxW, ref);
73
-
74
- // Cache once text animations are done
75
- const textAge = ref.revealed ? ref.frame - ref.revealedAt : 0;
76
- const textDone = ref.revealed && textAge > RAMP_FRAMES + MAX_STAGGER;
77
- const logoDone = ref.frame >= LOGO_SETTLE_FRAME;
78
-
79
- if (textDone && logoDone) {
80
- ref.settled = true;
81
- ref.cachedLines = listingLines;
82
- ref.cachedWidth = width;
83
- ref.cachedHeight = height;
84
- }
85
- }
86
-
87
- // Build content: logo + separator + listing
88
- const contentLines: string[] = [];
89
-
90
- // Logo centered above listing (same center as listing text)
91
- const listingMaxW = Math.floor(width * 0.7);
92
- const listingLeftPad = Math.floor((width - listingMaxW) / 2);
93
- for (const logoRow of logoLines) {
94
- const pad = Math.floor((listingMaxW - visibleWidth(logoRow)) / 2);
95
- contentLines.push(" ".repeat(LOGO_PAD) + " ".repeat(listingLeftPad + pad) + logoRow);
96
- }
97
-
98
- // Separator
99
- contentLines.push("");
100
-
101
- // Listing
102
- for (const listRow of listingLines) {
103
- contentLines.push(" ".repeat(LOGO_PAD) + " ".repeat(listingLeftPad) + listRow);
104
- }
105
-
106
- // Pad top and bottom to fill height (+5 top bias for visual centering)
107
- const VERTICAL_CENTER_BIAS = 5; // Shift content slightly upward for visual balance
108
- const contentHeight = contentLines.length;
109
- const remaining = Math.max(0, height - contentHeight);
110
- const topPad = Math.floor(remaining / 2) + VERTICAL_CENTER_BIAS;
111
- const bottomPad = remaining - topPad;
112
-
113
- const result: string[] = [];
114
- for (let i = 0; i < topPad; i++) result.push("");
115
- for (const line of contentLines) result.push(line);
116
- for (let i = 0; i < bottomPad; i++) result.push("");
117
-
118
- return result;
119
- }
120
-
121
- // ── Chat container discovery ────────────────────────────────────────────
122
-
123
- // Fragile: relies on TUI child ordering (header, chat, footer) which is an
124
- // internal layout detail of pi's InteractiveMode. If upstream changes the
125
- // child structure, this will need updating.
126
- //
127
- // Strategy (most specific → least specific):
128
- // 1. Container with "Scrollable" in constructor name
129
- // 2. Middle child — TUI layout is [headerContainer, chatContainer, footer]
130
- // This is position-stable even when chatContainer is empty (e.g. after clear)
131
- // 3. Container with most children (chat usually has the most)
132
- // 4. First Container found
133
- function findChatContainer(tui: TUI): Container | undefined {
134
- // Strategy 1: Look for Scrollable container
135
- for (const child of tui.children) {
136
- if (child instanceof Container && child.constructor.name.includes("Scrollable")) {
137
- return child;
138
- }
139
- }
140
-
141
- // Strategy 2: Middle child — TUI layout is [headerContainer, chatContainer, footer]
142
- if (tui.children.length >= 3) {
143
- const middle = tui.children[1];
144
- if (middle instanceof Container) return middle;
145
- }
146
-
147
- // Strategy 3: Find the Container with the most children (chat area)
148
- const containers = tui.children.filter((c): c is Container => c instanceof Container);
149
- if (containers.length > 1) {
150
- const largest = containers.reduce((best, c) =>
151
- c.children.length > best.children.length ? c : best,
152
- );
153
- if (largest.children.length > 0) return largest;
154
- }
155
-
156
- // Strategy 4: First Container
157
- if (containers.length > 0) return containers[0];
158
-
159
- return undefined;
160
- }
161
-
162
- // Pi 0.84+ moved resource sections from chatContainer to loadedResourcesContainer
163
- // (child of documentContainer). Find it by locating documentContainer (the first TUI
164
- // child that contains Container sub-children), then taking its second Container child.
165
- //
166
- // Pi 0.84+ TUI layout (regular mode, 7 top-level children):
167
- // documentContainer → [headerContainer(0), loadedResourcesContainer(1), chatContainer(2)]
168
- function findLoadedResourcesContainer(tui: TUI): Container | undefined {
169
- // Only run this heuristic on Pi >= 0.84.0 where loadedResourcesContainer exists.
170
- // On older Pi versions, return undefined so the caller falls back to findChatContainer().
171
- if (compareVersions(VERSION, "0.84.0") < 0) return undefined;
172
-
173
- // Find documentContainer: first tui child that has Container sub-children
174
- const docContainer = tui.children.find(
175
- (c) => c instanceof Container && c.children.some((child) => child instanceof Container)
176
- ) as Container | undefined;
177
- if (!docContainer) return undefined;
178
-
179
- const containerChildren = docContainer.children.filter(
180
- (c): c is Container => c instanceof Container
181
- );
182
- // Pi 0.84+: [headerContainer(0), loadedResourcesContainer(1), chatContainer(2)]
183
- // The index [1] assumption is load-bearing — tied to Pi 0.84+ initialization order.
184
- if (containerChildren.length < 2) return undefined;
185
- return containerChildren[1]; // loadedResourcesContainer
186
- }
187
-
188
- export function patchStartupListing(
189
- tui: TUI,
190
- _theme: Theme,
191
- ref: ListingRef,
192
- ): void {
193
- const resourcesContainer = findLoadedResourcesContainer(tui) ?? findChatContainer(tui);
194
- if (!resourcesContainer) {
195
- console.warn("[archimedes] Could not find resources container — startup listing disabled. This may indicate a pi TUI structure change.");
196
- return;
197
- }
198
- const rc = resourcesContainer as any;
199
-
200
- // Always update ref + restart animation (critical for /reload)
201
- rc[LISTING_REF] = ref;
202
- ref.frame = 0;
203
- ref.revealed = false;
204
- ref.revealedAt = 0;
205
- ref.scaffoldAt = 0;
206
- ref.settled = false;
207
- delete ref.cachedLines;
208
- delete ref.cachedWidth;
209
- delete ref.maxHeaderHeight;
210
-
211
- if (rc[ANIM_INTERVAL]) clearInterval(rc[ANIM_INTERVAL]);
212
- if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
213
-
214
- const interval = setInterval(() => {
215
- try {
216
- const current: ListingRef = rc[LISTING_REF];
217
- if (!current) {
218
- clearInterval(interval);
219
- return;
220
- }
221
- current.frame++;
222
- if (current.settled && current.frame >= LOGO_SETTLE_FRAME) {
223
- clearInterval(interval);
224
- rc[ANIM_INTERVAL] = null;
225
- return;
226
- }
227
- tui.requestRender();
228
- } catch {
229
- clearInterval(interval);
230
- }
231
- }, 16);
232
-
233
- rc[ANIM_INTERVAL] = interval;
234
-
235
- // Fetch latest version from npm
236
- fetchLatestVersion().then(v => {
237
- if (v) {
238
- const current: ListingRef = rc[LISTING_REF];
239
- current.latestVersion = v;
240
- // Invalidate cache so version updates on next render
241
- delete current.cachedLines;
242
- current.settled = false;
243
- }
244
- });
245
-
246
- // Patch clear() to intercept container rebuild
247
- if (!rc[PATCHED_CLEAR]) {
248
- rc[PATCHED_CLEAR] = true;
249
- const origClear = resourcesContainer.clear.bind(resourcesContainer);
250
- resourcesContainer.clear = () => {
251
- return origClear();
252
- };
253
- }
254
-
255
- // Only patch addChild once — the closure reads rc[LISTING_REF] dynamically
256
- if (rc[PATCHED_LISTING]) {
257
- resourcesContainer.clear();
258
- return;
259
- }
260
- rc[PATCHED_LISTING] = true;
261
-
262
- const origAddChild = resourcesContainer.addChild.bind(resourcesContainer);
263
- rc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
264
- resourcesContainer.clear();
265
-
266
- resourcesContainer.addChild = (component: Component) => {
267
- try {
268
- const currentRef: ListingRef = rc[LISTING_REF];
269
-
270
- if (component instanceof Text) {
271
- // pi ≥0.67.6 wraps startup sections in ExpandableText; collapsed body
272
- // is a lossy comma-joined base-name list. Parse the expanded text so
273
- // Extensions keep real names instead of "index.ts/index.js/index".
274
- const getExpanded = (component as any).getExpandedText;
275
- const plain = typeof getExpanded === "function"
276
- ? stripAnsi(getExpanded.call(component))
277
- : stripAnsi(component.render(MAX_RENDER_WIDTH).join("\n"));
278
-
279
- const section = parseSectionText(plain) ?? parseModelScope(plain);
280
- if (section) {
281
- const existing = currentRef.sections.find(s => s.name === section.name);
282
- if (existing) {
283
- existing.items = [...new Set([...existing.items, ...section.items])];
284
- } else {
285
- currentRef.sections.push(section);
286
- }
287
-
288
- // Invalidate cache so late-arriving sections show up
289
- currentRef.settled = false;
290
- delete currentRef.cachedLines;
291
-
292
- if (currentRef.revealed) {
293
- // Already revealed — show new section immediately
294
- tui.requestRender();
295
- } else {
296
- // Batch initial sections — reset debounce on each arrival
297
- if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
298
- rc[DEBOUNCE_TIMER] = setTimeout(() => {
299
- const ref: ListingRef = rc[LISTING_REF];
300
- ref.revealed = true;
301
- ref.revealedAt = ref.frame;
302
- ref.scaffoldAt = ref.frame;
303
- tui.requestRender();
304
- rc[DEBOUNCE_TIMER] = null;
305
- }, REVEAL_DEBOUNCE_MS);
306
- }
307
-
308
- return;
309
- }
310
-
311
- if (
312
- plain.includes("Listing all available commands") ||
313
- plain.includes("(Source: extension)") ||
314
- plain.trim().startsWith("/skill:")
315
- ) {
316
- return;
317
- }
318
- }
319
-
320
- if (component instanceof Spacer && !currentRef.revealed) return;
321
- try {
322
- origAddChild(component);
323
- } catch (err) {
324
- console.error("[archimedes] Error in resourcesContainer.addChild:", err);
325
- }
326
- } catch (err) {
327
- console.error("[archimedes] Error in startup listing addChild handler:", err);
328
- }
329
- };
330
- }
@@ -1,260 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import * as fc from "fast-check";
3
- import { stripAnsi } from "../text.js";
4
-
5
- // ── Helpers for dynamic imports with controlled TRUECOLOR ────────────────────
6
-
7
- async function importLogoWithTruecolor(truecolor: boolean) {
8
- vi.resetModules();
9
-
10
- // Set env before the module loads
11
- const origColorterm = process.env.COLORTERM;
12
- const origTerm = process.env.TERM;
13
- const origTermProgram = process.env.TERM_PROGRAM;
14
- const origWtSession = process.env.WT_SESSION;
15
-
16
- if (truecolor) {
17
- process.env.COLORTERM = "truecolor";
18
- } else {
19
- delete process.env.COLORTERM;
20
- process.env.TERM = "xterm";
21
- delete process.env.TERM_PROGRAM;
22
- delete process.env.WT_SESSION;
23
- }
24
-
25
- const mod = await import("./logo.js");
26
-
27
- // Restore env
28
- if (origColorterm === undefined) delete process.env.COLORTERM;
29
- else process.env.COLORTERM = origColorterm;
30
- if (origTerm === undefined) delete process.env.TERM;
31
- else process.env.TERM = origTerm;
32
- if (origTermProgram === undefined) delete process.env.TERM_PROGRAM;
33
- else process.env.TERM_PROGRAM = origTermProgram;
34
- if (origWtSession === undefined) delete process.env.WT_SESSION;
35
- else process.env.WT_SESSION = origWtSession;
36
-
37
- return mod;
38
- }
39
-
40
- // ── LOGO structure (static, no env dependency) ──────────────────────────────
41
-
42
- describe("LOGO structure", () => {
43
- it("has 8 rows", async () => {
44
- const mod = await importLogoWithTruecolor(false);
45
- expect(mod.LOGO.length).toBe(8);
46
- });
47
-
48
- it("each row has 16 characters", async () => {
49
- const mod = await importLogoWithTruecolor(false);
50
- for (const row of mod.LOGO) {
51
- expect(row.length).toBe(16);
52
- }
53
- });
54
-
55
- it("first row is 12 blocks + 4 spaces", async () => {
56
- const mod = await importLogoWithTruecolor(false);
57
- expect(mod.LOGO[0]).toBe("████████████ ");
58
- });
59
-
60
- it("last row is 4 blocks + 6 spaces + 4 blocks", async () => {
61
- const mod = await importLogoWithTruecolor(false);
62
- expect(mod.LOGO[7]).toBe("████ ████");
63
- });
64
-
65
- it("LOGO contains only block chars and spaces", async () => {
66
- const mod = await importLogoWithTruecolor(false);
67
- for (const row of mod.LOGO) {
68
- for (const ch of row) {
69
- expect(ch === " " || ch === "█").toBe(true);
70
- }
71
- }
72
- });
73
- });
74
-
75
- // ── Animation constants ──────────────────────────────────────────────────────
76
-
77
- describe("animation constants", () => {
78
- it("CHAR_FADE_FRAMES is 22", async () => {
79
- const mod = await importLogoWithTruecolor(false);
80
- expect(mod.CHAR_FADE_FRAMES).toBe(22);
81
- });
82
-
83
- it("LOGO_SETTLE_FRAME is 90", async () => {
84
- const mod = await importLogoWithTruecolor(false);
85
- expect(mod.LOGO_SETTLE_FRAME).toBe(90);
86
- });
87
-
88
- it("LOGO_PAD is 0", async () => {
89
- const mod = await importLogoWithTruecolor(false);
90
- expect(mod.LOGO_PAD).toBe(0);
91
- });
92
-
93
- it("LOGO_GAP is 4", async () => {
94
- const mod = await importLogoWithTruecolor(false);
95
- expect(mod.LOGO_GAP).toBe(4);
96
- });
97
- });
98
-
99
- // ── TRUECOLOR detection ──────────────────────────────────────────────────────
100
-
101
- describe("TRUECOLOR", () => {
102
- it("is true when COLORTERM contains truecolor", async () => {
103
- const mod = await importLogoWithTruecolor(true);
104
- expect(mod.TRUECOLOR).toBe(true);
105
- });
106
-
107
- it("is false when no truecolor env vars set", async () => {
108
- const mod = await importLogoWithTruecolor(false);
109
- expect(mod.TRUECOLOR).toBe(false);
110
- });
111
- });
112
-
113
- // ── getShinedLogo — non-truecolor ────────────────────────────────────────────
114
-
115
- describe("getShinedLogo (non-truecolor)", () => {
116
- let mod: typeof import("./logo.js");
117
-
118
- beforeEach(async () => {
119
- mod = await importLogoWithTruecolor(false);
120
- });
121
-
122
- it("returns LOGO unchanged when TRUECOLOR is false", () => {
123
- const result = mod.getShinedLogo(0, "wave");
124
- expect(result).toBe(mod.LOGO);
125
- });
126
-
127
- it("returns LOGO at any frame when TRUECOLOR is false", () => {
128
- expect(mod.getShinedLogo(999, "wave")).toBe(mod.LOGO);
129
- });
130
-
131
- it("returns 8 rows regardless of frame", () => {
132
- expect(mod.getShinedLogo(50, "diagonal").length).toBe(8);
133
- });
134
- });
135
-
136
- // ── getShinedLogo — truecolor ────────────────────────────────────────────────
137
-
138
- describe("getShinedLogo (truecolor)", () => {
139
- let mod: typeof import("./logo.js");
140
-
141
- beforeEach(async () => {
142
- mod = await importLogoWithTruecolor(true);
143
- });
144
-
145
- it("returns 8 rows", () => {
146
- expect(mod.getShinedLogo(0, "wave").length).toBe(8);
147
- });
148
-
149
- it("returns different output than LOGO when TRUECOLOR is true", () => {
150
- const result = mod.getShinedLogo(50, "wave");
151
- expect(result).not.toBe(mod.LOGO);
152
- });
153
-
154
- it("early frames show spaces for not-yet-revealed chars", () => {
155
- const result = mod.getShinedLogo(0, "vertical");
156
- // At frame 0, nothing should be revealed yet
157
- for (const row of result) {
158
- const stripped = stripAnsi(row);
159
- expect(stripped).toMatch(/^[\s]*$/);
160
- }
161
- });
162
-
163
- it("late frames show all characters revealed", () => {
164
- const result = mod.getShinedLogo(200, "vertical");
165
- for (let i = 0; i < result.length; i++) {
166
- const stripped = stripAnsi(result[i]!);
167
- const expected = stripAnsi(mod.LOGO[i]!);
168
- expect(stripped).toBe(expected);
169
- }
170
- });
171
-
172
- it("output contains ANSI gray escapes", () => {
173
- const result = mod.getShinedLogo(50, "wave");
174
- const joined = result.join("\n");
175
- expect(joined).toMatch(/\x1b\[38;2;\d+;\d+;\d+m/);
176
- });
177
-
178
- it("default style is wave", () => {
179
- const result1 = mod.getShinedLogo(50);
180
- const result2 = mod.getShinedLogo(50, "wave");
181
- expect(result1).toEqual(result2);
182
- });
183
- });
184
-
185
- // ── All animation styles ─────────────────────────────────────────────────────
186
-
187
- describe("animation styles", () => {
188
- const styles = [
189
- "diagonal",
190
- "top-right",
191
- "bottom-left",
192
- "bottom-right",
193
- "center-out",
194
- "wave",
195
- "horizontal",
196
- "vertical",
197
- "vertical-up",
198
- ] as const;
199
-
200
- it.each(styles)("style '%s' produces valid reveal times", async (style) => {
201
- const mod = await importLogoWithTruecolor(true);
202
- const result = mod.getShinedLogo(100, style);
203
- expect(result.length).toBe(8);
204
- // Each row should be a string
205
- for (const row of result) {
206
- expect(typeof row).toBe("string");
207
- }
208
- });
209
-
210
- it.each(styles)("style '%s' fully reveals at high frame count", async (style) => {
211
- const mod = await importLogoWithTruecolor(true);
212
- const result = mod.getShinedLogo(500, style);
213
- for (let i = 0; i < result.length; i++) {
214
- const stripped = stripAnsi(result[i]!);
215
- const expected = stripAnsi(mod.LOGO[i]!);
216
- expect(stripped).toBe(expected);
217
- }
218
- });
219
- });
220
-
221
- // ── Properties ───────────────────────────────────────────────────────────────
222
-
223
- describe("properties", () => {
224
- it("getShinedLogo always returns 8 rows (truecolor)", async () => {
225
- const mod = await importLogoWithTruecolor(true);
226
- fc.assert(
227
- fc.property(fc.nat(1000), n => {
228
- return mod.getShinedLogo(n, "wave").length === 8;
229
- }),
230
- );
231
- });
232
-
233
- it("each row visible width equals 16 after stripping ANSI (truecolor)", async () => {
234
- const mod = await importLogoWithTruecolor(true);
235
- fc.assert(
236
- fc.property(fc.nat(500), n => {
237
- const result = mod.getShinedLogo(n, "diagonal");
238
- for (const row of result) {
239
- const stripped = stripAnsi(row);
240
- // stripAnsi trims, so we check the raw row length instead
241
- // The actual row length (including ANSI) may vary, but the
242
- // visible content should always be 16 chars
243
- // Since stripAnsi trims trailing spaces, check untrimmed
244
- const raw = row.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "");
245
- if (raw.length !== 16) return false;
246
- }
247
- return true;
248
- }),
249
- );
250
- });
251
-
252
- it("getShinedLogo returns 8 rows (non-truecolor)", async () => {
253
- const mod = await importLogoWithTruecolor(false);
254
- fc.assert(
255
- fc.property(fc.nat(1000), n => {
256
- return mod.getShinedLogo(n, "wave").length === 8;
257
- }),
258
- );
259
- });
260
- });
@@ -1,85 +0,0 @@
1
- import { gray, rgb, extractRgb, lerp } from "../color.js";
2
- import type { AnimationStyle } from "../config.js";
3
-
4
- // ── Truecolor detection ────────────────────────────────────────
5
-
6
- export const TRUECOLOR = /truecolor|24bit/i.test(process.env.COLORTERM ?? "")
7
- || (process.env.TERM ?? "").includes("256color")
8
- || process.env.TERM_PROGRAM === "iTerm.app"
9
- || process.env.TERM_PROGRAM === "WezTerm"
10
- || process.env.TERM_PROGRAM === "vscode"
11
- || process.env.WT_SESSION !== undefined;
12
-
13
- // ── Logo ───────────────────────────────────────────────────────
14
-
15
- export const LOGO = [
16
- "████████████ ",
17
- "████████████ ",
18
- "████ ████ ",
19
- "████ ████ ",
20
- "████████ ████",
21
- "████████ ████",
22
- "████ ████",
23
- "████ ████",
24
- ];
25
-
26
- export const CHAR_FADE_FRAMES = 22;
27
- export const LOGO_SETTLE_FRAME = 90;
28
- export const LOGO_PAD = 0;
29
- export const LOGO_GAP = 4;
30
-
31
- const LOGO_COLS = 14;
32
- const LOGO_ROWS = 8;
33
- const CENTER_X = (LOGO_COLS - 1) / 2;
34
- const CENTER_Y = (LOGO_ROWS - 1) / 2;
35
-
36
- function computeRevealAt(x: number, y: number, style: AnimationStyle): number {
37
- switch (style) {
38
- case "diagonal":
39
- return ((x / 2) * 1.2 + (y / 2) * 3.5) * 1.4;
40
- case "top-right":
41
- return (((LOGO_COLS - 1 - x) / 2) * 1.2 + (y / 2) * 3.5) * 1.4;
42
- case "bottom-left":
43
- return ((x / 2) * 1.2 + ((LOGO_ROWS - 1 - y) / 2) * 3.5) * 1.4;
44
- case "bottom-right":
45
- return (((LOGO_COLS - 1 - x) / 2) * 1.2 + ((LOGO_ROWS - 1 - y) / 2) * 3.5) * 1.4;
46
- case "center-out": {
47
- const dist = Math.sqrt((x - CENTER_X) ** 2 + (y - CENTER_Y) ** 2);
48
- return dist * 4.5;
49
- }
50
- case "wave": {
51
- const base = ((x / 2) * 1.2 + (y / 2) * 3.5) * 1.4;
52
- const wave = Math.sin((x * 0.8 + y * 0.5) * 1.2) * 8;
53
- return base + wave;
54
- }
55
- case "horizontal":
56
- return x * 3.5;
57
- case "vertical":
58
- return y * 5.5;
59
- case "vertical-up":
60
- return (LOGO_ROWS - 1 - y) * 5.5;
61
- }
62
- }
63
-
64
- export function getShinedLogo(frame: number, style: AnimationStyle = "wave"): string[] {
65
- if (!TRUECOLOR) return LOGO;
66
-
67
- return LOGO.map((line, y) => {
68
- let result = "";
69
- for (let x = 0; x < line.length; x++) {
70
- const char = line[x]!;
71
- if (char === " ") { result += " "; continue; }
72
-
73
- const revealAt = computeRevealAt(x, y, style);
74
- const age = frame - revealAt;
75
-
76
- if (age <= 0) { result += " "; continue; }
77
-
78
- const t = Math.min(1, age / CHAR_FADE_FRAMES);
79
- const eased = 1 - (1 - t) * (1 - t);
80
- const brightness = Math.floor(lerp(50, 255, eased));
81
- result += gray(brightness, char);
82
- }
83
- return result;
84
- });
85
- }