mcpField
P
pi-tui-design

pi-tui-design

github.com/joelhooks设计与 UI8567 次安装1 周前更新
代码仓库

关于 pi-tui-design

面向 Pi 和 pi-tui 的终端界面设计技能,介绍布局、主题配色、键盘交互与组件实现,涵盖全屏界面、覆盖层及状态展示等呈现方式。

主要功能

  1. 01终端布局与字符设计
  2. 02主题令牌配色指引
  3. 03键盘交互约定
  4. 04组件渲染与缓存
  5. 05Pi 扩展的界面呈现方式

适用场景

  1. 01设计 Pi 终端仪表板
  2. 02构建键盘驱动的对话框
  3. 03渲染状态与进度组件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/joelhooks/pi-tools --skill pi-tui-design

安装后在 Claude Code 中输入/pi-tui-design确认已加载。

附带文件references/examples.md

详细介绍

Pi 的 TUI 设计

构建精心打磨、而非机器生成的终端界面。终端有诸多限制——固定宽度的字符网格、只能使用键盘、颜色取决于主题——而这些限制正是设计的一部分。

设计思路

编写代码前,先确定设计方向:

  • 基调:极简而精准?信息密集?活泼?工业风?终端有自己的一套美学语言——优雅的制框字符、密集的盲文图案、厚重的块元素、清晰的符号。
  • 范围:全屏接管(ctx.ui.custom)?浮动叠加层?常驻小组件?状态栏?工具渲染?让呈现方式与交互的重要程度相匹配。
  • 特色:什么细节能让界面显得经过精心设计?采用盲文精度的进度条。带强调色标题的对齐列。留有呼吸空间的对话框。

终端美学语言

Unicode 字符集——你的排版工具

终端 UI 没有字体可选。取而代之的是,字符集就是排版工具:

类别字符用途
制框字符(细线)─│┌┐└┘├┤┬┴┼标准边框、表格
制框字符(圆角)╭╮╰╯柔和、现代的观感
制框字符(粗线)━┃┏┓┗┛┣┫┳┻╋强调、标题
制框字符(双线)═║╔╗╚╝╠╣╦╩╬正式、规整
块元素█▓▒░▀▄▌▐进度条、密度、填充
盲文⠀⠁⠂⠃...⣿高分辨率图案、迷你图、图表
符号◆●○◉◎✓✗▸▹▶▷△▽★☆♦状态指示器、项目符号、选择项
数学符号/箭头→←↑↓↔↕⇒⟶⟵∙⋯导航提示、流程
Powerline░分段分隔符(取决于终端)

通过字符粗细建立层次:█(粗)→ ▓(中)→ ▒(细)→ ░(淡)→ (空)。用它表现视觉密度渐变,而不只是填充。

宽高比:终端字符单元格约为 2:1(高度是宽度的两倍)。██(两个块字符)看起来大致是正方形。设计空间布局时请考虑这一点——snake.ts 示例正因如此使用了 cellWidth = 2。

色彩规范

始终使用 pi 的主题令牌。 硬编码的 ANSI 转义序列会在用户切换主题时失效。

// ✗ Hardcoded — breaks on theme change
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;

// ✓ Theme-aware — adapts to dark/light, custom themes
const header = theme.fg("accent", theme.bold("Title"));
const status = theme.fg("success", "✓ OK");
const muted = theme.fg("muted", "secondary info");

色彩层次(来自包含 51 个令牌的主题系统):

  • accent — 主要强调、选择项、活动元素
  • text — 默认正文内容(通常为 "" = 终端默认色)
  • muted — 次要、弱化内容
  • dim — 第三级内容,几乎不可见
  • success / error / warning — 语义状态
  • border / borderAccent / borderMuted — 结构元素
  • toolTitle — 类似工具的上下文中的标题

区域的背景色:selectedBg、userMessageBg、customMessageBg、toolPendingBg、toolSuccessBg、toolErrorBg。

规则:一种强调色占主导。其他所有内容都使用 muted / dim。过度使用颜色会抹平层次——满屏绿色还不如完全不用颜色。

空间构图

width 参数就是画布边界。render() 返回的每一行都不得超出它。

留白节奏:一致的水平内边距能带来视觉上的舒展感。paddingX=1(左右各一个空格)是基准值。标题或许适合用 paddingX=2。拥挤的 UI 会让人感到压迫。

