first commit

This commit is contained in:
douboer@gmail.com
2026-03-03 13:23:14 +08:00
commit 3b7c1d558a
161 changed files with 28120 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
/**
* @remoteconn/terminal-core
* 零 DOM 跨平台终端核心库 — 供 Web/iOS/微信小程序三端共用。
*/
// ── 基础类型 ──────────────────────────────────────────────────────────────────
export type {
SessionState,
ConnectParams,
TerminalCredential,
FrameMeta,
TransportEvent,
TransportEventListener,
ColorValue,
CharAttribs,
TerminalCell,
TerminalLine,
CursorState,
TerminalSnapshot,
} from "./types";
export {
DEFAULT_FG,
DEFAULT_BG,
DEFAULT_COLOR,
FLAG_BOLD,
FLAG_DIM,
FLAG_ITALIC,
FLAG_UNDERLINE,
FLAG_BLINK,
FLAG_INVERSE,
FLAG_INVISIBLE,
FLAG_STRIKETHROUGH,
FLAG_OVERLINE,
makeDefaultAttribs,
copyAttribs,
makeBlankCell,
} from "./types";
// ── 接口 ──────────────────────────────────────────────────────────────────────
export type { TerminalTransport } from "./transport/terminalTransport";
export type { RendererAdapter } from "./renderer/rendererAdapter";
export type { IInputSource, InputEventMap, KeyPayload, InputPayload, PastePayload, CompositionPayload } from "./input/IInputSource";
export type { IMeasureAdapter } from "./layout/IMeasureAdapter";
// ── Session ───────────────────────────────────────────────────────────────────
export { SessionMachine, canTransition, assertTransition } from "./session/sessionMachine";
// ── Core ──────────────────────────────────────────────────────────────────────
export { TerminalCore } from "./renderer/terminalCore";
export { OutputBuffer } from "./renderer/outputBuffer";
export type { OutputBufferOptions } from "./renderer/outputBuffer";
// ── Input ─────────────────────────────────────────────────────────────────────
export { InputBridge } from "./input/inputBridge";
export type { InputBridgeOptions, CursorKeyMode } from "./input/inputBridge";
export { ImeController } from "./input/imeController";
export type { ImeState } from "./input/imeController";
// ── Layout ────────────────────────────────────────────────────────────────────
export { calcSize, sizeChanged } from "./layout/sizeCalculator";
export type { SizeResult } from "./layout/sizeCalculator";
export { toGlobalRow, isInBand, textCursorPos, charDisplayWidth } from "./layout/cursorMath";
export type { CursorPos } from "./layout/cursorMath";
// ── Sanitize ──────────────────────────────────────────────────────────────────
export { sanitizeTerminalOutput } from "./sanitize/terminalSanitizer";

View File

@@ -0,0 +1,42 @@
/**
* IInputSource — 平台输入事件源抽象接口Web DOM / 小程序 bindinput
* 所有实现在 apps/* 中packages/terminal-core 只定义接口。
*/
export interface KeyPayload {
key: string;
code: string;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
isComposing: boolean;
}
export interface InputPayload {
data: string;
isComposing: boolean;
}
export interface PastePayload {
text: string;
}
export interface CompositionPayload {
data: string;
}
export type InputEventMap = {
key: KeyPayload;
input: InputPayload;
paste: PastePayload;
compositionstart: CompositionPayload;
compositionend: CompositionPayload;
};
export interface IInputSource {
on<K extends keyof InputEventMap>(
event: K,
cb: (payload: InputEventMap[K]) => void
): () => void;
}

View File

