# @ashx-j/lunr-tui 中文文档

规范地址：https://npmcn.com/package/%40ashx-j/lunr-tui/

版本：0\.2\.40
npm 元数据更新时间：2026-10-25T15:36:11.956Z
中文译文更新时间：2026-11-06T23:19:58.438Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40ashx-j/lunr-tui
原文对照：https://npmcn.com/package/%40ashx-j/lunr-tui/?lang=original
项目主页：https://github.com/ashx-j/lunR#readme
源码仓库：https://github.com/ashx-j/lunR

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：2 MB，124 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.19\.0
- 运行依赖：2 个
- 首次发布：2026-08-16T12:36:33.066Z
- 最新版本发布：2026-10-25T15:36:11.686Z
- 近 30 天下载量：2369（2026-10-07 至 2026-11-05，npm 官方下载统计）
- 已发布版本数：59（近 12 个月 59 个）

## 中文简介

终端用户界面库,用于高效文本应用程序的差分渲染

## 安装

```sh
npm install @ashx-j/lunr-tui
```

也可以使用：`pnpm add @ashx-j/lunr-tui`、`yarn add @ashx-j/lunr-tui`、`bun add @ashx-j/lunr-tui`

## README

# 翻译:

最小终端UI框架,有差分渲染和同步输出,用于flicker无交互CLI应用程序.

## 特征

-   **区别对待**: 三战略渲染系统,只更新变化
-   **同步输出**: CSI 2026用于原子屏幕更新(无闪烁)
-   **括号粘贴模式**: 正确处理大于10行粘贴的大型粘贴
-   **基于构成部分**: 具有渲染( ) 方法的简单组件接口
-   **主题支持**: 组件接受自定义式的主题界面
-   **内建组件**: 文本、 截断文本、 输入、 编辑器、 Markdown、 装入器、 选择列表、 设置列表、 空间器、 图像、 框、 容器
-   **内含图像**: 在支持 Kitty 或 iTerm2 图形协议的终端中渲染图像
-   **自动完整支持**: 文件路径和斜线命令

## 快速启动

```typescript
import { TUI, Text, Editor, ProcessTerminal, matchesKey } from "@ashx-j/lunr-tui";

// Create terminal
const terminal = new ProcessTerminal();

// Create TUI
const tui = new TUI(terminal);

// Add components
tui.addChild(new Text("Welcome to my app!"));

import { defaultEditorTheme as editorTheme } from './test/test-themes.ts';
const editor = new Editor(tui, editorTheme);
editor.onSubmit = (text) => {
  console.log("Submitted:", text);
  tui.addChild(new Text(`You said: ${text}`));
};
tui.addChild(editor);

// Focus the editor so it receives keyboard input
tui.setFocus(editor);

// In raw mode Ctrl+C doesn't send SIGINT — intercept it here to allow exit
tui.addInputListener((data) => {
  if (matchesKey(data, 'ctrl+c')) {
    tui.stop();
    process.exit(0);
  }
});

// Start
tui.start();
```

## 核心 API

### 图瓦卢

管理组件和渲染的主要容器.

```typescript
const tui = new TUI(terminal);
tui.addChild(component);
tui.removeChild(component);
tui.start();
tui.stop();
tui.requestRender(); // Request a re-render

// Global debug key handler (Shift+Ctrl+D)
tui.onDebug = () => console.log("Debug triggered");
```

### 重叠

重叠将组件置于现有内容之上而不取而代之。 用于对话框、菜单和模式UI.