对齐:数字右对齐,标签左对齐。使用 visibleWidth() 计算可识别 ANSI 序列的列宽。用空格补齐,不要用制表符。

留白空间:在各个部分之间放置一个空的 Spacer(1),比加一条分隔线更有效。让内容留有呼吸空间。

嵌套盒体:Container > Box > [children] 可实现内边距、背景和垂直堆叠。不要把所有内容都塞进一个 render 函数——应组合多个组件。

动效与动画

不使用 CSS 过渡。通过 setInterval + tui.requestRender() 定时更新。

// Spinner pattern (see Loader component)
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
this.interval = setInterval(() => {
  this.frame = (this.frame + 1) % frames.length;
  this.invalidate();
  tui.requestRender();
}, 80);

常用模式:盲文点阵加载动画、块元素进度条、列表项依次显示(逐项渲染并添加延迟)、打字机效果文本、脉冲指示器。

必须清理:在 dispose() 中清除定时器。定时器泄漏会导致组件移除后仍继续渲染。

键盘交互设计

键盘是唯一的输入方式。让操作方式易于发现且保持一致。

标准约定(用户对此有预期):

  • ↑↓ 或 j/k — 浏览列表
  • Enter — 确认/选择
  • Escape — 取消/返回
  • Tab — 下一个字段
  • / 或开始输入 — 搜索/筛选

始终显示提示:在页脚显示可用按键。使用 keyHint() 进行适配主题的格式化。

const help = theme.fg("dim", "↑↓ navigate • enter select • esc cancel");

使用 pi-tui 中的 matchesKey() — 它会处理终端转义序列的差异:

import { matchesKey, Key } from "@mariozechner/pi-tui";

handleInput(data: string) {
  if (matchesKey(data, Key.up)) { /* ... */ }
  else if (matchesKey(data, Key.enter)) { /* ... */ }
  else if (matchesKey(data, Key.escape)) { /* ... */ }
  else if (matchesKey(data, Key.ctrl("c"))) { /* ... */ }
}

组件约定

每个 pi-tui 组件都实现:

interface Component {
  render(width: number): string[];   // Lines of output, each ≤ width
  handleInput?(data: string): void;  // Keyboard input when focused
  wantsKeyRelease?: boolean;         // Kitty protocol key release events
  invalidate(): void;                // Clear cached render state
}

关键规则:

  1. render() 输出的每一行都不得超过 width — 使用 truncateToWidth()
  2. 状态改变时调用 invalidate(),然后调用 tui.requestRender() 触发重新渲染
  3. 缓存渲染结果(cachedLines/cachedWidth)— 仅在状态或宽度改变时重新计算
  4. 逐行重新应用样式 — ANSI 样式会在行边界处重置
缓存模式
private cachedWidth?: number;
private cachedLines?: string[];
private version = 0;
private cachedVersion = -1;

render(width: number): string[] {
  if (this.cachedLines && this.cachedWidth === width && this.cachedVersion === this.version) {
    return this.cachedLines;
  }
  // ... compute lines ...
  this.cachedWidth = width;
  this.cachedLines = lines;
  this.cachedVersion = this.version;
  return lines;
}

invalidate(): void {
  this.cachedWidth = undefined;
  this.cachedLines = undefined;
}

状态改变时递增 this.version。主题改变时会自动调用 invalidate()。

主题失效处理

如果将主题颜色预先应用到子组件中,请在 invalidate() 时重建它们:

override invalidate(): void {
  super.invalidate();      // Clear child render caches
  this.rebuildContent();   // Re-apply current theme colors
}

可用组件