@@ -0,0 +1,81 @@
/**
* ImeController — IME 输入法状态机(纯逻辑,零 DOM 事件依赖)。
*
* 状态:
* idle → composingcompositionstart
* composing → commit_pendingcompositionend
* commit_pending → idle提交完成或超时
*
* 平台层Web/小程序)负责订阅 compositionstart/end 后调用此 controller 的方法。
*/
export type ImeState = "idle" | "composing" | "commit_pending";
export class ImeController {
private _state: ImeState = "idle";
/** 当前组合字符串 */
private _composingData = "";
/** 防止 compositionend 后对应的 input 事件重复提交的保护窗口ms */
private _commitGuardUntil = 0;
/** 超时守卫 timer无 DOM API — 由平台层通过注入的 setTimeoutFn 驱动) */
private _guardTimer: ReturnType<typeof setTimeout> | null = null;
constructor(
private readonly setTimeoutFn: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>,
private readonly clearTimeoutFn: (id: ReturnType<typeof setTimeout>) => void,
/** 超时自动复位ms默认 2000 */
private readonly timeoutMs = 2000
) {}
get state(): ImeState { return this._state; }
get composingData(): string { return this._composingData; }
get isComposing(): boolean { return this._state !== "idle"; }
onCompositionStart(data: string): void {
this._clearGuard();
this._state = "composing";
this._composingData = data;
// 安全超时:若 compositionend 迟迟不来,自动复位
this._guardTimer = this.setTimeoutFn(() => {
if (this._state !== "idle") {
this._state = "idle";
this._composingData = "";
}
}, this.timeoutMs);
}
onCompositionEnd(data: string): string {
this._clearGuard();
this._state = "commit_pending";
this._composingData = data;
// 设置提交保护窗口(防 input 事件重复触发)
this._commitGuardUntil = Date.now() + 50;
const committed = data;
this._state = "idle";
this._composingData = "";
return committed;
}
/**
* 判断当前 input 事件是否应被 IME 消耗(不应单独发送给 Transport
*/
shouldConsumeInputEvent(): boolean {
if (this._state === "composing") return true;
if (Date.now() < this._commitGuardUntil) return true;
return false;
}
reset(): void {
this._clearGuard();
this._state = "idle";
this._composingData = "";
this._commitGuardUntil = 0;
}
private _clearGuard(): void {
if (this._guardTimer !== null) {
this.clearTimeoutFn(this._guardTimer);
this._guardTimer = null;
}
}
}

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { InputBridge } from "./inputBridge";
describe("InputBridge.mapPaste", () => {
it("统一换行为 LF避免将多行粘贴转换为 CR", () => {
const bridge = new InputBridge({ bracketedPaste: false });
const input = "line1\r\nline2\nline3\rline4";
expect(bridge.mapPaste(input)).toBe("line1\nline2\nline3\nline4");
});
it("开启 bracketed paste 时包裹 ESC[200~/ESC[201~", () => {
const bridge = new InputBridge({ bracketedPaste: true });
expect(bridge.mapPaste("a\r\nb")).toBe("\x1b[200~a\nb\x1b[201~");
});
});

View File