```typescript
// Show overlay with default options (centered, max 80 cols)
const handle = tui.showOverlay(component);

// Show overlay with custom positioning and sizing
// Values can be numbers (absolute) or percentage strings (e.g., "50%")
const handle = tui.showOverlay(component, {
  // Sizing
  width: 60,              // Fixed width in columns
  width: "80%",           // Width as percentage of terminal
  minWidth: 40,           // Minimum width floor
  maxHeight: 20,          // Maximum height in rows
  maxHeight: "50%",       // Maximum height as percentage of terminal

  // Anchor-based positioning (default: 'center')
  anchor: 'bottom-right', // Position relative to anchor point
  offsetX: 2,             // Horizontal offset from anchor
  offsetY: -1,            // Vertical offset from anchor

  // Percentage-based positioning (alternative to anchor)
  row: "25%",             // Vertical position (0%=top, 100%=bottom)
  col: "50%",             // Horizontal position (0%=left, 100%=right)

  // Absolute positioning (overrides anchor/percent)
  row: 5,                 // Exact row position
  col: 10,                // Exact column position

  // Margin from terminal edges
  margin: 2,              // All sides
  margin: { top: 1, right: 2, bottom: 1, left: 2 },

  // Responsive visibility
  visible: (termWidth, termHeight) => termWidth >= 100  // Hide on narrow terminals

  // Focus behavior
  nonCapturing: true       // Don't auto-focus when shown
});

// OverlayHandle methods
handle.hide();              // Permanently remove the overlay
handle.setHidden(true);     // Temporarily hide (can show again)
handle.setHidden(false);    // Show again after hiding
handle.isHidden();          // Check if temporarily hidden
handle.focus();             // Focus and bring to visual front
handle.unfocus();           // Release focus to normal fallback
handle.unfocus({ target: baseComponent }); // Release this overlay to a specific component
handle.unfocus({ target: null });   // Release this overlay and leave focus empty
handle.isFocused();         // Check if overlay has focus

handle.unfocus();
// Overlay loses focus; TUI falls back to another visible capturing overlay or the previous focus target.

handle.unfocus({ target: null });
// Overlay loses focus; no component receives input until focus is set again.

// A focused visible overlay reclaims keyboard input after temporary replacement UI
// releases focus. If you want a specific component to receive input while overlays remain
// visible, call handle.unfocus({ target: component }).

// Hide topmost overlay
tui.hideOverlay();

// Check if any visible overlay is active
tui.hasOverlay();
```

**锁定值**数字 : `'center'`, (中文(简体) ), `'top-left'`, (中文(简体) ), `'top-right'`, (中文(简体) ), `'bottom-left'`, (中文(简体) ), `'bottom-right'`, (中文(简体) ), `'top-center'`, (中文(简体) ), `'bottom-center'`, (中文(简体) ), `'left-center'`, (中文(简体) ), `'right-center'`

**决议令**数字 :

1.  `minWidth` 宽度计算后作为下限应用
2.  职位:绝对 `row`编号 :`col` > 百分比 `row`编号 :`col` > 翻译: `anchor`
3.  `margin` 夹住最后位置以停留在终端界限内
4.  `visible` 回调控制覆盖是否构成( 称为每个框架)

### 组件接口

所有组成部分执行:

```typescript
interface Component {
  render(width: number): string[];
  handleInput?(data: string): void;
  handleClick?(localY: number, width: number, localX?: number): boolean;
  invalidate?(): void;
}
```

| 方法 | 说明 |
| --- | --- |
| `render(width)` | 返回一组字符串,每行一个。 每行 **不得超过 `width`** 否则TUI会出错. 使用 `truncateToWidth()` 或人工包装以确保这一点. |
| `handleInput?(data)` | 当组件有焦点并接收键盘输入时调用. 这个 `data` 字符串包含原始终端输入(可能包括ANSI逃生序列). |
| `handleClick?(localY, width, localX?)` | 请求点击组件内部零基坐标 . `localX` 是对与行处理器兼容的可选。 返回时 `true` 当处理. |
| `invalidate?()` | 请求清除缓存的状态 。 部件应该从零开始在下一个 `render()` 电话. |

TUI 附加一个完整的 SGR 重置, OSC 8 重置在每个已渲染行的末尾. 样式不会跨行。 如果您发出多行文本带有样式, 每行重应用样式或使用 `wrapTextWithAnsi()` 因此,样式被保留在每条被包裹的行上.

### 可焦点接口( IME 支持)

