Spaces:
Running
Running
| // 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 }; | |
| } | |