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 };
}