@@ -0,0 +1,173 @@
/**
* InputBridge — 键序列映射纯逻辑(零 DOM 事件依赖)。
*
* 负责将逻辑按键/文本映射为发送给 Transport 的 VT 字节串。
* 对齐 xterm.js@5.3.0 src/common/input/Keyboard.ts 行为。
*/
/** 应用光标键模式DECCKM ?1h 激活时使用 SS3 序列) */
export type CursorKeyMode = "normal" | "application";
/** bracketed paste 模式 */
export type BracketedPasteMode = boolean;
export interface InputBridgeOptions {
cursorKeyMode?: CursorKeyMode;
bracketedPaste?: BracketedPasteMode;
}
export class InputBridge {
private cursorKeyMode: CursorKeyMode;
private bracketedPaste: BracketedPasteMode;
constructor(opts: InputBridgeOptions = {}) {
this.cursorKeyMode = opts.cursorKeyMode ?? "normal";
this.bracketedPaste = opts.bracketedPaste ?? false;
}
setCursorKeyMode(mode: CursorKeyMode): void { this.cursorKeyMode = mode; }
setBracketedPaste(enabled: boolean): void { this.bracketedPaste = enabled; }
/**
* 将 KeyboardEvent 语义映射为 VT 字节串。
* 返回 null 表示该按键不产生终端输入(如单纯 modifier key
*/
mapKey(
key: string,
code: string,
ctrlKey: boolean,
altKey: boolean,
shiftKey: boolean,
metaKey: boolean
): string | null {
// --- ASCII 控制字符 ---
if (ctrlKey && !altKey && !metaKey) {
const ctrl = mapCtrlKey(key, code, shiftKey);
if (ctrl !== null) return ctrl;
}
// --- 功能键 ---
const fn = mapFunctionKey(key, code, shiftKey, ctrlKey, altKey, this.cursorKeyMode);
if (fn !== null) return fn;
// --- Alt/Meta 前缀 ---
if ((altKey || metaKey) && key.length === 1) {
return `\x1b${key}`;
}
// --- 普通可打印字符 ---
if (key.length > 0 && !key.startsWith("Dead") && key !== "Unidentified") {
// 过滤掉单纯的 modifier key 名称
if (MODIFIER_KEY_NAMES.has(key)) return null;
return key;
}
return null;
}
/**
* 将粘贴文本处理为终端输入字节串:
* - 行结束统一归一为 LF避免多行粘贴被当作多次回车
* - bracketed paste 模式下用 ESC[200~ / ESC[201~ 包裹
*/
mapPaste(text: string): string {
// 统一处理 CRLF / CR / LF确保多平台粘贴行为一致。
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
if (this.bracketedPaste) {
return `\x1b[200~${normalized}\x1b[201~`;
}
return normalized;
}
}
// ── 内部工具 ──────────────────────────────────────────────────────────────────
const MODIFIER_KEY_NAMES = new Set([
"Control", "Shift", "Alt", "Meta",
"CapsLock", "NumLock", "ScrollLock",
"OS", "Win",
]);
function mapCtrlKey(key: string, _code: string, shiftKey: boolean): string | null {
// CTRL + A-Z → 0x01-0x1A
if (key.length === 1) {
const upper = key.toUpperCase();
const cp = upper.charCodeAt(0);
if (cp >= 65 && cp <= 90) {
return String.fromCharCode(cp - 64);
}
}
// 特殊组合
switch (key) {
case "@": return "\x00";
case "[": return "\x1b";
case "\\": return "\x1c";
case "]": return "\x1d";
case "^": return "\x1e";
case "_": return "\x1f";
case " ": return "\x00";
case "Enter": return shiftKey ? "\n" : "\r";
case "Backspace": return "\x08";
default: return null;
}
}
function mapFunctionKey(
key: string,
_code: string,
shiftKey: boolean,
ctrlKey: boolean,
_altKey: boolean,
cursorKeyMode: CursorKeyMode
): string | null {
const app = cursorKeyMode === "application";
switch (key) {
// --- 光标键 ---
case "ArrowUp": return modifyArrow(app ? "\x1bOA" : "\x1b[A", shiftKey, ctrlKey);
case "ArrowDown": return modifyArrow(app ? "\x1bOB" : "\x1b[B", shiftKey, ctrlKey);
case "ArrowRight": return modifyArrow(app ? "\x1bOC" : "\x1b[C", shiftKey, ctrlKey);
case "ArrowLeft": return modifyArrow(app ? "\x1bOD" : "\x1b[D", shiftKey, ctrlKey);
// --- Enter / Backspace / Tab ---
case "Enter": return "\r";
case "Backspace": return ctrlKey ? "\x08" : "\x7f";
case "Tab": return shiftKey ? "\x1b[Z" : "\t";
case "Escape": return "\x1b";
case "Delete": return "\x1b[3~";
case "Insert": return "\x1b[2~";
// --- Home / End ---
case "Home": return ctrlKey ? "\x1b[1;5H" : app ? "\x1bOH" : "\x1b[H";
case "End": return ctrlKey ? "\x1b[1;5F" : app ? "\x1bOF" : "\x1b[F";
// --- Page Up / Page Down ---
case "PageUp": return shiftKey ? "\x1b[5;2~" : "\x1b[5~";
case "PageDown": return shiftKey ? "\x1b[6;2~" : "\x1b[6~";
// --- F1-F12 ---
case "F1": return "\x1bOP";
case "F2": return "\x1bOQ";
case "F3": return "\x1bOR";
case "F4": return "\x1bOS";
case "F5": return "\x1b[15~";
case "F6": return "\x1b[17~";
case "F7": return "\x1b[18~";
case "F8": return "\x1b[19~";
case "F9": return "\x1b[20~";
case "F10": return "\x1b[21~";
case "F11": return "\x1b[23~";
case "F12": return "\x1b[24~";
default: return null;
}
}
function modifyArrow(base: string, shiftKey: boolean, ctrlKey: boolean): string {
if (ctrlKey && shiftKey) {
// replace final letter with modifier param
return base.slice(0, -1) + ";6" + base.slice(-1);
}
if (ctrlKey) return base.slice(0, -1) + ";5" + base.slice(-1);
if (shiftKey) return base.slice(0, -1) + ";2" + base.slice(-1);
return base;
}

