@simon_he/vue-tui
v0.0.3
Published
A Vue 3 terminal UI component library
Readme
Vue Terminal
用 Vue 组件语法绘制和交互的终端渲染框架。目标是让 “终端” 具备:
- 类终端的写入/删除/更新/尺寸调整能力
- ANSI 样式渲染(颜色、加粗、下划线等)
- 事件机制(点击、移动、滚动、拖拽)
- Vue 组件 + 响应式数据驱动终端界面更新
快速开始
浏览器渲染
<script setup lang="ts">
import { computed, ref } from 'vue'
import { TerminalProvider, TBox, TInput, TText, useLayout } from '@simon_he/vue-tui'
const layout = useLayout()
const cols = computed(() => layout.clipRect?.w ?? 80)
const rows = computed(() => layout.clipRect?.h ?? 24)
const input = ref('')
</script>
<template>
<TerminalProvider :cols="80" :rows="24" :default-style="{ fg: 'whiteBright' }">
<TBox :x="0" :y="0" :w="80" :h="24" border title="Demo" :padding="1" :style="{ fg: 'blueBright' }">
<TText :x="0" :y="0" :w="78" :value="`cols=${cols} rows=${rows}`" />
<TInput :x="0" :y="20" :w="78" v-model="input" placeholder="Type..." />
</TBox>
</TerminalProvider>
</template>终端(真实 CLI)渲染
内置 vue-terminal run,直接在真实终端中跑 UI(stdout renderer + stdin driver):
pnpm build
node cli.mjs --app basicGoatChain(示例应用)
GoatChain 风格的示例应用放在 examples/goatchain(支持 browser + terminal 构建)。
示例工程
# 浏览器示例(交互测试)
pnpm -C examples/basic dev
pnpm -C examples/goatchain dev
# 打包 browser
pnpm build:examples
# 打包成 terminal 产物并运行
pnpm build:examples:terminal
pnpm run:basic:terminal
pnpm run:goatchain:terminalterminal 构建产物会生成 dist-terminal/terminal.js,可以直接作为 bin 入口使用。
LLM 集成(对接指南)
GoatChain 示例应用目前用的是 mock LLM,但你可以很快替换成任意 LLM SDK(OpenAI/Anthropic/本地模型)。
建议做法:
- 用 ACP 风格的流式协议驱动 UI(支持
text_delta/tool_call_delta/tool_call_end/tool_result/done)。 - tool_call 的执行可以交给 agent 侧(推荐,和 GoatChain CLI 一致),UI 只负责渲染与“暂停→继续”的用户输入。
- 在 UI 侧维护 token 统计与 context window(真实 tokenizer 或近似估算均可)。
GoatChain 的核心协议定义在:
apps/goatchain/src/core/acp.tsapps/goatchain/src/core/model.tsapps/goatchain/src/core/store.tsapps/goatchain/src/tools-mock.ts
你只需要实现一个 GoatChainAgent(receiveMessage(messages, metadata) → AsyncGenerator<ACPMessage>),并传给 GoatChainShell/GoatChainProvider,其余 UI 与交互可以复用。
核心概念
- Buffer: 以 Cell 网格存储字符与样式,负责最小化更新
- Renderer: 将 Buffer diff 渲染为 DOM(后续可扩展 canvas)
- EventManager: DOM 事件 -> cell 坐标 -> 命中测试 -> 派发给组件
- Vue Layer: 提供
TerminalProvider与内置组件 (TText/TBox/TView) - UI Layer: 通用组件与指令(
select,v-if,v-show等) - Vue3 对齐: 事件名称/修饰符/行为尽量与浏览器 Vue3 保持一致
模块分层(建议结构)
src/
core/
buffer/ # Cell 网格、dirty 标记、宽字符
terminal/ # Terminal API(write/clear/resize/batch/commit)
ansi/ # ANSI 解析 -> Style
renderer/
dom/ # 行/Span 渲染、diff 更新
events/
manager/ # 坐标映射、hit test、事件派发
vue/
components/ # TerminalProvider, TText, TBox, TView
composables/ # useTerminal, useTerminalNode
directives/ # v-if, v-show, v-model, v-focus 等
runtime/ # 动态插入、组件工厂、命令式渲染数据流
Vue 组件 -> Terminal API -> Buffer -> Diff -> Renderer -> DOM
DOM 事件 -> EventManager -> HitTest -> Vue 组件事件回调核心数据结构(设计草案)
type Style = {
fg?: string; bg?: string;
bold?: boolean; dim?: boolean; italic?: boolean;
underline?: boolean; inverse?: boolean;
};
type Cell = {
ch: string;
style: Style;
width: 1 | 2; // 宽字符支持
};
type Buffer = {
rows: Cell[][];
cols: number;
rowsCount: number;
dirtyRows: boolean[];
};终端语义范围(初版约束)
write()默认不解析 ANSI,仅做“就地写入”,越界裁剪writeAnsi()解析 ANSI 样式与基础控制序列- 光标与换行:
write()未提供x/y时使用内部 cursor,带x/y不更新全局 cursor scroll()仅滚动可视区域(是否保留 scrollback 由实现决定)
Viewport 与 Scrollback(建议)
- Buffer 区分 viewport(可视区域)与 scrollback(历史区域)
- cursor 驱动写入在底部时超出触发滚动
scroll(lines)只移动 viewport,不影响 buffer 内容- 支持
scrollTo()与 scrollback 上限(内存可控)
Terminal API(设计草案)
interface Terminal {
resize(cols: number, rows: number): void;
clear(x?: number, y?: number, w?: number, h?: number): void;
write(text: string, opts?: { x?: number; y?: number; style?: Style }): void;
writeAnsi(text: string, opts?: { x?: number; y?: number }): void;
put(x: number, y: number, ch: string, style?: Style): void;
fill(x: number, y: number, w: number, h: number, ch?: string, style?: Style): void;
scroll(lines: number): void;
setCursor(x: number, y: number, visible?: boolean): void;
batch(fn: () => void): void;
commit(): void;
on(event: TerminalEvent, cb: (e: TerminalEventPayload) => void): () => void;
dispose(): void;
}事件模型(设计草案)
- 事件名称与 Vue3 DOM 事件对齐(
@click,@keydown,@input等) - DOM 事件转换为 cell 坐标 (cellX/cellY)
- hit test 查找当前坐标命中的节点 (支持 zIndex)
type TerminalEvent =
| 'click' | 'dblclick' | 'contextmenu'
| 'pointerdown' | 'pointerup' | 'pointermove' | 'pointerenter' | 'pointerleave'
| 'mousedown' | 'mouseup' | 'mousemove' | 'mouseenter' | 'mouseleave'
| 'wheel'
| 'keydown' | 'keyup'
| 'input' | 'change'
| 'compositionstart' | 'compositionupdate' | 'compositionend'
| 'focus' | 'blur'
| 'drag';
type TerminalBaseEvent = {
type: TerminalEvent;
target: TerminalNode | null;
currentTarget: TerminalNode | null;
bubbles: boolean;
cancelable: boolean;
defaultPrevented: boolean;
timeStamp: number;
stopPropagation(): void;
preventDefault(): void;
nativeEvent?: Event;
};
type TerminalPointerEvent = TerminalBaseEvent & {
clientX: number;
clientY: number;
cellX: number;
cellY: number;
button?: number;
buttons?: number;
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
deltaY?: number;
};
type TerminalKeyboardEvent = TerminalBaseEvent & {
key: string;
code: string;
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
repeat?: boolean;
};事件派发与冒泡(建议)
- 默认冒泡,支持
stopPropagation()/preventDefault(),语义与 DOM 一致 - 支持捕获阶段(可选),用于全局拦截(与
.capture对齐) - 拖拽使用 pointer capture,保证按下后持续命中
- 支持
contextmenu事件(右键菜单)
事件桥接(建议)
- Terminal 事件与 Vue 组件事件做统一归一化
- 事件路径包含命中节点链,便于冒泡与拦截
pointer事件优先于mouse,便于兼容触控
Vue3 浏览器事件对齐(约定)
- 事件名称:与浏览器 DOM 事件一致(
click,keydown,input,focus等) - 事件修饰符:支持
.stop.prevent.capture.once.passive.self.exact - 键盘修饰符:
.ctrl.shift.alt.meta.enter.esc等与 Vue3 行为一致 - 指针/鼠标:
pointer*优先,mouse*兼容;clientX/clientY保留 - 事件对象:保留原生字段,并额外提供
cellX/cellY与nativeEvent v-model:保持modelValue+update:modelValue的 Vue3 约定
事件映射表(核心字段)
DOM Mouse/Pointer Event -> TerminalPointerEvent
- clientX/clientY 保留,额外提供 cellX/cellY
- button/buttons/ctrlKey/shiftKey/altKey/metaKey 保留
DOM Keyboard Event -> TerminalKeyboardEvent
- key/code/repeat/ctrlKey/shiftKey/altKey/metaKey 保留
DOM Input/Composition Event -> TerminalBaseEvent
- inputType/data 通过 nativeEvent 获取@input / @change 触发规则(建议)
TInput输入过程中触发input,提交时触发changecompositionstart/update/end与浏览器一致,结束后触发一次inputv-model绑定:input时更新modelValue,change作为提交确认
Pointer Capture / Focus / Blur 规则(建议)
pointerdown后启用捕获,直到pointerup释放focus由点击或v-focus触发,blur由切换焦点或卸载触发focusin/focusout可选支持,用于冒泡式焦点事件
Vue 组件层(设计草案)
<TerminalProvider :cols="80" :rows="24">
<TView :x="2" :y="2" :w="20" :h="5" @click="onClick">
<TText :value="title" :style="{ fg: 'green' }" />
<TBox border />
</TView>
</TerminalProvider>TerminalProvider: 创建 terminal、绑定 renderer 和 event managerTView: 提供局部坐标系、事件区域、zIndexTText: 响应式写入文字TBox: 绘制边框、标题
通用组件与指令
- 通用组件:
TSelect,TInput,TList,TTable,TDialog - 指令:
v-if: 条件渲染/卸载组件,释放 buffer 区域v-show: 仅隐藏展示,保持节点存在以保留事件与状态v-model: 绑定TInput文本与光标状态v-focus: 让组件获得焦点(键盘输入)
动态插入组件(命令式)
支持在事件回调中插入组件,例如监听 input 后插入 TSelect。
通过 useTerminalRuntime() 或 createPortal() 提供命令式入口。
const { mount } = useTerminalRuntime()
function onInputCommit(value: string) {
if (value.startsWith('/')) {
mount(TSelect, {
x: 2, y: 10, w: 20, h: 6,
options: ['a', 'b', 'c'],
onSelect: v => console.log(v),
})
}
}约束:
- 动态插入的组件必须显式提供布局 (x/y/w/h) 或依赖父容器布局
- 插入/卸载会触发局部重绘,不影响其它区域
命令式渲染协议(建议)
mount()返回 handle,包含update()/unmount()/move()等操作- 允许传入
key复用实例,避免频繁销毁 TerminalProvider卸载时自动清理所有命令式节点
type TerminalHandle = {
update: (props: Record<string, unknown>) => void;
move: (x: number, y: number) => void;
unmount: () => void;
};组件生命周期与资源管理(建议)
- 挂载:注册事件节点、占用 buffer 区域、写入初始内容
- 更新:清理旧区域 + 写入新内容,保持最小 dirty 范围
- 卸载:释放事件节点、清理 buffer 区域、移除焦点/输入绑定
对 v-if/v-show 的行为约定:
v-if: 触发卸载与资源释放(清 buffer、解绑事件)v-show: 仅隐藏渲染,保留节点与状态(不解绑事件)
焦点与输入协议(建议)
- 引入 focus manager,维护当前焦点组件
v-focus/focus()触发焦点切换,自动更新光标样式- 键盘事件仅派发给焦点组件
TInput需要处理:- 文本插入、删除、移动光标
- 组合输入(IME)阶段的暂存显示
- Enter/Escape 行为(提交/取消)
键盘与快捷键(建议)
- 统一规范化键名(如
Enter/Esc/ArrowUp) - 仅焦点组件接收输入,支持全局快捷键注册
- 支持组合键与平台差异(macOS/Windows/Linux)
常见组件交互约定(示例)
TSelect:- 上下键切换选项,Enter 确认,Esc 取消
onOpen/onClose用于显示/隐藏下拉
TList:- 支持滚动、虚拟化(列表过长时)
TDialog:- 居中布局、遮罩区域、阻止底层事件
响应式与尺寸变化(建议)
TerminalProvider监听容器尺寸变化,触发resize(cols, rows)并广播resize事件- 组件接收
onResize或useTerminalSize()响应尺寸变化 - 布局建议:
TView支持w/h为百分比或auto(依赖父容器)- resize 时触发局部重排,避免全量重绘
布局与坐标系统(建议)
- 坐标系基于 cell 网格,所有位置/尺寸以整数为主
- 布局模式:
absolute:x/y/w/h明确指定flow: 垂直/水平流布局(简化版)anchor: 支持left/right/top/bottom锚定
- 约束:
minW/maxW/minH/maxH,超出时裁剪 - 容器提供
padding与clip,子组件默认裁剪到内容区域
布局计算(简版规则)
- 先计算父容器 contentRect,再计算子组件布局
absolute优先:有明确x/y/w/h时直接裁剪anchor次之:根据边距计算剩余空间flow最后:按方向累积尺寸,支持gap与wrapauto尺寸:基于内容测量(文本/子组件)得到
带边框 Box 的内容保护
当 TBox 带边框时,需要保护内容区域不被破坏:
- 外框占 1 cell 边距,内容区域为
(x+1, y+1, w-2, h-2) - 内容写入必须裁剪到内容区域,避免覆盖边框
- 当内容超出时,支持
overflow: clip | scroll - 建议
TBox内部提供padding与contentRect,子组件写入基于contentRect
渲染策略(初版)
- 以行 (row) 为单位进行 dirty 标记
- 每行合并相同样式为 span,减少 DOM 节点数
batch()+commit()合并更新
渲染调度(建议)
- 所有写入走调度器聚合到下一帧(
requestAnimationFrame) batch()在同一帧内合并多个写入,减少 diff 次数- 支持
flush()立即渲染(用于调试或关键交互)
字体度量与像素映射(建议)
- 统一使用等宽字体,测量
cellWidth/cellHeight - 设备像素比变化时重新测量并触发重排
- 支持固定
cellSize覆盖测量结果(用于稳定布局)
ANSI 支持
writeAnsi()接受 ANSI 字符串,解析后写入 Buffer- 可先实现轻量解析器,后续切换到
ansi-styles/ansis
文本分段与宽字符策略(建议)
- 使用 grapheme 分段,避免组合字符被拆分
- 宽字符占用 2 cell,后续 cell 标记为 continuation
- 无法渲染的字符使用占位符(如
?)并记录告警
主题与样式系统(建议)
- 提供
Theme对象(默认 fg/bg、16/256 色板、字体设置) - ANSI 颜色映射依赖主题,可热更新
- 允许组件局部覆盖主题(如
TDialog)
光标、选择与剪贴板(建议)
- 光标形态:
block/underline/bar,支持闪烁 - 选择:鼠标拖拽选区,支持跨行
- 剪贴板:复制/粘贴基于 Buffer 内容而非 DOM 文本
设计注意事项
- 文本宽度:需支持 grapheme + wcwidth,避免 emoji/组合字符错位
- 组合模型:建议引入容器裁剪与 zIndex,避免组件相互覆盖
- 调度机制:统一 batch/commit,避免组件更新引发频繁重绘
- 事件映射:需基于字体度量映射 cell 坐标,resize 时重算
- 滚动与 resize:需要明确 scrollback 与重排策略
后续计划(草案)
- 基础组件库完善 (TList/TInput/TTable)
- Canvas 渲染器
- 虚拟滚动和大 buffer 支持