显示文本光标并需要 IME (输入法编辑器) 支持的组件应当执行 `Focusable` 接口 :

```typescript
import { CURSOR_MARKER, type Component, type Focusable } from "@ashx-j/lunr-tui";

class MyInput implements Component, Focusable {
  focused: boolean = false;  // Set by TUI when focus changes
  
  render(width: number): string[] {
    const marker = this.focused ? CURSOR_MARKER : "";
    // Emit marker right before the fake cursor
    return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
  }
}
```

当a `Focusable` 部分有重点,TUI:

1.  设定 `focused = true` 内容
2.  为 `CURSOR_MARKER` (零宽装甲运兵车出逃序列)
3.  将硬件终端光标定位在该位置
4.  仅在硬件光标时显示 `showHardwareCursor` 已启用

光标默认仍然隐藏 。 这保持了假光标渲染,同时仍然将硬件光标定位到以隐藏光标跟踪IME候选窗口的终端上. 一些终端需要一个可见的IME定位硬件光标; 启用它与 `TUI` 构造器选项, `setShowHardwareCursor(true)`,或 `PI_HARDWARE_CURSOR=1`编辑 `Editor` 财务报告和审定财务报表 `Input` 内建组件已经执行此接口.

**装有输入的容器组件:** 当容器组件(对话框、选择器等)包含一个 `Input` 或者说 `Editor` 儿童,容器必须执行 `Focusable` 并向儿童宣传重点状态:

```typescript
import { Container, type Focusable, Input } from "@ashx-j/lunr-tui";

class SearchDialog extends Container implements Focusable {
  private searchInput: Input;

  // Propagate focus to child input for IME cursor positioning
  private _focused = false;
  get focused(): boolean { return this._focused; }
  set focused(value: boolean) {
    this._focused = value;
    this.searchInput.focused = value;
  }

  constructor() {
    super();
    this.searchInput = new Input();
    this.addChild(this.searchInput);
  }
}
```

没有这种传播, 输入一个IME( 中文、 日文、 韩文等) 将会显示候选人的窗口位置错误 .

## 内建组件

### 集装箱

儿童组.

```typescript
const container = new Container();
container.addChild(component);
container.removeChild(component);
```

### 方框

适用于所有儿童的贴花和背景颜色的容器.

```typescript
const box = new Box(
  1,                              // paddingX (default: 1)
  1,                              // paddingY (default: 1)
  (text) => chalk.bgGray(text)   // optional background function
);
box.addChild(new Text("Content"));
box.setBgFn((text) => chalk.bgBlue(text));  // Change background dynamically
```

### 文本

显示多行文字并用字包和粘贴.

```typescript
const text = new Text(
  "Hello World",                  // text content
  1,                              // paddingX (default: 1)
  1,                              // paddingY (default: 1)
  (text) => chalk.bgGray(text)   // optional background function
);
text.setText("Updated text");
text.setCustomBgFn((text) => chalk.bgBlue(text));
```

### 截断文本

单行文本可切换以适应取景口宽度. 用于状态行和标题 .

```typescript
const truncated = new TruncatedText(
  "This is a very long line that will be truncated...",
  0,  // paddingX (default: 0)
  0   // paddingY (default: 0)
);
```

### 投入

单行文字输入并横向滚动.

```typescript
const input = new Input();
input.onSubmit = (value) => console.log(value);
input.setValue("initial");
input.getValue();
```

**密钥绑定 :**

-   `Enter` - 提交
-   `Ctrl+A` 编号 : `Ctrl+E` - 线路始末
-   `Ctrl+W` 或者说 `Alt+Backspace` - 向后删除单词
-   `Ctrl+U` - 删除为行首
-   `Ctrl+K` - 删除到行尾
-   `Ctrl+Left` 编号 : `Ctrl+Right` - 文字导航
-   `Alt+Left` 编号 : `Alt+Right` - 文字导航
-   箭头密钥, 后空格, 按预期删除工作

### 编辑器