View File

@@ -0,0 +1,14 @@
/**
* IMeasureAdapter — 平台测量接口(字符/容器尺寸获取)。
* Web 由 ResizeObserver + Canvas 实现;小程序由 wx.createSelectorQuery 实现。
*/
export interface IMeasureAdapter {
/** 测量单个等宽字符宽/高(像素)*/
measureChar(): { widthPx: number; heightPx: number };
/** 测量终端容器内部可用宽/高(已去除 padding像素*/
measureContainer(): { widthPx: number; heightPx: number };
/** 订阅容器尺寸变化;返回取消订阅函数 */
onResize(cb: () => void): () => void;
}

View File

@@ -0,0 +1,80 @@
/**
* cursorMath — 光标坐标纯算法工具。
* 零 DOM 依赖。区分"终端光标"VT core 语义)与"输入光标"(输入框 selectionStart
*/
export interface CursorPos { row: number; col: number; }
/** 将终端光标视口坐标转为全局行号 */
export function toGlobalRow(baseY: number, cursorY: number): number {
return baseY + cursorY;
}
/**
* 判断触摸行是否在光标激活带内。
* activationRadius 建议 2 行。
*/
export function isInBand(touchRow: number, cursorGlobalRow: number, activationRadius = 2): boolean {
return Math.abs(touchRow - cursorGlobalRow) <= activationRadius;
}
/**
* 从 textarea 文本推导输入框光标位置((row, col) 坐标0-based
* 纯函数O(n) 扫描文本。
*/
export function textCursorPos(
text: string,
selectionStart: number,
cols: number
): CursorPos {
let row = 0;
let col = 0;
for (let i = 0; i < selectionStart && i < text.length; i++) {
const ch = text[i] ?? "";
if (ch === "\n") {
row++;
col = 0;
} else {
const w = charDisplayWidth(ch);
col += w;
if (col >= cols) {
// 自动折行
row += Math.floor(col / cols);
col = col % cols;
}
}
}
return { row, col };
}
/**
* 字符显示宽度ASCII=1, CJK/全角=2, 控制字符=0
* 使用简化的 East Asian Width 判断;完整版可插入 unicode-east-asian-width 库。
*/
export function charDisplayWidth(ch: string): 0 | 1 | 2 {
if (!ch) return 0;
const cp = ch.codePointAt(0) ?? 0;
if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0)) return 0; // 控制字符
if (isWide(cp)) return 2;
return 1;
}
function isWide(cp: number): boolean {
// GB2312/CJK 基本覆盖范围
if (cp >= 0x1100 && cp <= 0x115F) return true; // Hangul Jamo
if (cp === 0x2329 || cp === 0x232A) return true;
if (cp >= 0x2E80 && cp <= 0x303E) return true; // CJK Radicals
if (cp >= 0x3040 && cp <= 0x33FF) return true; // Japanese
if (cp >= 0x3400 && cp <= 0x4DBF) return true; // CJK Ext A
if (cp >= 0x4E00 && cp <= 0x9FFF) return true; // CJK Unified
if (cp >= 0xA000 && cp <= 0xA4CF) return true; // Yi
if (cp >= 0xAC00 && cp <= 0xD7AF) return true; // Hangul Syllables
if (cp >= 0xF900 && cp <= 0xFAFF) return true; // CJK Compatibility
if (cp >= 0xFE10 && cp <= 0xFE19) return true; // Vertical forms
if (cp >= 0xFE30 && cp <= 0xFE6F) return true; // CJK Compatibility Small
if (cp >= 0xFF00 && cp <= 0xFF60) return true; // Fullwidth
if (cp >= 0xFFE0 && cp <= 0xFFE6) return true;
if (cp >= 0x1F300 && cp <= 0x1F9FF) return true; // Emoji
if (cp >= 0x20000 && cp <= 0x2FFFD) return true; // CJK Ext B-F
return false;
}

