anvilsign in

collin/anvil

1const 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};
11function 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}
19const ESC = 0x1b;
20function 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 --
128const MAX_TRACES = 500;
129export 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}