@opentui/core
Version:
OpenTUI is a TypeScript library for building terminal user interfaces (TUIs)
669 lines (663 loc) • 19.7 kB
JavaScript
// @bun
import {
ANSI,
CliRenderer,
TreeSitterClient,
resolveRenderLib
} from "./index-h3dbfsf6.js";
// src/testing/test-renderer.ts
import { Readable } from "stream";
// src/testing/mock-keys.ts
var KeyCodes = {
RETURN: "\r",
LINEFEED: `
`,
TAB: "\t",
BACKSPACE: "\b",
DELETE: "\x1B[3~",
HOME: "\x1B[H",
END: "\x1B[F",
ESCAPE: "\x1B",
ARROW_UP: "\x1B[A",
ARROW_DOWN: "\x1B[B",
ARROW_RIGHT: "\x1B[C",
ARROW_LEFT: "\x1B[D",
F1: "\x1BOP",
F2: "\x1BOQ",
F3: "\x1BOR",
F4: "\x1BOS",
F5: "\x1B[15~",
F6: "\x1B[17~",
F7: "\x1B[18~",
F8: "\x1B[19~",
F9: "\x1B[20~",
F10: "\x1B[21~",
F11: "\x1B[23~",
F12: "\x1B[24~"
};
var kittyKeyCodeMap = {
escape: 27,
tab: 9,
return: 13,
backspace: 127,
insert: 57348,
delete: 57349,
left: 57350,
right: 57351,
up: 57352,
down: 57353,
pageup: 57354,
pagedown: 57355,
home: 57356,
end: 57357,
f1: 57364,
f2: 57365,
f3: 57366,
f4: 57367,
f5: 57368,
f6: 57369,
f7: 57370,
f8: 57371,
f9: 57372,
f10: 57373,
f11: 57374,
f12: 57375
};
function encodeKittySequence(codepoint, modifiers) {
let modMask = 0;
if (modifiers?.shift)
modMask |= 1;
if (modifiers?.meta)
modMask |= 2;
if (modifiers?.ctrl)
modMask |= 4;
if (modifiers?.super)
modMask |= 8;
if (modifiers?.hyper)
modMask |= 16;
if (modMask === 0) {
return `\x1B[${codepoint}u`;
} else {
return `\x1B[${codepoint};${modMask + 1}u`;
}
}
function encodeModifyOtherKeysSequence(charCode, modifiers) {
let modMask = 0;
if (modifiers?.shift)
modMask |= 1;
if (modifiers?.meta)
modMask |= 2;
if (modifiers?.ctrl)
modMask |= 4;
if (modifiers?.super)
modMask |= 8;
if (modifiers?.hyper)
modMask |= 16;
if (modMask === 0) {
return String.fromCharCode(charCode);
}
return `\x1B[27;${modMask + 1};${charCode}~`;
}
function resolveKeyInput(key) {
let keyValue;
let keyName;
if (typeof key === "string") {
if (key in KeyCodes) {
keyValue = KeyCodes[key];
keyName = key.toLowerCase();
} else {
keyValue = key;
keyName = undefined;
}
} else {
keyValue = KeyCodes[key];
if (!keyValue) {
throw new Error(`Unknown key: ${key}`);
}
keyName = String(key).toLowerCase();
}
return { keyValue, keyName };
}
function createMockKeys(renderer, options) {
const useKittyKeyboard = options?.kittyKeyboard ?? false;
const useOtherModifiersMode = options?.otherModifiersMode ?? false;
const effectiveOtherModifiersMode = useOtherModifiersMode && !useKittyKeyboard;
const pressKeys = async (keys, delayMs = 0) => {
for (const key of keys) {
const { keyValue: keyCode } = resolveKeyInput(key);
renderer.stdin.emit("data", Buffer.from(keyCode));
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
};
const pressKey = (key, modifiers) => {
if (useKittyKeyboard) {
let { keyValue, keyName } = resolveKeyInput(key);
const valueToKeyNameMap = {
"\b": "backspace",
"\r": "return",
"\n": "return",
"\t": "tab",
"\x1B": "escape",
"\x1B[A": "up",
"\x1B[B": "down",
"\x1B[C": "right",
"\x1B[D": "left",
"\x1B[H": "home",
"\x1B[F": "end",
"\x1B[3~": "delete"
};
if (keyValue && valueToKeyNameMap[keyValue]) {
keyName = valueToKeyNameMap[keyValue];
}
if (keyName && keyName.startsWith("arrow_")) {
keyName = keyName.substring(6);
}
if (keyName && kittyKeyCodeMap[keyName]) {
const kittyCode = kittyKeyCodeMap[keyName];
const sequence = encodeKittySequence(kittyCode, modifiers);
renderer.stdin.emit("data", Buffer.from(sequence));
return;
}
if (keyValue && keyValue.length === 1 && !keyValue.startsWith("\x1B")) {
const codepoint = keyValue.codePointAt(0);
if (codepoint) {
const sequence = encodeKittySequence(codepoint, modifiers);
renderer.stdin.emit("data", Buffer.from(sequence));
return;
}
}
}
if (effectiveOtherModifiersMode && modifiers) {
let { keyValue, keyName } = resolveKeyInput(key);
const valueToCharCodeMap = {
"\b": 127,
"\r": 13,
"\n": 13,
"\t": 9,
"\x1B": 27,
" ": 32
};
let charCode;
if (keyValue && valueToCharCodeMap[keyValue] !== undefined) {
charCode = valueToCharCodeMap[keyValue];
} else if (keyValue && keyValue.length === 1 && !keyValue.startsWith("\x1B")) {
charCode = keyValue.charCodeAt(0);
}
if (charCode !== undefined) {
const sequence = encodeModifyOtherKeysSequence(charCode, modifiers);
renderer.stdin.emit("data", Buffer.from(sequence));
return;
}
}
let keyCode = resolveKeyInput(key).keyValue;
if (modifiers) {
if (keyCode.startsWith("\x1B[") && keyCode.length > 2) {
const modifier = 1 + (modifiers.shift ? 1 : 0) + (modifiers.meta ? 2 : 0) + (modifiers.ctrl ? 4 : 0) + (modifiers.super ? 8 : 0) + (modifiers.hyper ? 16 : 0);
if (modifier > 1) {
const tildeMatch = keyCode.match(/^\x1b\[(\d+)~$/);
if (tildeMatch) {
keyCode = `\x1B[${tildeMatch[1]};${modifier}~`;
} else {
const ending = keyCode.slice(-1);
keyCode = `\x1B[1;${modifier}${ending}`;
}
}
} else if (keyCode.length === 1) {
let char = keyCode;
if (char === "\b" && (modifiers.ctrl || modifiers.super || modifiers.hyper)) {
const modifier = 1 + (modifiers.shift ? 1 : 0) + (modifiers.meta ? 2 : 0) + (modifiers.ctrl ? 4 : 0) + (modifiers.super ? 8 : 0) + (modifiers.hyper ? 16 : 0);
keyCode = `\x1B[27;${modifier};127~`;
} else if (modifiers.ctrl) {
if (char >= "a" && char <= "z") {
keyCode = String.fromCharCode(char.charCodeAt(0) - 96);
} else if (char >= "A" && char <= "Z") {
keyCode = String.fromCharCode(char.charCodeAt(0) - 64);
} else {
const specialCtrlMap = {
"[": "\x1B",
"\\": "\x1C",
"]": "\x1D",
"^": "\x1E",
_: "\x1F",
"?": "\x7F",
"/": "\x1F",
"-": "\x1F",
".": "\x1E",
",": "\x1C",
"@": "\x00",
" ": "\x00"
};
if (char in specialCtrlMap) {
keyCode = specialCtrlMap[char];
}
}
if (modifiers.meta) {
keyCode = `\x1B${keyCode}`;
}
} else {
if (modifiers.shift && char >= "a" && char <= "z") {
char = char.toUpperCase();
}
if (modifiers.meta) {
keyCode = `\x1B${char}`;
} else {
keyCode = char;
}
}
} else if (modifiers.meta && !keyCode.startsWith("\x1B")) {
keyCode = `\x1B${keyCode}`;
}
}
renderer.stdin.emit("data", Buffer.from(keyCode));
};
const typeText = async (text, delayMs = 0) => {
const keys = text.split("");
await pressKeys(keys, delayMs);
};
const pressReturn = (modifiers) => {
pressKey(KeyCodes.RETURN, modifiers);
};
const pressEscape = (modifiers) => {
pressKey(KeyCodes.ESCAPE, modifiers);
};
const pressTab = (modifiers) => {
pressKey(KeyCodes.TAB, modifiers);
};
const pressBackspace = (modifiers) => {
pressKey(KeyCodes.BACKSPACE, modifiers);
};
const pressArrow = (direction, modifiers) => {
const keyMap = {
up: KeyCodes.ARROW_UP,
down: KeyCodes.ARROW_DOWN,
left: KeyCodes.ARROW_LEFT,
right: KeyCodes.ARROW_RIGHT
};
pressKey(keyMap[direction], modifiers);
};
const pressCtrlC = () => {
pressKey("c", { ctrl: true });
};
const pasteBracketedText = (text) => {
return pressKeys([ANSI.bracketedPasteStart, text, ANSI.bracketedPasteEnd]);
};
return {
pressKeys,
pressKey,
typeText,
pressEnter: pressReturn,
pressEscape,
pressTab,
pressBackspace,
pressArrow,
pressCtrlC,
pasteBracketedText
};
}
// src/testing/mock-mouse.ts
var MouseButtons = {
LEFT: 0,
MIDDLE: 1,
RIGHT: 2,
WHEEL_UP: 64,
WHEEL_DOWN: 65,
WHEEL_LEFT: 66,
WHEEL_RIGHT: 67
};
function createMockMouse(renderer) {
let currentPosition = { x: 0, y: 0 };
let buttonsPressed = new Set;
const generateMouseEvent = (type, x, y, button = MouseButtons.LEFT, modifiers = {}) => {
let buttonCode = button;
if (modifiers.shift)
buttonCode |= 4;
if (modifiers.alt)
buttonCode |= 8;
if (modifiers.ctrl)
buttonCode |= 16;
switch (type) {
case "move":
buttonCode = 32 | 3;
if (modifiers.shift)
buttonCode |= 4;
if (modifiers.alt)
buttonCode |= 8;
if (modifiers.ctrl)
buttonCode |= 16;
break;
case "drag":
buttonCode = (buttonsPressed.size > 0 ? Array.from(buttonsPressed)[0] : button) | 32;
if (modifiers.shift)
buttonCode |= 4;
if (modifiers.alt)
buttonCode |= 8;
if (modifiers.ctrl)
buttonCode |= 16;
break;
case "scroll":
break;
}
const ansiX = x + 1;
const ansiY = y + 1;
let pressRelease = "M";
if (type === "up" || type === "move" || type === "drag") {
pressRelease = "m";
}
return `\x1B[<${buttonCode};${ansiX};${ansiY}${pressRelease}`;
};
const emitMouseEvent = async (type, x, y, button = MouseButtons.LEFT, options = {}) => {
const { modifiers = {}, delayMs = 0 } = options;
const eventSequence = generateMouseEvent(type, x, y, button, modifiers);
renderer.stdin.emit("data", Buffer.from(eventSequence));
currentPosition = { x, y };
if (type === "down" && button < 64) {
buttonsPressed.add(button);
} else if (type === "up") {
buttonsPressed.delete(button);
}
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
};
const moveTo = async (x, y, options = {}) => {
const { button = MouseButtons.LEFT, delayMs = 0, modifiers = {} } = options;
if (buttonsPressed.size > 0) {
await emitMouseEvent("drag", x, y, Array.from(buttonsPressed)[0], { modifiers, delayMs });
} else {
await emitMouseEvent("move", x, y, button, { modifiers, delayMs });
}
currentPosition = { x, y };
};
const click = async (x, y, button = MouseButtons.LEFT, options = {}) => {
const { delayMs = 10, modifiers = {} } = options;
await emitMouseEvent("down", x, y, button, { modifiers, delayMs });
await new Promise((resolve) => setTimeout(resolve, delayMs));
await emitMouseEvent("up", x, y, button, { modifiers, delayMs });
};
const doubleClick = async (x, y, button = MouseButtons.LEFT, options = {}) => {
const { delayMs = 10, modifiers = {} } = options;
await click(x, y, button, { modifiers, delayMs });
await new Promise((resolve) => setTimeout(resolve, delayMs));
await click(x, y, button, { modifiers, delayMs });
};
const pressDown = async (x, y, button = MouseButtons.LEFT, options = {}) => {
const { modifiers = {}, delayMs = 0 } = options;
await emitMouseEvent("down", x, y, button, { modifiers, delayMs });
};
const release = async (x, y, button = MouseButtons.LEFT, options = {}) => {
const { modifiers = {}, delayMs = 0 } = options;
await emitMouseEvent("up", x, y, button, { modifiers, delayMs });
};
const drag = async (startX, startY, endX, endY, button = MouseButtons.LEFT, options = {}) => {
const { delayMs = 10, modifiers = {} } = options;
await pressDown(startX, startY, button, { modifiers });
const steps = 5;
const dx = (endX - startX) / steps;
const dy = (endY - startY) / steps;
for (let i = 1;i <= steps; i++) {
const currentX = Math.round(startX + dx * i);
const currentY = Math.round(startY + dy * i);
await emitMouseEvent("drag", currentX, currentY, button, { modifiers, delayMs });
}
await release(endX, endY, button, { modifiers });
};
const scroll = async (x, y, direction, options = {}) => {
const { modifiers = {}, delayMs = 0 } = options;
let button;
switch (direction) {
case "up":
button = MouseButtons.WHEEL_UP;
break;
case "down":
button = MouseButtons.WHEEL_DOWN;
break;
case "left":
button = MouseButtons.WHEEL_LEFT;
break;
case "right":
button = MouseButtons.WHEEL_RIGHT;
break;
}
await emitMouseEvent("scroll", x, y, button, { modifiers, delayMs });
};
const getCurrentPosition = () => {
return { ...currentPosition };
};
const getPressedButtons = () => {
return Array.from(buttonsPressed);
};
return {
moveTo,
click,
doubleClick,
pressDown,
release,
drag,
scroll,
getCurrentPosition,
getPressedButtons,
emitMouseEvent
};
}
// src/testing/test-renderer.ts
var decoder = new TextDecoder;
async function createTestRenderer(options) {
process.env.OTUI_USE_CONSOLE = "false";
const useKittyKeyboard = options.kittyKeyboard ? { events: true } : options.useKittyKeyboard;
const renderer = await setupTestRenderer({
...options,
useKittyKeyboard,
useAlternateScreen: false,
useConsole: false
});
renderer.disableStdoutInterception();
const mockInput = createMockKeys(renderer, {
kittyKeyboard: options.kittyKeyboard,
otherModifiersMode: options.otherModifiersMode
});
const mockMouse = createMockMouse(renderer);
const renderOnce = async () => {
await renderer.loop();
};
return {
renderer,
mockInput,
mockMouse,
renderOnce,
captureCharFrame: () => {
const currentBuffer = renderer.currentRenderBuffer;
const frameBytes = currentBuffer.getRealCharBytes(true);
return decoder.decode(frameBytes);
},
captureSpans: () => {
const currentBuffer = renderer.currentRenderBuffer;
const lines = currentBuffer.getSpanLines();
const cursorState = renderer.getCursorState();
return {
cols: currentBuffer.width,
rows: currentBuffer.height,
cursor: [cursorState.x, cursorState.y],
lines
};
},
resize: (width, height) => {
renderer.processResize(width, height);
}
};
}
async function setupTestRenderer(config) {
const stdin = config.stdin || new Readable({ read() {} });
const stdout = config.stdout || process.stdout;
const width = config.width || stdout.columns || 80;
const height = config.height || stdout.rows || 24;
const renderHeight = config.experimental_splitHeight && config.experimental_splitHeight > 0 ? config.experimental_splitHeight : height;
const ziglib = resolveRenderLib();
const rendererPtr = ziglib.createRenderer(width, renderHeight, {
testing: true,
remote: config.remote ?? false
});
if (!rendererPtr) {
throw new Error("Failed to create test renderer");
}
if (config.useThread === undefined) {
config.useThread = true;
}
if (process.platform === "linux") {
config.useThread = false;
}
ziglib.setUseThread(rendererPtr, config.useThread);
const renderer = new CliRenderer(ziglib, rendererPtr, stdin, stdout, width, height, config);
process.off("SIGWINCH", renderer["sigwinchHandler"]);
return renderer;
}
// src/testing/mock-tree-sitter-client.ts
class MockTreeSitterClient extends TreeSitterClient {
_highlightPromises = [];
_mockResult = { highlights: [] };
_autoResolveTimeout;
constructor(options) {
super({ dataPath: "/tmp/mock" });
this._autoResolveTimeout = options?.autoResolveTimeout;
}
async highlightOnce(content, filetype) {
const { promise, resolve } = Promise.withResolvers();
let timeout;
if (this._autoResolveTimeout !== undefined) {
timeout = setTimeout(() => {
const index = this._highlightPromises.findIndex((p) => p.promise === promise);
if (index !== -1) {
resolve(this._mockResult);
this._highlightPromises.splice(index, 1);
}
}, this._autoResolveTimeout);
}
this._highlightPromises.push({ promise, resolve, timeout });
return promise;
}
setMockResult(result) {
this._mockResult = result;
}
resolveHighlightOnce(index = 0) {
if (index >= 0 && index < this._highlightPromises.length) {
const item = this._highlightPromises[index];
if (item.timeout) {
clearTimeout(item.timeout);
}
item.resolve(this._mockResult);
this._highlightPromises.splice(index, 1);
}
}
resolveAllHighlightOnce() {
for (const { resolve, timeout } of this._highlightPromises) {
if (timeout) {
clearTimeout(timeout);
}
resolve(this._mockResult);
}
this._highlightPromises = [];
}
isHighlighting() {
return this._highlightPromises.length > 0;
}
}
// src/testing/spy.ts
function createSpy() {
const calls = [];
const spy = (...args) => {
calls.push(args);
};
spy.calls = calls;
spy.callCount = () => calls.length;
spy.calledWith = (...expected) => {
return calls.some((call) => JSON.stringify(call) === JSON.stringify(expected));
};
spy.reset = () => calls.length = 0;
return spy;
}
// src/testing/test-recorder.ts
class TestRecorder {
renderer;
frames = [];
recording = false;
frameNumber = 0;
startTime = 0;
originalRenderNative;
decoder = new TextDecoder;
recordBuffers;
constructor(renderer, options) {
this.renderer = renderer;
this.recordBuffers = options?.recordBuffers || {};
}
rec() {
if (this.recording) {
return;
}
this.recording = true;
this.frames = [];
this.frameNumber = 0;
this.startTime = Date.now();
this.originalRenderNative = this.renderer["renderNative"].bind(this.renderer);
this.renderer["renderNative"] = () => {
this.originalRenderNative();
this.captureFrame();
};
}
stop() {
if (!this.recording) {
return;
}
this.recording = false;
if (this.originalRenderNative) {
this.renderer["renderNative"] = this.originalRenderNative;
this.originalRenderNative = undefined;
}
}
get recordedFrames() {
return [...this.frames];
}
clear() {
this.frames = [];
this.frameNumber = 0;
}
get isRecording() {
return this.recording;
}
captureFrame() {
const currentBuffer = this.renderer.currentRenderBuffer;
const frameBytes = currentBuffer.getRealCharBytes(true);
const frame = this.decoder.decode(frameBytes);
const recordedFrame = {
frame,
timestamp: Date.now() - this.startTime,
frameNumber: this.frameNumber++
};
if (this.recordBuffers.fg || this.recordBuffers.bg || this.recordBuffers.attributes) {
const buffers = currentBuffer.buffers;
recordedFrame.buffers = {};
if (this.recordBuffers.fg) {
recordedFrame.buffers.fg = new Float32Array(buffers.fg);
}
if (this.recordBuffers.bg) {
recordedFrame.buffers.bg = new Float32Array(buffers.bg);
}
if (this.recordBuffers.attributes) {
recordedFrame.buffers.attributes = new Uint8Array(buffers.attributes);
}
}
this.frames.push(recordedFrame);
}
}
export {
createTestRenderer,
createSpy,
createMockMouse,
createMockKeys,
TestRecorder,
MouseButtons,
MockTreeSitterClient,
KeyCodes
};
//# debugId=CB79F086C6FCC24864756E2164756E21
//# sourceMappingURL=testing.js.map