来自 @mariozechner/pi-tui
组件用途主要 API
Text自动换行的多行文本new Text(content, paddingX, paddingY, bgFn?)、.setText()
TruncatedText截断的单行文本new TruncatedText(text, paddingX, paddingY)
Box带内边距和背景的容器new Box(paddingX, paddingY, bgFn)、.addChild()、.setBgFn()
Container垂直排列的子组件.addChild()、.removeChild()、.clear()
Spacer空白垂直间距new Spacer(lines)
Markdown带语法高亮的 Markdown 渲染new Markdown(content, paddingX, paddingY, mdTheme)
Image终端图像(Kitty/iTerm2/Ghostty/WezTerm)new Image(base64, mimeType, theme, options)
SelectList支持筛选和滚动的交互式列表items、maxVisible、theme;.onSelect、.onCancel
SettingsList带值的切换设置items、maxVisible、theme、onChange、onClose
Loader带消息的盲文点阵加载动画new Loader(tui, spinnerColor, messageColor, message)
CancellableLoader支持按 Escape 取消的加载器为 Loader 包装中止信号
Input单行文本输入实现 Focusable,用于 IME 光标定位
Editor多行文本编辑器完整编辑功能,支持撤销、kill-ring 和按键绑定
来自 @mariozechner/pi-coding-agent
组件用途主要 API
DynamicBorder自适应宽度的水平边框new DynamicBorder((s: string) => theme.fg("accent", s))
BorderedLoader带边框且支持取消的加载器new BorderedLoader(tui, theme, message)、.signal、.onAbort
CustomEditor内置应用按键绑定的编辑器扩展它以实现模态编辑(vim)和自定义快捷键
工具函数
import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
import { matchesKey, Key } from "@mariozechner/pi-tui";
import { DynamicBorder, getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
  • visibleWidth(str) — 忽略 ANSI 转义码,获取显示宽度
  • truncateToWidth(str, width, ellipsis?) — 截断文本,可选择添加省略号
  • wrapTextWithAnsi(str, width) — 自动换行并保留 ANSI 码
  • matchesKey(data, key) — 将键盘输入与按键标识符进行比较
  • getMarkdownTheme() — Markdown 组件的主题对象
  • keyHint(action, description) — 适配主题的按键绑定提示文本

交付界面

根据交互选择合适的界面:

全屏接管 — ctx.ui.custom(component)

适用于复杂交互:仪表盘、游戏、多步骤向导。

await ctx.ui.custom((tui, theme, keybindings, done) => {
  return new MyComponent(tui, theme, () => done(result));
});
覆层 — ctx.ui.custom(factory, { overlay: true })

浮于现有内容之上。适用于快速选择、确认和面板。

const result = await ctx.ui.custom<string | null>(
  (tui, theme, kb, done) => new MyDialog(theme, done),
  {
    overlay: true,
    overlayOptions: {
      anchor: "center",        // 9 positions: center, top-left, top-center, etc.
      width: "50%",            // number or percentage string
      minWidth: 40,
      maxHeight: "80%",
      offsetX: -2, offsetY: 0,
      margin: 2,               // or { top, right, bottom, left }
      visible: (w, h) => w >= 80,  // responsive hide
    },
  }
);
小组件 — ctx.ui.setWidget(id, lines|factory, options?)

持久显示在编辑器上方或下方。适用于状态、进度和列表。

ctx.ui.setWidget("my-widget", (tui, theme) => ({
  render: () => [theme.fg("accent", "● Active") + " — 3 items pending"],
  invalidate: () => {},
}));
// placement: "belowEditor" for below
// Clear: ctx.ui.setWidget("my-widget", undefined)
状态栏 — ctx.ui.setStatus(id, content)

在页脚中显示单行持久指示器。

ctx.ui.setStatus("mode", theme.fg("accent", "● DESIGN"));
// Clear: ctx.ui.setStatus("mode", undefined)
工具渲染 — renderCall/renderResult

自定义对话中的工具调用显示。返回带有 (0, 0) 内边距的 Text 组件——外层的 Box 会处理内边距。

renderCall(args, theme) {
  return new Text(theme.fg("toolTitle", theme.bold("my_tool ")) + theme.fg("muted", args.action), 0, 0);
},
renderResult(result, { expanded, isPartial }, theme) {
  if (isPartial) return new Text(theme.fg("warning", "Working..."), 0, 0);
  let text = theme.fg("success", "✓ Done");
  if (expanded) text += "\n" + theme.fg("dim", JSON.stringify(result.details, null, 2));
  return new Text(text, 0, 0);
}
页脚 — ctx.ui.setFooter(factory)

替换整个页脚栏。可访问 Git 分支、扩展状态和 Token 统计信息。

ctx.ui.setFooter((tui, theme, footerData) => ({
  render: (width) => [truncateToWidth(`${model} (${footerData.getGitBranch() || "no git"})`, width)],
  invalidate: () => {},
  dispose: footerData.onBranchChange(() => tui.requestRender()),
}));

TUI 反模式

终端 UI 中相当于“AI 垃圾内容”的做法:

不要这样做应该这样做
硬编码 ANSI 颜色(\x1b[31m)使用主题令牌(theme.fg("error", ...))
行宽超过 width 参数对每一行调用 truncateToWidth()
不调用 invalidate()(导致缓存的渲染内容过时)状态变化时清除全部缓存
忽略字符单元格宽高比(2:1)用双宽字符绘制“方形”元素
大段无结构文本使用带有 Spacer、边框和对齐的分区
缺少键盘操作提示在页脚行显示可用按键
定时器或计时器泄漏使用 dispose() 执行清理
主题颜色预先固定,且不会重建覆盖 invalidate() 以重建主题化内容
扁平的渲染函数,没有组合结构采用 Container → Box → 子项的层级结构
所有组件采用相同的视觉风格为每个组件有意设计美学方向
所有加载状态都用通用旋转图标使用符合上下文的进度显示(进度条、百分比、步骤计数)
固定宽度布局根据 width 参数自适应,并设置 minWidth 下限

扩展架构模式

使用 Component + Focusable 的覆层组件

Nico 的覆层始终显式建模生命周期:

import type { Component, Focusable, TUI } from "@mariozechner/pi-tui";
import type { Theme } from "@mariozechner/pi-coding-agent";
import { matchesKey } from "@mariozechner/pi-tui";

type Done = (result: string | null) => void;

class OverlayTemplate implements Component, Focusable {
  focused = false;

  constructor(
    private tui: TUI,
    private theme: Theme,
    private done: Done,
  ) {}

  handleInput(data: string): void {
    if (matchesKey(data, "escape")) this.done(null);
    if (matchesKey(data, "return")) this.done("selected");
  }

  render(_width: number): string[] {
    return [];
  }

  invalidate(): void {}
  dispose(): void {}
}

await ctx.ui.custom((tui, theme, _kb, done) => {
  return new OverlayTemplate(tui, theme, (result) => done(result));
}, { overlay: true, overlayOptions: { anchor: "center", width: 64 } });

InteractiveShellOverlay 和 MessengerConfigOverlay 尤其清楚地体现了这种结构:

  • 构造函数为 constructor(tui, theme, done)。
  • handleInput 处理命令键和移动键。
  • render(width) 构建完整帧文本。
  • invalidate 清除渲染缓存。
  • dispose 移除定时器或计时器,并取消注册回调。
共享渲染辅助函数

使用可复用的辅助函数进行渲染,可以减少不同覆层之间的视觉差异:

import type { Theme } from "@mariozechner/pi-coding-agent";
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";

export function pad(s: string, len: number): string {
  const vis = visibleWidth(s);
  return s + " ".repeat(Math.max(0, len - vis));
}

export function row(content: string, width: number, theme: Theme): string {
  const innerW = width - 2;
  return theme.fg("border", "│") + pad(" " + truncateToWidth(content, innerW - 1), innerW) + theme.fg("border", "│");
}

export function renderHeader(text: string, width: number, theme: Theme): string {
  const innerW = width - 2;
  const padLen = Math.max(0, innerW - visibleWidth(text));
  const padLeft = Math.floor(padLen / 2);
  const padRight = padLen - padLeft;
  return (
    theme.fg("border", "╭" + "─".repeat(padLeft)) +
    theme.fg("accent", text) +
    theme.fg("border", "─".repeat(padRight) + "╮")
  );
}

export function renderFooter(text: string, width: number, theme: Theme): string {
  const innerW = width - 2;
  const padLen = Math.max(0, innerW - visibleWidth(text));
  const padLeft = Math.floor(padLen / 2);
  const padRight = padLen - padLeft;
  return (
    theme.fg("border", "╰" + "─".repeat(padLeft)) +
    theme.fg("dim", text) +
    theme.fg("border", "─".repeat(padRight) + "╯")
  );
}

pi-subagents 中的共享辅助文件(render-helpers.ts)包含 fuzzyFilter()、formatPath() 和 formatScrollInfo(),正是用于此目的。

替换页脚

ctx.ui.setFooter() 是 Nico 用于实现完整状态栏的最佳模式:

ctx.ui.setFooter((tui: any, _theme: Theme, footerData: ReadonlyFooterDataProvider) => {
  footerDataRef = footerData;
  tuiRef = tui;
  const unsub = footerData.onBranchChange(() => tui.requestRender());

  return {
    dispose: unsub,
    invalidate() {},
    render(width: number): string[] {
      const segments: string[] = [];
      const ctx = buildSegmentContext(footerData, activeTheme);
      const preset = getPreset("default");

      for (const id of [...preset.leftSegments, ...preset.rightSegments, ...(preset.secondarySegments ?? [])]) {
        const rendered = renderSegment(id, ctx);
        if (rendered.visible && rendered.content) segments.push(rendered.content);
      }

      if (segments.length === 0) return [];
      return [" " + segments.join(` ${fg(activeTheme, "separator", "⟩", colors)} `) + " "];
    },
  };
});

pi-powerline-footer 也通过覆盖编辑器渲染来替换编辑器和页脚行,并从 setFooter 返回 [],以保持只有一条渲染路径。

配置和设置持久化

这些代码仓库中的扩展持久化都遵循以下结构:

import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

interface ExtensionSettings {
  theme: "dark" | "light";
  enabled: boolean;
}

const extensionDir = join(homedir(), ".pi", "agent", "extensions", "your-extension");
const settingsPath = join(extensionDir, "settings.json");
const extensionDefaults: ExtensionSettings = { theme: "dark", enabled: true };

function loadSettings(): ExtensionSettings {
  if (!existsSync(settingsPath)) return extensionDefaults;
  const raw = readFileSync(settingsPath, "utf-8");
  return { ...extensionDefaults, ...JSON.parse(raw) };
}

function saveSettings(next: Partial<ExtensionSettings>): void {
  mkdirSync(extensionDir, { recursive: true });
  writeFileSync(settingsPath, JSON.stringify({ ...loadSettings(), ...next }, null, 2));
}

先与默认值合并,只有在发生变更时才保存(例如覆层中的 escape/q、设置命令等),做法与 pi-messenger 和 pi-subagent 一致。

免手动操作/后台模式

InteractiveShellOverlay 展示了后台模式的实现方式:

if (options.mode === "hands-free" || options.mode === "dispatch") {
  this.state = "hands-free";
  this.sessionId = options.sessionId ?? generateSessionId(options.name);
  sessionManager.registerActive({
    id: this.sessionId,
    command: options.command,
    reason: options.reason,
    write: (data) => this.session.write(data),
    kill: () => this.killSession(),
    background: () => this.backgroundSession(),
    getOutput: (opts) => this.getOutputSinceLastCheck(opts),
    onComplete: (cb) => this.registerCompleteCallback(cb),
    setUpdateInterval: (ms) => this.setUpdateInterval(ms),
    setQuietThreshold: (ms) => this.setQuietThreshold(ms),
  });
}

if (this.options.onHandsFreeUpdate) {
  this.options.onHandsFreeUpdate({ status: "running", sessionId: this.sessionId, runtime: 0, tail: [], tailTruncated: false, totalCharsSent: 0, budgetExhausted: false });
}

在免手动模式下收到用户输入时,Nico 会调用接管流程(Ctrl+T、Ctrl+B 或输入的按键),停止定时更新,标记为已接管,并可选择在继续透传输入前取消注册会话所有权。

组件组合模式

对话框模式

带圆角边框和键盘操作提示的标准对话框结构:

class DialogComponent implements Component, Focusable {
  focused = false;
  constructor(private tui: any, private theme: Theme, private done: () => void) {}

  render(width: number): string[] {
    const inner = Math.max(20, width - 2);
    const lines = [
      `╭${"─".repeat(Math.max(0, inner - 20))} Dialog Header ${"─".repeat(Math.max(0, inner - 20))}╮`,
      `│ Content line 1...                                                                  │`,
      `├${"─".repeat(inner)}┤`,
      `│ Press \u2191\u2193 navigate • enter confirm • esc cancel                         │`,
      `╰${"─".repeat(inner)}╯`,
    ];
    return lines.map((line) => truncateToWidth(line, width));
  }

  handleInput(data: string) {
    if (matchesKey(data, "escape")) this.done();
  }

  invalidate() {}
  dispose() {}
}

确认、取消排队提示和调色板操作都应使用这种确切的组合方式。

模糊筛选列表

Nico 风格的列表筛选方式一致:

  • 每输入一个字符就进行筛选
  • 筛选内容变化时将选择重置到顶部
  • 维护 selectedIndex 和 scrollOffset
  • 通过紧凑的进度指示器显示滚动和选择状态
class FuzzyListComponent implements Component, Focusable {
  focused = false;
  private query = "";
  private selectedIndex = 0;
  private scrollOffset = 0;
  private maxVisible = 8;
  private filtered = this.items;

  constructor(private items: Array<{ name: string; description: string; model?: string }>) {}

  handleInput(data: string): void {
    if (matchesKey(data, "up") && this.selectedIndex > 0) this.selectedIndex--;
    else if (matchesKey(data, "down") && this.selectedIndex < this.filtered.length - 1) this.selectedIndex++;
    else if (data.length === 1 && data.charCodeAt(0) >= 32) this.query += data;
    else if (matchesKey(data, "backspace")) this.query = this.query.slice(0, -1);

    this.filtered = fuzzyFilter(this.items, this.query);
    this.selectedIndex = 0;
    const maxStart = Math.max(0, this.filtered.length - this.maxVisible);
    this.scrollOffset = Math.min(this.scrollOffset, maxStart);
  }

  render(width: number): string[] {
    const lines = [];
    const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filtered.length - this.maxVisible));
    const end = Math.min(start + this.maxVisible, this.filtered.length);

    for (let i = start; i < end; i++) {
      const item = this.filtered[i];
      const selected = i === this.selectedIndex ? "▸" : "·";
      lines.push(`${selected} ${item.name} ${item.description ?? ""}`);
    }

    if (this.filtered.length > this.maxVisible) {
      lines.push(`${this.selectedIndex + 1}/${this.filtered.length}`);
    }
    return lines;
  }

  invalidate() {}
  dispose() {}
}
基于片段的渲染

