/** * logo.ts — 启动 logo 的字形(静态) * * 字形是 `curl -fsSL https://pi.dev/install.sh | sh` 顶部那只 π 印记,取自 * `npm:pi-claude-code-tui@0.1.14`(它自己也是照抄安装脚本)。上游把它做成 22 帧动画, * 本仓库只要一只静态 logo,所以这里**只留落定态那一格的形状**,帧表、笔画推进、底座闪动 * 全部去掉。 * * 形状用 4×4 的布尔格(一格 = 三个 `█`,实心 / 空格),比上游那张「1 基坐标 + 逐帧取色」 * 的表好读也好测。 * * **刻意补了上游漏掉的一格**:上游落定帧(phase 6)的格子表是 * `3,2 3,3 3,4 4,2 4,4 5,2 5,3 6,2 6,5`,而它前几帧(phase 4/5 以及白闪帧)都带 `(5,5)`。 * 少了这格,右腿会断成「悬空的一只脚」,静态显示时特别明显(动画里一闪而过没人注意), * 显然是上游那张表的笔误。这里按 `(5,5)` 在场的那版形状画。 * * 本文件不 import pi / pi-tui:颜色由 `index.ts` 用 `theme.fg("accent", …)` 现取, * 所以 `/theme` 换肤能跟随;也让 `node --test clients/pi/extensions/startup-logo/*.test.ts` 能直接跑。 */ /** 一格 = 三个块字符。 */ export const MARK_CELL = "███"; /** 空格占位(与 `MARK_CELL` 等宽,保证每行可见宽度一致,侧栏才对得齐)。 */ export const MARK_BLANK = " ".repeat(MARK_CELL.length); /** * 印记左侧留白:每行前面一个空格。 * * 顶格贴着终端最左边看着太挤,统一缩进一格;这一格算进 `MARK_WIDTH`,侧栏列与窄终端判定 * 都跟着走,调用方不需要另补。 */ export const MARK_INDENT = " "; /** 4×4 布尔格,1 = 实心。行序自上而下,列序自左而右。 */ const MARK_GRID: readonly (readonly (0 | 1)[])[] = [ [1, 1, 1, 0], [1, 0, 1, 0], [1, 1, 0, 1], [1, 0, 0, 1], ]; /** 印记高度(行)。 */ export const MARK_ROWS = MARK_GRID.length; /** 印记本体宽度(列)× 每格字符数 = 12。 */ export const MARK_COLS = MARK_GRID[0]!.length; export const MARK_GLYPH_WIDTH = MARK_COLS * MARK_CELL.length; /** 每行可见宽度(含左侧留白)= 13。 */ export const MARK_WIDTH = MARK_INDENT.length + MARK_GLYPH_WIDTH; /** 单格查询(越界当作空格),给单测和调试用。 */ export function markCellFilled(row: number, col: number): boolean { return MARK_GRID[row]?.[col] === 1; } /** * 印记行的画笔:收「这一格实不实心」,返回**可见宽度为 3** 的字符串。 * * 刻意做成逐格而不是「整行交给一个 paint」:整行包色会把行尾那几个占位空格也吃进色块里, * 于是「每行可见宽度 == MARK_WIDTH」这个不变量在带色版本上就不成立了(侧栏列会歪)。 * * 默认画笔返回未着色的 `███` / 空格,方便单测。 */ export type MarkCellPainter = (filled: boolean) => string; export function markLines(paintCell: MarkCellPainter = (filled) => (filled ? MARK_CELL : MARK_BLANK)): string[] { return MARK_GRID.map((row) => MARK_INDENT + row.map((filled) => paintCell(filled === 1)).join("")); } /** * 把侧栏文字(已着色)贴到印记右侧,**垂直居中**(2 条落在第 1、2 行)。 * * 前提:传进来的行**可见宽度已等于 `MARK_WIDTH`**(`markLines` 保证),所以直接接在行尾就 * 对得齐,这里不按字符串长度补 —— 行里有 ANSI 时按长度补一定歪。左侧留白(`MARK_INDENT`) * 已经算在 `MARK_WIDTH` 里,所以侧栏列也跟着右移一格,不需要在这里额外处理。 * * **不裁行尾占位空格**:它们是「每行 `MARK_WIDTH` 列」这个不变量的一部分。上游那种整行包色的写法 * 在这里也不成立(行尾是 ANSI 重置符而不是空白,`trimEnd` 会静默变成空操作),所以统一不裁, * 让行为与调用方怎么上色无关。这些空格没有底色,终端里不可见,pi 也照样按宽度截断。 */ export function attachSideText(lines: readonly string[], side: readonly string[], gap = 2): string[] { if (lines.length === 0) return []; const start = Math.max(0, Math.floor((lines.length - side.length) / 2)); return lines.map((line, i) => { const text = side[i - start]; return i >= start && text ? `${line}${" ".repeat(gap)}${text}` : line; }); } export interface HeaderSections { /** 已着色的 logo 行(含侧栏文字)。 */ logo: readonly string[]; /** 已着色的快捷键提示行(内置 header 那一行紧凑版)。 */ hints?: string; /** 已着色的说明行。 */ onboarding?: string; } /** * 完整 header 行序:logo → 空行 → 提示 → 空行 → 说明。 * 缺某段就不留多余空行(窄终端回退时 logo 段为空)。 */ export function composeHeaderLines(sections: HeaderSections): string[] { const lines = [...sections.logo]; if (sections.hints) { if (lines.length > 0) lines.push(""); lines.push(sections.hints); } if (sections.onboarding) { if (lines.length > 0) lines.push(""); lines.push(sections.onboarding); } return lines; } /** 绝对路径缩短:家目录内显示成 `~/...`。 */ export function shortenPath(cwd: string, home: string | undefined): string { if (!home) return cwd; if (cwd === home) return "~"; if (cwd.startsWith(`${home}/`)) return `~${cwd.slice(home.length)}`; return cwd; }