View File

@@ -0,0 +1,27 @@
/**
* sizeCalculator — 纯算法px → cols/rows。
* 零 DOM 依赖,所有 DOM 测量由平台的 IMeasureAdapter 注入。
*/
export interface SizeResult {
cols: number;
rows: number;
}
export function calcSize(
containerWidthPx: number,
containerHeightPx: number,
charWidthPx: number,
lineHeightPx: number
): SizeResult {
const cols = Math.max(20, Math.floor(containerWidthPx / Math.max(1, charWidthPx)));
const rows = Math.max(8, Math.floor(containerHeightPx / Math.max(1, lineHeightPx)));
return { cols, rows };
}
/**
* 判断尺寸变化是否超过阈值(>= 1 col 或 >= 1 row避免抖动触发频繁 resize。
*/
export function sizeChanged(a: SizeResult, b: SizeResult): boolean {
return Math.abs(a.cols - b.cols) + Math.abs(a.rows - b.rows) >= 1;
}

View File

@@ -0,0 +1,75 @@
/**
* OutputBuffer — 终端输出缓冲区。
* 双阈值(条目数 + 字节数)裁剪,保证低端设备不 OOM。
*/
export interface OutputBufferOptions {
maxEntries?: number; // 默认 500
maxBytes?: number; // 默认 512 * 1024 (512KB)
}
export class OutputBuffer {
private entries: string[] = [];
private totalBytes = 0;
private readonly maxEntries: number;
private readonly maxBytes: number;
/** 缓冲区修订号:每次 push 后递增,供外部脏检测 */
public revision = 0;
constructor(opts: OutputBufferOptions = {}) {
this.maxEntries = Math.max(200, opts.maxEntries ?? 500);
this.maxBytes = Math.max(65536, opts.maxBytes ?? 512 * 1024);
}
push(data: string): void {
if (data.length === 0) return;
this.entries.push(data);
this.totalBytes += data.length * 2; // UTF-16 字符 ≈ 2 字节估算
this.trim();
this.revision++;
}
/** 获取全部条目(用于重放) */
getAll(): readonly string[] {
return this.entries;
}
/** 获取从指定修订号之后新增的条目(增量重放辅助) */
getSince(revision: number): string {
// 简化实现revision 不足时全量重放
if (revision <= 0) return this.entries.join("");
// 粗略计算需要从后面取多少条目
// 这里保守地返回全量;如需精确增量可增加 revision→index 映射表
return this.entries.join("");
}
clear(): void {
this.entries = [];
this.totalBytes = 0;
this.revision++;
}
get size(): number {
return this.entries.length;
}
get bytes(): number {
return this.totalBytes;
}
private trim(): void {
// 先按条目裁剪
while (this.entries.length > this.maxEntries) {
const removed = this.entries.shift();
if (removed !== undefined) this.totalBytes -= removed.length * 2;
}
// 再按字节裁剪
while (this.totalBytes > this.maxBytes && this.entries.length > 1) {
const removed = this.entries.shift();
if (removed !== undefined) this.totalBytes -= removed.length * 2;
}
if (this.totalBytes < 0) this.totalBytes = 0;
}
}

View File