多行文本编辑器,具有自动完成,文件完成,粘贴处理等功能,并在内容超过终端高度时进行垂直滚动.

```typescript
interface EditorTheme {
  borderColor: (str: string) => string;
  selectList: SelectListTheme;
}

interface EditorOptions {
  paddingX?: number;  // Horizontal padding (default: 0)
}

const editor = new Editor(tui, theme, options?);  // tui is required for height-aware scrolling
editor.onSubmit = (text) => console.log(text);
editor.onChange = (text) => console.log("Changed:", text);
editor.disableSubmit = true; // Disable submit temporarily
editor.setAutocompleteProvider(provider);
editor.borderColor = (s) => chalk.blue(s); // Change border dynamically
editor.setPaddingX(1); // Update horizontal padding dynamically
editor.getPaddingX();  // Get current padding
```

**特征 :**

-   多行编辑并装有字包
-   斜线命令自动完成( 类型) `/`(中文(简体) )
-   文件路径自动完成( 按 `Tab`(中文(简体) )
-   大型粘贴处理( > 10行创建) `[paste #1 +50 lines]` 标记)
-   水平行上方/下方编辑器
-   假光标渲染( 隐藏真实光标)

**密钥绑定 :**

-   `Enter` - 提交
-   `Shift+Enter`, (中文(简体) ), `Ctrl+Enter`,或 `Alt+Enter` - 新线(依赖地铁,Alt+Enter最可靠)
-   `Tab` - 自动完成
-   `Ctrl+K` - 删除到行尾
-   `Ctrl+U` - 删除为行首
-   `Ctrl+W` 或者说 `Alt+Backspace` - 向后删除单词
-   `Alt+D` 或者说 `Alt+Delete` - 删除前面的字
-   `Ctrl+A` 编号 : `Ctrl+E` - 线路始末
-   `Ctrl+]` - 跳到字符上(听到下个按键,然后将光标移动到第一次出现)
-   `Ctrl+Alt+]` - 向后跳转到字符
-   箭头密钥, 后空格, 按预期删除工作

### 标记

以语法加亮和主题支持来标记下.

```typescript
interface MarkdownTheme {
  heading: (text: string) => string;
  link: (text: string) => string;
  linkUrl: (text: string) => string;
  code: (text: string) => string;
  codeBlock: (text: string) => string;
  codeBlockBorder: (text: string) => string;
  quote: (text: string) => string;
  quoteBorder: (text: string) => string;
  hr: (text: string) => string;
  listBullet: (text: string) => string;
  bold: (text: string) => string;
  italic: (text: string) => string;
  strikethrough: (text: string) => string;
  underline: (text: string) => string;
  highlightCode?: (code: string, lang?: string) => string[];
}

interface DefaultTextStyle {
  color?: (text: string) => string;
  bgColor?: (text: string) => string;
  bold?: boolean;
  italic?: boolean;
  strikethrough?: boolean;
  underline?: boolean;
}

const md = new Markdown(
  "# Hello\n\nSome **bold** text",
  1,              // paddingX
  1,              // paddingY
  theme,          // MarkdownTheme
  defaultStyle    // optional DefaultTextStyle
);
md.setText("Updated markdown");
```

**特征 :**

-   标题、粗体、斜体、代码块、列表、链接、块词
-   HTML 标记作为纯文本渲染
-   可选语法高亮通过 `highlightCode`
-   铺设支持
-   为性能进行缓冲

### 装入器

动画装入自旋器.

```typescript
const loader = new Loader(
  tui,                              // TUI instance for render updates
  (s) => chalk.cyan(s),            // spinner color function
  (s) => chalk.gray(s),            // message color function
  "Loading..."                      // message (default: "Loading...")
);
loader.start();
loader.setMessage("Still loading...");
loader.stop();
```

### 可切换连接器

以 Escape 密钥处理方式扩展加载器, 并中止用于取消ASync 操作的信号 .

