| 1 | const FLAG_NAMES = { |
| 2 | 0x01: "bold", |
| 3 | 0x02: "dim", |
| 4 | 0x04: "italic", |
| 5 | 0x08: "underline", |
| 6 | 0x10: "blink", |
| 7 | 0x20: "reverse", |
| 8 | 0x40: "invisible", |
| 9 | 0x80: "strikethrough", |
| 10 | }; |
| 11 | function flagsToNames(flags) { |
| 12 | const names = []; |
| 13 | for (const [bit, name] of Object.entries(FLAG_NAMES)) { |
| 14 | if (flags & Number(bit)) |
| 15 | names.push(name); |
| 16 | } |
| 17 | return names; |
| 18 | } |
| 19 | const ESC = 0x1b; |
| 20 | function scanSequences(data) { |
| 21 | const entries = []; |
| 22 | const ts = Date.now(); |
| 23 | let i = 0; |
| 24 | let textStart = 0; |
| 25 | const flushText = () => { |
| 26 | if (i > textStart) { |
| 27 | const raw = data.slice(textStart, i); |
| 28 | if (raw.length > 0 && !/^[\x00-\x1f]+$/.test(raw)) { |
| 29 | entries.push({ ts, type: "text", raw: raw.slice(0, 60) }); |
| 30 | } |
| 31 | } |
| 32 | }; |
| 33 | while (i < data.length) { |
| 34 | if (data.charCodeAt(i) !== ESC) { |
| 35 | i++; |
| 36 | continue; |
| 37 | } |
| 38 | flushText(); |
| 39 | const seqStart = i; |
| 40 | i++; // skip ESC |
| 41 | if (i >= data.length) |
| 42 | break; |
| 43 | const next = data[i]; |
| 44 | if (next === "[") { |
| 45 | // CSI sequence |
| 46 | i++; |
| 47 | let priv = ""; |
| 48 | if (i < data.length && |
| 49 | (data[i] === "?" || data[i] === ">" || data[i] === "!")) { |
| 50 | priv = data[i]; |
| 51 | i++; |
| 52 | } |
| 53 | let paramStr = ""; |
| 54 | while (i < data.length && |
| 55 | ((data.charCodeAt(i) >= 0x30 && data.charCodeAt(i) <= 0x3b) || |
| 56 | data[i] === ":")) { |
| 57 | paramStr += data[i]; |
| 58 | i++; |
| 59 | } |
| 60 | // skip intermediates |
| 61 | while (i < data.length && |
| 62 | data.charCodeAt(i) >= 0x20 && |
| 63 | data.charCodeAt(i) <= 0x2f) { |
| 64 | i++; |
| 65 | } |
| 66 | let final = ""; |
| 67 | if (i < data.length && |
| 68 | data.charCodeAt(i) >= 0x40 && |
| 69 | data.charCodeAt(i) <= 0x7e) { |
| 70 | final = data[i]; |
| 71 | i++; |
| 72 | } |
| 73 | const raw = data.slice(seqStart, i); |
| 74 | const params = paramStr |
| 75 | ? paramStr |
| 76 | .split(/[;:]/) |
| 77 | .map(Number) |
| 78 | .filter((n) => !isNaN(n)) |
| 79 | : []; |
| 80 | const type = final === "m" ? "sgr" : "csi"; |
| 81 | entries.push({ |
| 82 | ts, |
| 83 | type, |
| 84 | raw, |
| 85 | params: params.length > 0 ? params : undefined, |
| 86 | private: priv || undefined, |
| 87 | final, |
| 88 | }); |
| 89 | } |
| 90 | else if (next === "]") { |
| 91 | // OSC sequence |
| 92 | i++; |
| 93 | while (i < data.length && |
| 94 | data.charCodeAt(i) !== 0x07 && |
| 95 | !(data.charCodeAt(i) === ESC && |
| 96 | i + 1 < data.length && |
| 97 | data[i + 1] === "\\")) { |
| 98 | i++; |
| 99 | } |
| 100 | if (i < data.length) { |
| 101 | if (data.charCodeAt(i) === 0x07) |
| 102 | i++; |
| 103 | else if (data.charCodeAt(i) === ESC) |
| 104 | i += 2; // ESC backslash |
| 105 | } |
| 106 | const raw = data.slice(seqStart, i); |
| 107 | entries.push({ ts, type: "osc", raw: raw.slice(0, 80) }); |
| 108 | } |
| 109 | else if (next >= " " && next <= "~") { |
| 110 | // Simple ESC + char |
| 111 | i++; |
| 112 | entries.push({ |
| 113 | ts, |
| 114 | type: "esc", |
| 115 | raw: data.slice(seqStart, i), |
| 116 | final: next, |
| 117 | }); |
| 118 | } |
| 119 | else { |
| 120 | i++; |
| 121 | } |
| 122 | textStart = i; |
| 123 | } |
| 124 | flushText(); |
| 125 | return entries; |
| 126 | } |
| 127 | // -- Main debug adapter -- |
| 128 | const MAX_TRACES = 500; |
| 129 | export class DebugAdapter { |
| 130 | constructor() { |
| 131 | this._traces = []; |
| 132 | this._bridge = null; |
| 133 | this._perf = { |
| 134 | frameCount: 0, |
| 135 | totalRenderMs: 0, |
| 136 | avgRenderMs: 0, |
| 137 | maxRenderMs: 0, |
| 138 | lastDirtyRows: 0, |
| 139 | }; |
| 140 | } |
| 141 | get traces() { |
| 142 | return this._traces; |
| 143 | } |
| 144 | get perf() { |
| 145 | return this._perf; |
| 146 | } |
| 147 | setBridge(bridge) { |
| 148 | this._bridge = bridge; |
| 149 | } |
| 150 | traceWrite(data) { |
| 151 | const str = typeof data === "string" ? data : new TextDecoder().decode(data); |
| 152 | const entries = scanSequences(str); |
| 153 | for (const entry of entries) { |
| 154 | this._traces.push(entry); |
| 155 | } |
| 156 | if (this._traces.length > MAX_TRACES) { |
| 157 | this._traces = this._traces.slice(-MAX_TRACES); |
| 158 | } |
| 159 | } |
| 160 | recordRender(renderMs, dirtyRows) { |
| 161 | this._perf.frameCount++; |
| 162 | this._perf.totalRenderMs += renderMs; |
| 163 | this._perf.avgRenderMs = this._perf.totalRenderMs / this._perf.frameCount; |
| 164 | if (renderMs > this._perf.maxRenderMs) { |
| 165 | this._perf.maxRenderMs = renderMs; |
| 166 | } |
| 167 | this._perf.lastDirtyRows = dirtyRows; |
| 168 | } |
| 169 | resetPerf() { |
| 170 | this._perf = { |
| 171 | frameCount: 0, |
| 172 | totalRenderMs: 0, |
| 173 | avgRenderMs: 0, |
| 174 | maxRenderMs: 0, |
| 175 | lastDirtyRows: 0, |
| 176 | }; |
| 177 | } |
| 178 | // -- Cell inspector -- |
| 179 | cell(row, col) { |
| 180 | if (!this._bridge) |
| 181 | return null; |
| 182 | const c = this._bridge.getCell(row, col); |
| 183 | return { |
| 184 | ...c, |
| 185 | charStr: c.chars ?? (c.char >= 32 ? String.fromCodePoint(c.char) : ""), |
| 186 | flagNames: flagsToNames(c.flags), |
| 187 | }; |
| 188 | } |
| 189 | row(row) { |
| 190 | if (!this._bridge) |
| 191 | return null; |
| 192 | const cols = this._bridge.getCols(); |
| 193 | const cells = []; |
| 194 | for (let c = 0; c < cols; c++) { |
| 195 | cells.push(this.cell(row, c)); |
| 196 | } |
| 197 | return cells; |
| 198 | } |
| 199 | grid() { |
| 200 | if (!this._bridge) |
| 201 | return null; |
| 202 | const cursor = this._bridge.getCursor(); |
| 203 | return { |
| 204 | rows: this._bridge.getRows(), |
| 205 | cols: this._bridge.getCols(), |
| 206 | cursor, |
| 207 | altScreen: this._bridge.usingAltScreen(), |
| 208 | scrollbackCount: this._bridge.getScrollbackCount(), |
| 209 | }; |
| 210 | } |
| 211 | unhandled() { |
| 212 | if (!this._bridge) |
| 213 | return []; |
| 214 | return this._bridge.getUnhandledSequences(); |
| 215 | } |
| 216 | // -- Console-friendly dump -- |
| 217 | dump(count = 50) { |
| 218 | const entries = this._traces.slice(-count); |
| 219 | console.group(`%cwterm debug — last ${entries.length} traces`, "color: #569cd6; font-weight: bold"); |
| 220 | for (const e of entries) { |
| 221 | const badge = e.type === "sgr" |
| 222 | ? "%cSGR" |
| 223 | : e.type === "csi" |
| 224 | ? "%cCSI" |
| 225 | : e.type === "osc" |
| 226 | ? "%cOSC" |
| 227 | : e.type === "esc" |
| 228 | ? "%cESC" |
| 229 | : "%cTXT"; |
| 230 | const color = e.type === "sgr" |
| 231 | ? "background:#2d5a27;color:#fff;padding:1px 4px;border-radius:2px" |
| 232 | : e.type === "csi" |
| 233 | ? "background:#1e4a7a;color:#fff;padding:1px 4px;border-radius:2px" |
| 234 | : "background:#555;color:#fff;padding:1px 4px;border-radius:2px"; |
| 235 | const detail = [ |
| 236 | e.private ? `private=${e.private}` : "", |
| 237 | e.params ? `params=[${e.params}]` : "", |
| 238 | e.final ? `final=${e.final}` : "", |
| 239 | ] |
| 240 | .filter(Boolean) |
| 241 | .join(" "); |
| 242 | console.log(`${badge} ${e.raw.slice(0, 40)}`, color, detail ? ` ${detail}` : ""); |
| 243 | } |
| 244 | console.groupEnd(); |
| 245 | } |
| 246 | dumpUnhandled() { |
| 247 | const entries = this.unhandled(); |
| 248 | if (entries.length === 0) { |
| 249 | console.log("%cwterm debug — no unhandled sequences", "color: #6a9955"); |
| 250 | return; |
| 251 | } |
| 252 | console.group(`%cwterm debug — ${entries.length} unhandled sequences`, "color: #d7ba7d; font-weight: bold"); |
| 253 | for (const e of entries) { |
| 254 | console.log(` final=${e.final} private=${e.private || "-"} params=[${e.params.slice(0, e.paramCount)}]`); |
| 255 | } |
| 256 | console.groupEnd(); |
| 257 | } |
| 258 | } |