@@ -0,0 +1,28 @@
import type { TerminalSnapshot } from "../types";
/**
* RendererAdapter — 渲染层抽象接口(每个平台各自实现)。
* 渲染器只读 snapshot禁止回写 TerminalCore 内部状态。
*/
export interface RendererAdapter {
/**
* 挂载到平台容器。
* Web: container 为 HTMLElement
* 小程序: 由 terminal-core-view 组件自行管理,传 null。
*/
mount(container: unknown): void;
/** 增量写入 VT 字节流(内部转发给 TerminalCore.write触发重新渲染 */
write(data: string): void;
/** 通知渲染器终端尺寸已变化 */
resize(cols: number, rows: number): void;
/**
* 全量重放快照(模式切换时调用,将渲染器状态同步到最新 snapshot
*/
applySnapshot(snapshot: TerminalSnapshot): void;
/** 销毁渲染器,释放 DOM/Canvas/定时器等资源 */
dispose(): void;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
/**
* TerminalSanitizer — 过滤 stdout/stderr 中会干扰渲染的噪音序列。
*
* 目前仅过滤同步更新模式DECSM ?2026 = DECRQM-like sync-update
* 以及其他已知会在某些 SSH 服务端发送但 terminal-core 不处理的序列。
* 不得过滤正常 CSI/OSC 序列,否则会破坏 TUI 程序显示。
*/
/** 同步更新开始/结束序列DEC private: ?2026h / ?2026l */
const SYNC_UPDATE_RE = /\x1b\[\?2026[hl]/g;
/** Bracketed paste 开关(部分链路丢失 ESC 后会残留 '?2004h/l' 文本) */
const BRACKETED_PASTE_RE = /(?:\x1b\[)?\?2004[hl]/g;
/** XTPUSHCOLORS / XTPOPCOLORSPs=10/11—— terminal-core 不处理,安全丢弃 */
const XTPUSH_POP_COLORS_RE = /\x1b\[(?:10|11)m/g;
export function sanitizeTerminalOutput(data: string): string {
return data
.replace(SYNC_UPDATE_RE, "")
.replace(BRACKETED_PASTE_RE, "")
.replace(XTPUSH_POP_COLORS_RE, "");
}

View File

@@ -0,0 +1,61 @@
import type { SessionState } from "../types";
/** 合法状态迁移表 */
const TRANSITIONS: Record<SessionState, SessionState[]> = {
idle: ["connecting", "disconnected"],
connecting: ["auth_pending", "error", "disconnected"],
auth_pending: ["connected", "error", "disconnected"],
connected: ["reconnecting", "disconnected", "error"],
reconnecting: ["connected", "error", "disconnected"],
disconnected: ["connecting", "idle"],
error: ["connecting", "disconnected"],
};
export function canTransition(from: SessionState, to: SessionState): boolean {
return (TRANSITIONS[from] ?? []).includes(to);
}
export function assertTransition(from: SessionState, to: SessionState): void {
if (!canTransition(from, to)) {
throw new Error(`非法状态跳转: ${from}${to}`);
}
}
/**
* SessionMachine — 会话生命周期状态机。
* 纯状态逻辑,无任何副作用,零 DOM 依赖。
*/
export class SessionMachine {
private _state: SessionState = "idle";
private listeners = new Set<(state: SessionState) => void>();
get state(): SessionState {
return this._state;
}
transition(to: SessionState): void {
assertTransition(this._state, to);
this._state = to;
for (const fn of this.listeners) {
fn(this._state);
}
}
tryTransition(to: SessionState): boolean {
if (!canTransition(this._state, to)) return false;
this.transition(to);
return true;
}
onChange(fn: (state: SessionState) => void): () => void {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
reset(): void {
this._state = "idle";
for (const fn of this.listeners) {
fn(this._state);
}
}
}

View File

@@ -0,0 +1,15 @@
import type { ConnectParams, FrameMeta, SessionState, TransportEventListener } from "../types";
/**
* TerminalTransport — 网络层抽象接口(三端各自实现)。
* 此接口在 packages/terminal-core 定义apps/web 现有实现兼容此签名。
*/
export interface TerminalTransport {
connect(params: ConnectParams): Promise<void>;
send(data: string, meta?: FrameMeta): Promise<void>;
resize(cols: number, rows: number): Promise<void>;
disconnect(reason?: string): Promise<void>;
/** 返回取消订阅函数 */
on(listener: TransportEventListener): () => void;
getState(): SessionState;
}

View File

@@ -0,0 +1,149 @@
/**
* 所有平台共用的基础类型定义。
* 此文件不得引入任何 DOM/BOM API不得依赖 @remoteconn/shared 或 apps/* 模块。
*/
// ── 会话状态 ───────────────────────────────────────────────────────────────────
export type SessionState =
| "idle"
| "connecting"
| "auth_pending"
| "connected"
| "reconnecting"
| "disconnected"
| "error";
// ── 连接参数 ───────────────────────────────────────────────────────────────────
export interface ConnectParams {
host: string;
port: number;
username: string;
credential: TerminalCredential;
knownHostFingerprint?: string;
cols: number;
rows: number;
clientSessionKey?: string;
}
export type TerminalCredential =
| { type: "password"; password: string }
| { type: "privateKey"; privateKey: string; passphrase?: string }
| { type: "certificate"; privateKey: string; certificate: string; passphrase?: string };
// ── 帧 metastdin 可选附加) ──────────────────────────────────────────────────
export interface FrameMeta {
source?: "keyboard" | "assist" | "paste";
txnId?: string;
}
// ── Transport 事件 ─────────────────────────────────────────────────────────────
export type TransportEvent =
| { type: "stdout"; data: string }
| { type: "stderr"; data: string }
| { type: "latency"; data: number }
| { type: "connected"; fingerprint?: string }
| { type: "disconnect"; reason: string }
| { type: "error"; code: string; message: string };
export type TransportEventListener = (event: TransportEvent) => void;
// ── 颜色值 ────────────────────────────────────────────────────────────────────
export interface ColorValue {
mode: "default" | "p16" | "p256" | "rgb";
/** p16: 0-15p256: 0-255rgb: 0xRRGGBB */
value: number;
}
export const DEFAULT_FG: ColorValue = { mode: "default", value: 0 };
export const DEFAULT_BG: ColorValue = { mode: "default", value: 0 };
export const DEFAULT_COLOR: ColorValue = { mode: "default", value: 0 };
// ── SGR flagsbitmask ───────────────────────────────────────────────────────
export const FLAG_BOLD = 1 << 0;
export const FLAG_DIM = 1 << 1;
export const FLAG_ITALIC = 1 << 2;
export const FLAG_UNDERLINE = 1 << 3;
export const FLAG_BLINK = 1 << 4;
export const FLAG_INVERSE = 1 << 5;
export const FLAG_INVISIBLE = 1 << 6;
export const FLAG_STRIKETHROUGH = 1 << 7;
export const FLAG_OVERLINE = 1 << 8;
// ── 单元格 ────────────────────────────────────────────────────────────────────
export interface TerminalCell {
char: string; // 空单元格为 ' '
width: 1 | 2; // CJK 宽字符占 2
fg: ColorValue;
bg: ColorValue;
flags: number;
underlineStyle: "none" | "single" | "double" | "curly" | "dotted" | "dashed";
underlineColor: ColorValue;
}
// ── 行 ────────────────────────────────────────────────────────────────────────
export interface TerminalLine {
cells: TerminalCell[];
isWrapped: boolean;
}
// ── 光标状态 ──────────────────────────────────────────────────────────────────
export interface CursorState {
x: number; // 列0-based
y: number; // 行视口相对0-based
globalRow: number; // baseY + y全局行号供触摸激活带使用
visible: boolean; // DECTCEM
}
// ── 快照(渲染器唯一输入数据结构) ─────────────────────────────────────────────
export interface TerminalSnapshot {
cols: number;
rows: number;
cursor: CursorState;
lines: TerminalLine[]; // 长度 = rows仅当前可视区
title: string;
revision: number; // 每次 write() 后递增
isAlternateBuffer: boolean;
}
// ── 字符属性VT 内部使用) ───────────────────────────────────────────────────
export interface CharAttribs {
fg: ColorValue;
bg: ColorValue;
flags: number;
underlineStyle: "none" | "single" | "double" | "curly" | "dotted" | "dashed";
underlineColor: ColorValue;
}
export function makeDefaultAttribs(): CharAttribs {
return {
fg: { mode: "default", value: 0 },
bg: { mode: "default", value: 0 },
flags: 0,
underlineStyle: "none",
underlineColor: { mode: "default", value: 0 },
};
}
export function copyAttribs(a: CharAttribs): CharAttribs {
return {
fg: { ...a.fg },
bg: { ...a.bg },
flags: a.flags,
underlineStyle: a.underlineStyle,
underlineColor: { ...a.underlineColor },
};
}
export function makeBlankCell(attribs?: CharAttribs): TerminalCell {
if (!attribs) {
return { char: " ", width: 1, fg: DEFAULT_FG, bg: DEFAULT_BG, flags: 0, underlineStyle: "none", underlineColor: DEFAULT_COLOR };
}
return {
char: " ", width: 1,
fg: { ...attribs.fg },
bg: { ...attribs.bg },
flags: attribs.flags,
underlineStyle: attribs.underlineStyle,
underlineColor: { ...attribs.underlineColor },
};
}