Files
2026-03-21 18:57:10 +08:00

559 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* global module, require */
/**
* 轻量 VT 输入处理层:
* 1. 承接“模式位切换 / 查询响应 / 软重置”这类协议状态逻辑;
* 2. 不直接持有 buffer 数据结构,只通过上下文回调读写运行态;
* 3. 先把最容易继续膨胀的协议分支从 `terminalBufferState` 中拆出,为后续继续下沉 CSI/ESC 处理铺路。
*/
const { DEFAULT_TERMINAL_MODES } = require("./terminalBufferSet.js");
function createTerminalVtInputHandler(options) {
const source = options && typeof options === "object" ? options : {};
const responses = Array.isArray(source.responses) ? source.responses : [];
const runtimeColors = source.runtimeColors && typeof source.runtimeColors === "object" ? source.runtimeColors : {};
const runtimeState = source.runtimeState && typeof source.runtimeState === "object" ? source.runtimeState : {};
const bufferCols = Math.max(1, Math.round(Number(source.bufferCols) || 0));
const bufferRows = Math.max(1, Math.round(Number(source.bufferRows) || 0));
const cloneAnsiState =
typeof source.cloneAnsiState === "function"
? source.cloneAnsiState
: (state) => ({ ...(state && typeof state === "object" ? state : {}) });
const toOscRgbString =
typeof source.toOscRgbString === "function" ? source.toOscRgbString : () => "";
const getActiveBuffer =
typeof source.getActiveBuffer === "function" ? source.getActiveBuffer : () => null;
const setCursorRow = typeof source.setCursorRow === "function" ? source.setCursorRow : () => {};
const getCursorCol = typeof source.getCursorCol === "function" ? source.getCursorCol : () => 0;
const setCursorCol = typeof source.setCursorCol === "function" ? source.setCursorCol : () => {};
const getScrollTop = typeof source.getScrollTop === "function" ? source.getScrollTop : () => 0;
const setScrollTop = typeof source.setScrollTop === "function" ? source.setScrollTop : () => {};
const getScrollBottom =
typeof source.getScrollBottom === "function" ? source.getScrollBottom : () => Math.max(0, bufferRows - 1);
const setScrollBottom =
typeof source.setScrollBottom === "function" ? source.setScrollBottom : () => {};
const setAnsiState = typeof source.setAnsiState === "function" ? source.setAnsiState : () => {};
const moveCursorUp = typeof source.moveCursorUp === "function" ? source.moveCursorUp : () => {};
const moveCursorDown = typeof source.moveCursorDown === "function" ? source.moveCursorDown : () => {};
const moveCursorRight =
typeof source.moveCursorRight === "function" ? source.moveCursorRight : () => {};
const moveCursorLeft = typeof source.moveCursorLeft === "function" ? source.moveCursorLeft : () => {};
const moveCursorNextLine =
typeof source.moveCursorNextLine === "function" ? source.moveCursorNextLine : () => {};
const moveCursorPreviousLine =
typeof source.moveCursorPreviousLine === "function" ? source.moveCursorPreviousLine : () => {};
const setCursorColumn1 =
typeof source.setCursorColumn1 === "function" ? source.setCursorColumn1 : () => {};
const setCursorRow1 = typeof source.setCursorRow1 === "function" ? source.setCursorRow1 : () => {};
const setCursorPosition1 =
typeof source.setCursorPosition1 === "function" ? source.setCursorPosition1 : () => {};
const clearDisplayByMode =
typeof source.clearDisplayByMode === "function" ? source.clearDisplayByMode : () => {};
const clearLineByMode =
typeof source.clearLineByMode === "function" ? source.clearLineByMode : () => {};
const eraseChars = typeof source.eraseChars === "function" ? source.eraseChars : () => {};
const insertChars = typeof source.insertChars === "function" ? source.insertChars : () => {};
const deleteChars = typeof source.deleteChars === "function" ? source.deleteChars : () => {};
const insertLines = typeof source.insertLines === "function" ? source.insertLines : () => {};
const deleteLines = typeof source.deleteLines === "function" ? source.deleteLines : () => {};
const scrollRegionUp =
typeof source.scrollRegionUp === "function" ? source.scrollRegionUp : () => {};
const scrollRegionDown =
typeof source.scrollRegionDown === "function" ? source.scrollRegionDown : () => {};
const setScrollRegion =
typeof source.setScrollRegion === "function" ? source.setScrollRegion : () => {};
const resetCursorForOriginMode =
typeof source.resetCursorForOriginMode === "function" ? source.resetCursorForOriginMode : null;
const indexDown = typeof source.indexDown === "function" ? source.indexDown : () => {};
const nextLine = typeof source.nextLine === "function" ? source.nextLine : () => {};
const reverseIndex =
typeof source.reverseIndex === "function" ? source.reverseIndex : () => {};
const getScreenCursorRow =
typeof source.getScreenCursorRow === "function" ? source.getScreenCursorRow : () => 0;
const saveCurrentCursor =
typeof source.saveCurrentCursor === "function" ? source.saveCurrentCursor : () => {};
const restoreCurrentCursor =
typeof source.restoreCurrentCursor === "function" ? source.restoreCurrentCursor : () => {};
const switchActiveBuffer =
typeof source.switchActiveBuffer === "function" ? source.switchActiveBuffer : () => {};
const ansiResetState =
source.ansiResetState && typeof source.ansiResetState === "object" ? source.ansiResetState : {};
function pushDeviceStatusResponse(privateMarker, code) {
if (code === 5) {
responses.push(`${privateMarker === "?" ? "\u001b[?" : "\u001b["}0n`);
return;
}
if (code === 6) {
const row = getScreenCursorRow() + 1;
const col = Math.min(bufferCols, getCursorCol()) + 1;
responses.push(privateMarker === "?" ? `\u001b[?${row};${col}R` : `\u001b[${row};${col}R`);
}
}
/**
* 设备属性查询只回报当前实现真实支持的“最小可用口径”,
* 避免把不存在的终端特性伪装成已支持。
*/
function pushDeviceAttributesResponse(privateMarker, code) {
if (code > 0) {
return;
}
if (privateMarker === ">") {
responses.push("\u001b[>0;276;0c");
return;
}
responses.push("\u001b[?1;2c");
}
function pushOscColorReport(ident) {
let color = "";
if (ident === "10") {
color = runtimeColors.defaultForeground;
} else if (ident === "11") {
color = runtimeColors.defaultBackground;
} else if (ident === "12") {
color = runtimeColors.defaultCursor;
}
const rgb = toOscRgbString(color);
if (!rgb) {
return;
}
responses.push(`\u001b]${ident};${rgb}\u001b\\`);
}
/**
* OSC 10/11/12 允许把多个查询串在一条指令里,这里只对当前已实现的颜色槽位做查询响应。
*/
function handleOscSequence(ident, data) {
const base = Number(ident);
if (![10, 11, 12].includes(base)) {
return;
}
const slots = String(data || "").split(";");
for (let offset = 0; offset < slots.length; offset += 1) {
const slotIdent = String(base + offset);
if (!["10", "11", "12"].includes(slotIdent)) {
break;
}
if (slots[offset] === "?") {
pushOscColorReport(slotIdent);
}
}
}
function pushModeStatusResponse(privateMarker, mode, status) {
const prefix = privateMarker === "?" ? "?" : "";
responses.push(`\u001b[${prefix}${mode};${status}$y`);
}
/**
* 这里只回报当前运行态里真实维护的模式位。
* 未实现模式统一返回 0避免协议层把“未知能力”冒充成“已支持”。
*/
function resolveModeStatus(privateMarker, value) {
const mode = Math.round(Number(value) || 0);
if (!mode) return 0;
if (privateMarker !== "?") {
if (mode === 4) {
return runtimeState.modes.insertMode ? 1 : 2;
}
return 0;
}
if (mode === 1) return runtimeState.modes.applicationCursorKeys ? 1 : 2;
if (mode === 6) return runtimeState.modes.originMode ? 1 : 2;
if (mode === 7) return runtimeState.modes.wraparound ? 1 : 2;
if (mode === 25) return runtimeState.modes.cursorHidden ? 2 : 1;
if (mode === 45) return runtimeState.modes.reverseWraparound ? 1 : 2;
if (mode === 66) return runtimeState.modes.applicationKeypad ? 1 : 2;
if (mode === 47 || mode === 1047 || mode === 1049) {
const activeBuffer = getActiveBuffer();
return activeBuffer && activeBuffer.isAlt ? 1 : 2;
}
if (mode === 1048) return 1;
if (mode === 1004) return runtimeState.modes.sendFocus ? 1 : 2;
if (mode === 2004) return runtimeState.modes.bracketedPasteMode ? 1 : 2;
return 0;
}
function pushModeReport(privateMarker, value) {
const mode = Math.round(Number(value) || 0);
if (!mode) return;
pushModeStatusResponse(privateMarker, mode, resolveModeStatus(privateMarker, mode));
}
function pushStatusStringResponse(payload) {
const value = String(payload || "");
if (value === "m") {
responses.push("\u001bP1$r0m\u001b\\");
return;
}
if (value === "r") {
responses.push(`\u001bP1$r${getScrollTop() + 1};${getScrollBottom() + 1}r\u001b\\`);
return;
}
if (value === " q") {
responses.push("\u001bP1$r2 q\u001b\\");
return;
}
if (value === '"q') {
responses.push('\u001bP1$r0"q\u001b\\');
return;
}
if (value === '"p') {
responses.push('\u001bP1$r61;1"p\u001b\\');
return;
}
responses.push("\u001bP0$r\u001b\\");
}
/**
* DECSTR 只做软重置,不清屏、不搬动当前 cursor。
* 这部分逻辑必须保持和现有主链路一致,避免复杂 TUI 在软重置后发生可见跳变。
*/
function softResetTerminal() {
runtimeState.modes = { ...DEFAULT_TERMINAL_MODES };
setAnsiState(cloneAnsiState(ansiResetState));
setScrollTop(0);
setScrollBottom(Math.max(0, bufferRows - 1));
const activeBuffer = getActiveBuffer();
if (activeBuffer) {
activeBuffer.savedCursorRow = 0;
activeBuffer.savedCursorCol = 0;
activeBuffer.savedAnsiState = cloneAnsiState(ansiResetState);
}
}
function handlePrivateMode(enabled, value) {
const mode = Math.round(Number(value) || 0);
if (!mode) return;
if (mode === 1) {
runtimeState.modes.applicationCursorKeys = enabled;
return;
}
if (mode === 6) {
runtimeState.modes.originMode = enabled;
if (resetCursorForOriginMode) {
resetCursorForOriginMode(enabled);
return;
}
const activeBuffer = getActiveBuffer();
setCursorRow(enabled && activeBuffer && activeBuffer.isAlt ? getScrollTop() : 0);
setCursorCol(0);
return;
}
if (mode === 45) {
runtimeState.modes.reverseWraparound = enabled;
return;
}
if (mode === 66) {
runtimeState.modes.applicationKeypad = enabled;
return;
}
if (mode === 7) {
runtimeState.modes.wraparound = enabled;
return;
}
if (mode === 25) {
runtimeState.modes.cursorHidden = !enabled;
return;
}
if (mode === 47 || mode === 1047) {
switchActiveBuffer(enabled ? "alt" : "normal", enabled);
return;
}
if (mode === 1048) {
if (enabled) {
saveCurrentCursor();
} else {
restoreCurrentCursor();
}
return;
}
if (mode === 1049) {
if (enabled) {
saveCurrentCursor();
switchActiveBuffer("alt", true);
} else {
switchActiveBuffer("normal", false);
restoreCurrentCursor();
}
return;
}
if (mode === 2004) {
runtimeState.modes.bracketedPasteMode = enabled;
return;
}
if (mode === 1004) {
runtimeState.modes.sendFocus = enabled;
}
}
function handleAnsiMode(enabled, value) {
const mode = Math.round(Number(value) || 0);
if (!mode) return;
if (mode === 4) {
runtimeState.modes.insertMode = enabled;
}
}
function resolveCsiNumber(values, index, fallback) {
const value = values && Number(values[index]);
if (!Number.isFinite(value)) return fallback;
const normalized = Math.round(value);
if (normalized < 0) return 0;
return normalized;
}
/**
* 这里只下沉“CSI -> 光标动作”的协议解释:
* 1. handler 负责默认参数、1-based 行列语义和 final byte 分派;
* 2. bufferState 仍负责真实行列边界、history/alt buffer 与 origin mode 的具体落点。
*/
function handleCursorControl(final, values) {
const code = String(final || "");
if (code === "A") {
moveCursorUp(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "B") {
moveCursorDown(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "C") {
moveCursorRight(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "D") {
moveCursorLeft(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "E") {
moveCursorNextLine(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "F") {
moveCursorPreviousLine(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "G") {
setCursorColumn1(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "d") {
setCursorRow1(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "H" || code === "f") {
setCursorPosition1(
Math.max(1, resolveCsiNumber(values, 0, 1)),
Math.max(1, resolveCsiNumber(values, 1, 1))
);
return true;
}
return false;
}
/**
* 擦除类 CSI 只在这里解释参数语义:
* 1. `J / K` 走“显示/行清除模式”;
* 2. `X` 走“从当前光标起擦除 N 列”,默认值保持 1。
*/
function handleEraseControl(final, values) {
const code = String(final || "");
if (code === "J") {
clearDisplayByMode(resolveCsiNumber(values, 0, 0));
return true;
}
if (code === "K") {
clearLineByMode(resolveCsiNumber(values, 0, 0));
return true;
}
if (code === "X") {
eraseChars(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
return false;
}
/**
* 编辑/滚动类 CSI 仍然只解释“协议参数 -> buffer 动作”的映射,
* 真正的 cell 变更、滚动区约束和 viewport/history 处理继续留在 bufferState。
*/
function handleEditControl(final, values) {
const code = String(final || "");
if (code === "@") {
insertChars(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "P") {
deleteChars(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "L") {
insertLines(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "M") {
deleteLines(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "S") {
scrollRegionUp(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "T") {
scrollRegionDown(Math.max(1, resolveCsiNumber(values, 0, 1)));
return true;
}
if (code === "r") {
const top = Math.max(1, resolveCsiNumber(values, 0, 1));
const rawBottom = values && values.length > 1 ? resolveCsiNumber(values, 1, bufferRows) : bufferRows;
const bottom = rawBottom > 0 ? rawBottom : bufferRows;
setScrollRegion(top, bottom);
return true;
}
return false;
}
/**
* 保存/恢复光标这类协议动作目前同时存在于 CSI 与 ESC 两条入口:
* 1. `CSI s / u`
* 2. `ESC 7 / 8`
* 这里统一只做协议语义分派,真实保存内容仍由 bufferState 维护。
*/
function handleCursorSaveRestoreControl(final) {
const code = String(final || "");
if (code === "s" || code === "7") {
saveCurrentCursor();
return true;
}
if (code === "u" || code === "8") {
restoreCurrentCursor();
return true;
}
return false;
}
/**
* 查询类 CSI 统一在这里做协议分派,减少主循环里零散的条件判断:
* 1. `DECRQM``CSI Ps $ p` / `CSI ? Ps $ p`
* 2. `DSR / CPR``CSI n` / `CSI ? n`
* 3. `DA1 / DA2``CSI c` / `CSI > c`
*/
function handleQueryControl(privateMarker, intermediates, final, values) {
const marker = String(privateMarker || "");
const middle = String(intermediates || "");
const code = String(final || "");
if (middle === "$" && code === "p" && (marker === "" || marker === "?")) {
(Array.isArray(values) && values.length > 0 ? values : [0]).forEach((mode) => {
pushModeReport(marker, mode);
});
return true;
}
if (code === "n" && (marker === "" || marker === "?")) {
pushDeviceStatusResponse(marker, resolveCsiNumber(values, 0, 0));
return true;
}
if (code === "c" && (marker === "" || marker === ">")) {
pushDeviceAttributesResponse(marker, resolveCsiNumber(values, 0, 0));
return true;
}
return false;
}
/**
* ESC 入口目前只保留真正的 ESC 协议语义分派:
* 1. `ESC 7 / 8` 保存恢复光标
* 2. `ESC D / E / M` index / next line / reverse index
*/
function handleEscControl(final) {
const code = String(final || "");
if (handleCursorSaveRestoreControl(code)) {
return true;
}
if (code === "D") {
indexDown();
return true;
}
if (code === "E") {
nextLine();
return true;
}
if (code === "M") {
reverseIndex();
return true;
}
return false;
}
/**
* `CSI` 顶层分派只负责“协议入口 -> 已有子处理器”的路由:
* 1. 查询类、模式切换、软重置优先处理,保持和现有协议优先级一致;
* 2. 普通 ANSI 模式与无私有前缀的光标/擦除/编辑动作继续复用已有细分处理器;
* 3. `SGR` 仍留在 bufferState因为它直接作用于当前 ansiState 运行态。
*/
function handleCsiControl(privateMarker, intermediates, final, values) {
const marker = String(privateMarker || "");
const code = String(final || "");
if (handleQueryControl(marker, intermediates, code, values)) {
return true;
}
if (marker === "?") {
if (code === "h" || code === "l") {
const enabled = code === "h";
(Array.isArray(values) && values.length > 0 ? values : [0]).forEach((mode) =>
handlePrivateMode(enabled, mode)
);
return true;
}
return false;
}
if (marker === "!" && code === "p") {
softResetTerminal();
return true;
}
if (!marker && (code === "h" || code === "l")) {
values.forEach((value) => {
handleAnsiMode(code === "h", value);
});
return true;
}
if (marker) {
return false;
}
return (
handleCursorControl(code, values) ||
handleEraseControl(code, values) ||
handleEditControl(code, values) ||
handleCursorSaveRestoreControl(code)
);
}
return {
handleCsiControl,
handleCursorControl,
handleEraseControl,
handleEditControl,
handleCursorSaveRestoreControl,
handleQueryControl,
handleEscControl,
handleAnsiMode,
handleOscSequence,
handlePrivateMode,
pushDeviceAttributesResponse,
pushDeviceStatusResponse,
pushModeReport,
pushStatusStringResponse,
softResetTerminal
};
}
module.exports = {
createTerminalVtInputHandler
};