```typescript
const loader = new CancellableLoader(
  tui,                              // TUI instance for render updates
  (s) => chalk.cyan(s),            // spinner color function
  (s) => chalk.gray(s),            // message color function
  "Working..."                      // message
);
loader.onAbort = () => done(null); // Called when user presses Escape
doAsyncWork(loader.signal).then(done);
```

**属性 :**

-   `signal: AbortSignal` - 用户按出逃时中止
-   `aborted: boolean` - 装载器是否中止
-   `onAbort?: () => void` - 用户按出逃时召回

### 选择列表

与键盘导航交互选择列表 .

```typescript
interface SelectItem {
  value: string;
  label: string;
  description?: string;
}

interface SelectListTheme {
  selectedPrefix: (text: string) => string;
  selectedText: (text: string) => string;
  description: (text: string) => string;
  scrollInfo: (text: string) => string;
  noMatch: (text: string) => string;
}

const list = new SelectList(
  [
    { value: "opt1", label: "Option 1", description: "First option" },
    { value: "opt2", label: "Option 2", description: "Second option" },
  ],
  5,      // maxVisible
  theme   // SelectListTheme
);

list.onSelect = (item) => console.log("Selected:", item);
list.onCancel = () => console.log("Cancelled");
list.onSelectionChange = (item) => console.log("Highlighted:", item);
list.setFilter("opt"); // Filter items
```

**控件 :**

-   箭头密钥: 导航
-   输入: 选择
-   逃脱: 取消

### 设置列表

设置带有值循环和子菜单的面板 .

```typescript
interface SettingItem {
  id: string;
  label: string;
  description?: string;
  currentValue: string;
  values?: string[];  // If provided, Enter/Space cycles through these
  submenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;
}

interface SettingsListTheme {
  label: (text: string, selected: boolean) => string;
  value: (text: string, selected: boolean) => string;
  description: (text: string) => string;
  cursor: string;
  hint: (text: string) => string;
}

const settings = new SettingsList(
  [
    { id: "theme", label: "Theme", currentValue: "dark", values: ["dark", "light"] },
    { id: "model", label: "Model", currentValue: "gpt-4", submenu: (val, done) => modelSelector },
  ],
  10,      // maxVisible
  theme,   // SettingsListTheme
  (id, newValue) => console.log(`${id} changed to ${newValue}`),
  () => console.log("Cancelled")
);
settings.updateValue("theme", "light");
```

**控件 :**

-   箭头密钥: 导航
-   输入/ 空间: 激活( 周期值或打开子菜单)
-   逃脱: 取消

### 空间器

垂直间距的空行.

```typescript
const spacer = new Spacer(2); // 2 empty lines (default: 1)
```

### 图像

将支持 Kitty 图形协议( Kitty, Ghostty, Wez Term) 或 iTerm2 内置图像的终端内置图像 。 返回到不支持的终端上的文本占位符 .

```typescript
interface ImageTheme {
  fallbackColor: (str: string) => string;
}

interface ImageOptions {
  maxWidthCells?: number;
  maxHeightCells?: number;
  filename?: string;
}

const image = new Image(
  base64Data,       // base64-encoded image data
  "image/png",      // MIME type
  theme,            // ImageTheme
  options           // optional ImageOptions
);
tui.addChild(image);
```

所支持的格式:PNG,JPEG,GIF,WebP. 从图像头自动解析尺寸 .

## 自动完成

### 组合自动完成器

支持斜线命令和文件路径.

```typescript
import { CombinedAutocompleteProvider } from "@ashx-j/lunr-tui";

const provider = new CombinedAutocompleteProvider(
  [
    { name: "help", description: "Show help" },
    { name: "clear", description: "Clear screen" },
    { name: "delete", description: "Delete last message" },
  ],
  process.cwd() // base path for file completion
);

editor.setAutocompleteProvider(provider);
```

**特征 :**

-   类型 `/` 显示斜线命令
-   新闻 `Tab` 文件路径补全
-   与 `~/`, (中文(简体) ), `./`, (中文(简体) ), `../`,以及 `@` 前缀
-   可附加文件的过滤器 `@` 前缀