Powerline 风格的状态栏最适合使用独立的片段对象:

interface RenderedSegment {
  content: string;
  visible: boolean;
}

interface StatusLineSegment {
  id: string;
  render(ctx: any): RenderedSegment;
}

const SEGMENTS: Record<string, StatusLineSegment> = {
  model: {
    id: "model",
    render(ctx) {
      if (!ctx.model?.name) return { content: "", visible: false };
      return { content: `🤖 ${ctx.model.name}`, visible: true };
    },
  },
  git: {
    id: "git",
    render(ctx) {
      if (!ctx.git?.branch) return { content: "", visible: false };
      return { content: ` ${ctx.git.branch}`, visible: true };
    },
  },
};

function renderSegment(id: string, ctx: any): RenderedSegment {
  return SEGMENTS[id]?.render(ctx) ?? { content: "", visible: false };
}

分别渲染每个片段,然后用语义分隔符连接;每个片段决定自身是否可见(内容为空时设为 visible: false)。

复制粘贴示例

完整、独立的组件实现请参阅 references/examples.md:

  • 选择对话框 — SelectList + DynamicBorder + 键盘提示
  • 状态仪表盘 — 多分区边框绘制布局、对齐的列、语义颜色
  • 进度跟踪器 — 动画点字进度条、计时器生命周期、dispose 清理
  • 数据表格 — 可滚动行、列对齐、行高亮
  • 持久小组件 — 编辑器上方的健康状态指示器,单行紧凑显示
  • 工具渲染器 — 使用 renderCall/renderResult 展示可展开的详情
  • 叠加面板 — 侧边面板、响应式显示、锚点定位

上游示例

pi 仓库中的扩展示例展示了更多模式:

  • snake.ts:完整的游戏循环、边框绘制、通过 pi.appendEntry() 持久化会话
  • space-invaders.ts:Kitty 按键释放事件(wantsKeyRelease)、多实体渲染
  • overlay-qa-tests.ts:全部 9 种锚点位置、响应式显示、约 30 FPS 动画
  • preset.ts:使用 DynamicBorder 的 SelectList——标准对话框模式
  • plan-mode:使用 setStatus + setWidget 显示持久模式指示器
  • todo.ts:使用 renderCall/renderResult 自定义工具渲染

评论

支持 Markdown