Spaces:
Running
Running
File size: 3,146 Bytes
1bb8739 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | // libghostty's structured snapshot exposes the screen and cursor, but not the
// input modes a TUI enabled before a browser attached. Keep the small subset of
// terminal state that changes how xterm turns browser interaction into PTY
// bytes, so a canonical repaint can restore behavior as well as pixels.
const MOUSE_PROTOCOL_MODES = new Set([9, 1000, 1002, 1003]);
const MOUSE_ENCODING_MODES = new Set([1005, 1006, 1015, 1016]);
export function createTerminalModeTracker() {
let applicationCursorKeys = false;
let mouseProtocol = 0;
let mouseEncoding = 0;
let sendFocus = false;
let bracketedPaste = false;
// 0 ground, 1 ESC, 2 CSI, 3 CSI ? parameters. State is retained across PTY
// chunks because escape sequences are not guaranteed to arrive atomically.
let parserState = 0;
let parameters = '';
const reset = () => {
applicationCursorKeys = false;
mouseProtocol = 0;
mouseEncoding = 0;
sendFocus = false;
bracketedPaste = false;
};
const applyPrivateMode = (mode, enabled) => {
if (mode === 1) applicationCursorKeys = enabled;
else if (MOUSE_PROTOCOL_MODES.has(mode)) mouseProtocol = enabled ? mode : 0;
else if (MOUSE_ENCODING_MODES.has(mode)) mouseEncoding = enabled ? mode : 0;
else if (mode === 1004) sendFocus = enabled;
else if (mode === 2004) bracketedPaste = enabled;
};
const finishPrivateMode = (enabled) => {
for (const value of parameters.split(';')) {
if (!/^\d+$/.test(value)) continue;
applyPrivateMode(Number(value), enabled);
}
parameters = '';
};
const feed = (data) => {
const text = typeof data === 'string' ? data : Buffer.from(data).toString('latin1');
for (const char of text) {
if (parserState === 0) {
if (char === '\x1b') parserState = 1;
else if (char === '\x9b') parserState = 2;
continue;
}
if (parserState === 1) {
if (char === '[') parserState = 2;
else if (char === 'c') { reset(); parserState = 0; }
else parserState = char === '\x1b' ? 1 : 0;
continue;
}
if (parserState === 2) {
if (char === '?') { parameters = ''; parserState = 3; }
else parserState = char === '\x1b' ? 1 : 0;
continue;
}
if ((char >= '0' && char <= '9') || char === ';') {
// A real mode list is tiny. Bound malformed output so it cannot grow a
// retained parser string without limit.
if (parameters.length < 128) parameters += char;
else { parameters = ''; parserState = 0; }
} else if (char === 'h' || char === 'l') {
finishPrivateMode(char === 'h');
parserState = 0;
} else {
parameters = '';
parserState = char === '\x1b' ? 1 : 0;
}
}
};
const restoreAnsi = () => {
let out = '';
if (applicationCursorKeys) out += '\x1b[?1h';
if (mouseProtocol) out += `\x1b[?${mouseProtocol}h`;
if (mouseEncoding) out += `\x1b[?${mouseEncoding}h`;
if (sendFocus) out += '\x1b[?1004h';
if (bracketedPaste) out += '\x1b[?2004h';
return out;
};
return { feed, reset, restoreAnsi };
}
|