## 密钥检测

使用 `matchesKey()` 与 `Key` 用于检测键盘输入的助手( 支持 Kitty 键盘协议 ) :

```typescript
import { matchesKey, Key } from "@ashx-j/lunr-tui";

if (matchesKey(data, Key.ctrl("c"))) {
  process.exit(0);
}

if (matchesKey(data, Key.enter)) {
  submit();
} else if (matchesKey(data, Key.escape)) {
  cancel();
} else if (matchesKey(data, Key.up)) {
  moveUp();
}
```

**关键标识符** (使用时 `Key.*` 自动补全,或字符串字元:

-   基本密钥 : `Key.enter`, (中文(简体) ), `Key.escape`, (中文(简体) ), `Key.tab`, (中文(简体) ), `Key.space`, (中文(简体) ), `Key.backspace`, (中文(简体) ), `Key.delete`, (中文(简体) ), `Key.home`, (中文(简体) ), `Key.end`
-   箭头键 : `Key.up`, (中文(简体) ), `Key.down`, (中文(简体) ), `Key.left`, (中文(简体) ), `Key.right`
-   带有修改符 : `Key.ctrl("c")`, (中文(简体) ), `Key.shift("tab")`, (中文(简体) ), `Key.alt("left")`, (中文(简体) ), `Key.ctrlShift("p")`
-   字符串格式也起作用 : `"enter"`, (中文(简体) ), `"ctrl+c"`, (中文(简体) ), `"shift+tab"`, (中文(简体) ), `"ctrl+shift+p"`

## 区别对待

TUI使用三个渲染策略:

1.  **第一个**: 输出所有行而不清理回滚
2.  **视图端口上方的宽度已更改或更改**: 清除屏幕并全部重置
3.  **普通更新**: 将光标移动到第一个已更改行上, 清除到结束, 渲染已更改行

所有更新都包含在 **同步输出** (单位:千美元)`\x1b[?2026h` . `\x1b[?2026l`用于原子,无闪光渲染.

## 终端接口

TUI 与执行 `Terminal` 接口 :

```typescript
interface Terminal {
  start(onInput: (data: string) => void, onResize: () => void): void;
  stop(): void;
  write(data: string): void;
  get columns(): number;
  get rows(): number;
  moveBy(lines: number): void;
  hideCursor(): void;
  showCursor(): void;
  clearLine(): void;
  clearFromCursor(): void;
  clearScreen(): void;
}
```

**内建实施:**

-   `ProcessTerminal` - 用途 `process.stdin/stdout`
-   `VirtualTerminal` - 测试(用途) `@xterm/headless`(中文(简体) )

## 公用事业

```typescript
import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@ashx-j/lunr-tui";

// Get visible width of string (ignoring ANSI codes)
const width = visibleWidth("\x1b[31mHello\x1b[0m"); // 5

// Truncate string to width (preserving ANSI codes, adds ellipsis)
const truncated = truncateToWidth("Hello World", 8); // "Hello..."

// Truncate without ellipsis
const truncatedNoEllipsis = truncateToWidth("Hello World", 8, ""); // "Hello Wo"

// Wrap text to width (preserving ANSI codes across line breaks)
const lines = wrapTextWithAnsi("This is a long line that needs wrapping", 20);
// ["This is a long line", "that needs wrapping"]
```

## 创建自定义组件

当创建自定义组件时, **返回的每行 `render()` 不得超过 `width` 参数**。如果有线比终端宽,则TUI会出错.

### 处理输入

使用 `matchesKey()` 与 `Key` 键盘输入帮助器 :

```typescript
import { matchesKey, Key, truncateToWidth } from "@ashx-j/lunr-tui";
import type { Component } from "@ashx-j/lunr-tui";

class MyInteractiveComponent implements Component {
  private selectedIndex = 0;
  private items = ["Option 1", "Option 2", "Option 3"];
  
  public onSelect?: (index: number) => void;
  public onCancel?: () => void;

  handleInput(data: string): void {
    if (matchesKey(data, Key.up)) {
      this.selectedIndex = Math.max(0, this.selectedIndex - 1);
    } else if (matchesKey(data, Key.down)) {
      this.selectedIndex = Math.min(this.items.length - 1, this.selectedIndex + 1);
    } else if (matchesKey(data, Key.enter)) {
      this.onSelect?.(this.selectedIndex);
    } else if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
      this.onCancel?.();
    }
  }

  render(width: number): string[] {
    return this.items.map((item, i) => {
      const prefix = i === this.selectedIndex ? "> " : "  ";
      return truncateToWidth(prefix + item, width);
    });
  }
}
```

### 处理行宽

利用所提供的水电设施确保线路符合:

```typescript
import { visibleWidth, truncateToWidth } from "@ashx-j/lunr-tui";
import type { Component } from "@ashx-j/lunr-tui";

class MyComponent implements Component {
  private text: string;

  constructor(text: string) {
    this.text = text;
  }

  render(width: number): string[] {
    // Option 1: Truncate long lines
    return [truncateToWidth(this.text, width)];

    // Option 2: Check and pad to exact width
    const line = this.text;
    const visible = visibleWidth(line);
    if (visible > width) {
      return [truncateToWidth(line, width)];
    }
    // Pad to exact width (optional, for backgrounds)
    return [line + " ".repeat(width - visible)];
  }
}
```

### ANSI 守则考虑

两者 `visibleWidth()` 财务报告和审定财务报表 `truncateToWidth()` 正确处理 ANSI 逃生代码 :

-   `visibleWidth()` 计算宽度时忽略 ANSI 代码
-   `truncateToWidth()` 保存 ANSI 代码并在切换时正确关闭

```typescript
import chalk from "chalk";

const styled = chalk.red("Hello") + " " + chalk.blue("World");
const width = visibleWidth(styled); // 11 (not counting ANSI codes)
const truncated = truncateToWidth(styled, 8); // Red "Hello" + " W..." with proper reset
```

### 缓存

就性能而言,组件应缓存其产出,并仅在必要时重新交付:

```typescript
class CachedComponent implements Component {
  private text: string;
  private cachedWidth?: number;
  private cachedLines?: string[];

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

    const lines = [truncateToWidth(this.text, width)];

    this.cachedWidth = width;
    this.cachedLines = lines;
    return lines;
  }

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

## 示例

见 `test/chat-simple.ts` 用于完整聊天接口示例:

-   带有自定义背景颜色的下沉信件
-   回复时正在装入自旋器
-   有自动完成和斜线命令的编辑器
-   信件之间的空格

运行它:

```bash
npx tsx test/chat-simple.ts
```

## 发展

```bash
# Install dependencies (from monorepo root)
npm install

# Run type checking
npm run check

# Run the demo
npx tsx test/chat-simple.ts
```

### 调试日志

设定 `PI_TUI_WRITE_LOG` 以捕捉写成 stdout 的原始 ANSI 流.

```bash
PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx test/chat-simple.ts
```

## 常见问题

### @ashx-j/lunr-tui 是什么？

终端用户界面库,用于高效文本应用程序的差分渲染

### 如何安装 @ashx-j/lunr-tui？

在项目目录运行 npm install @ashx-j/lunr-tui，或使用 pnpm add / yarn add / bun add。

### @ashx-j/lunr-tui 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @ashx-j/lunr-tui 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @ashx-j/lunr-tui 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=22\.19\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @ashx-j/lunr-tui 可以商用吗？

@ashx-j/lunr-tui 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @ashx-j/lunr-tui 的下载量有多少？

根据 npm 官方下载统计，@ashx-j/lunr-tui 在 2026/10/07 – 2026/11/05 这 30 天内被下载 2,369 次，日均约 79 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @ashx-j/lunr-tui 还在维护吗？

最新版本 0\.2\.40 发布于 2026/10/25（13 天前）；至今共发布 59 个版本，其中过去 12 个月发布了 59 个；首次发布于 2026/08/16。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

