terminfo.dev 1.7.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminfo.dev",
3
- "version": "1.7.0",
3
+ "version": "2.1.0",
4
4
  "description": "Test your terminal's feature support and contribute to terminfo.dev",
5
5
  "keywords": [
6
6
  "ansi",
package/src/detect.ts CHANGED
@@ -56,7 +56,10 @@ export function detectTerminal(): TerminalInfo {
56
56
  const bundleId = process.env.__CFBundleIdentifier
57
57
  if (bundleId && os === "macos") {
58
58
  for (const [termName, bid] of Object.entries(BUNDLE_IDS)) {
59
- if (bundleId === bid) { name = termName; break }
59
+ if (bundleId === bid) {
60
+ name = termName
61
+ break
62
+ }
60
63
  }
61
64
  // If bundle ID didn't match known terminals, use it as-is
62
65
  if (name === "unknown" && bundleId) {
@@ -117,15 +120,18 @@ function getMacOSAppVersion(terminalName: string): string {
117
120
  const appPath = execFileSync("mdfind", [`kMDItemCFBundleIdentifier == '${bundleId}'`], {
118
121
  encoding: "utf-8",
119
122
  timeout: 3000,
120
- }).trim().split("\n")[0]
123
+ })
124
+ .trim()
125
+ .split("\n")[0]
121
126
 
122
127
  if (!appPath) return ""
123
128
 
124
129
  // Read version from Info.plist
125
- const version = execFileSync("/usr/libexec/PlistBuddy", [
126
- "-c", "Print :CFBundleShortVersionString",
127
- `${appPath}/Contents/Info.plist`,
128
- ], { encoding: "utf-8", timeout: 2000 }).trim()
130
+ const version = execFileSync(
131
+ "/usr/libexec/PlistBuddy",
132
+ ["-c", "Print :CFBundleShortVersionString", `${appPath}/Contents/Info.plist`],
133
+ { encoding: "utf-8", timeout: 2000 },
134
+ ).trim()
129
135
 
130
136
  return version
131
137
  } catch {
@@ -135,10 +141,14 @@ function getMacOSAppVersion(terminalName: string): string {
135
141
 
136
142
  function detectOS(): string {
137
143
  switch (process.platform) {
138
- case "darwin": return "macos"
139
- case "linux": return "linux"
140
- case "win32": return "windows"
141
- default: return process.platform
144
+ case "darwin":
145
+ return "macos"
146
+ case "linux":
147
+ return "linux"
148
+ case "win32":
149
+ return "windows"
150
+ default:
151
+ return process.platform
142
152
  }
143
153
  }
144
154
 
package/src/index.ts CHANGED
@@ -215,7 +215,7 @@ program
215
215
  notes: data.notes,
216
216
  responses: data.responses,
217
217
  generated: new Date().toISOString(),
218
- cliVersion: "1.7.0",
218
+ cliVersion: "2.1.0",
219
219
  probeCount: ALL_PROBES.length,
220
220
  })
221
221
  if (url) {
@@ -4,6 +4,12 @@
4
4
  * Unlike headless probes (which read cell state programmatically), these send
5
5
  * escape sequences to stdout and read responses from stdin. They verify what
6
6
  * the terminal *claims* to support, not just what a headless library exposes.
7
+ *
8
+ * Probe patterns:
9
+ * 1. Cursor position verification — write sequence, query DSR 6, verify position
10
+ * 2. DECRPM mode query — ask terminal if it recognizes a mode
11
+ * 3. Behavioral mode test — enable mode, verify behavior, disable mode
12
+ * 4. Query/response — send query sequence, match response pattern
7
13
  */
8
14
 
9
15
  import { query, queryCursorPosition, measureRenderedWidth, queryMode } from "../tty.ts"
@@ -20,7 +26,110 @@ export interface Probe {
20
26
  run: () => Promise<ProbeResult>
21
27
  }
22
28
 
29
+ // ── Helper: cursor position probe (move + verify) ──
30
+
31
+ function cursorProbe(
32
+ id: string,
33
+ name: string,
34
+ setup: string,
35
+ sequence: string,
36
+ expectedRow: number,
37
+ expectedCol: number,
38
+ ): Probe {
39
+ return {
40
+ id,
41
+ name,
42
+ async run() {
43
+ process.stdout.write(setup)
44
+ process.stdout.write(sequence)
45
+ const pos = await queryCursorPosition()
46
+ if (!pos) return { pass: false, note: "No cursor response" }
47
+ return {
48
+ pass: pos[0] === expectedRow && pos[1] === expectedCol,
49
+ note:
50
+ pos[0] === expectedRow && pos[1] === expectedCol
51
+ ? undefined
52
+ : `got ${pos[0]};${pos[1]}, expected ${expectedRow};${expectedCol}`,
53
+ response: `${pos[0]};${pos[1]}`,
54
+ }
55
+ },
56
+ }
57
+ }
58
+
59
+ // ── Helper: SGR probe (write SGR + char, verify cursor advanced by 1) ──
60
+
61
+ function sgrProbe(id: string, name: string, sequence: string): Probe {
62
+ return {
63
+ id,
64
+ name,
65
+ async run() {
66
+ // Write SGR sequence + text, verify cursor advances (sequence was parsed, not printed)
67
+ process.stdout.write("\x1b[1;1H\x1b[2K") // clear line
68
+ process.stdout.write(sequence + "X\x1b[0m")
69
+ const pos = await queryCursorPosition()
70
+ if (!pos) return { pass: false, note: "No cursor response" }
71
+ // Cursor should be at col 2 (wrote 1 char "X")
72
+ return {
73
+ pass: pos[1] === 2,
74
+ note: pos[1] === 2 ? undefined : `cursor at col ${pos[1]}, expected 2`,
75
+ }
76
+ },
77
+ }
78
+ }
79
+
80
+ // ── Helper: DECRPM mode probe ──
81
+
82
+ function modeProbe(id: string, name: string, modeNum: number): Probe {
83
+ return {
84
+ id,
85
+ name,
86
+ async run() {
87
+ const result = await queryMode(modeNum)
88
+ if (result === null) return { pass: false, note: "No DECRPM response" }
89
+ return {
90
+ pass: result !== "unknown",
91
+ note: result === "unknown" ? "Mode not recognized" : `Mode ${result}`,
92
+ response: result,
93
+ }
94
+ },
95
+ }
96
+ }
97
+
98
+ // ── Helper: behavioral mode probe (try DECRPM first, fall back to behavior) ──
99
+
100
+ function behavioralModeProbe(
101
+ id: string,
102
+ name: string,
103
+ modeNum: number,
104
+ enableSeq: string,
105
+ disableSeq: string,
106
+ behaviorTest: () => Promise<ProbeResult>,
107
+ ): Probe {
108
+ return {
109
+ id,
110
+ name,
111
+ async run() {
112
+ // Try DECRPM first
113
+ const decrpmResult = await queryMode(modeNum)
114
+ if (decrpmResult !== null && decrpmResult !== "unknown") {
115
+ return {
116
+ pass: true,
117
+ note: `DECRPM: mode ${decrpmResult}`,
118
+ response: decrpmResult,
119
+ }
120
+ }
121
+ // Fall back to behavioral test
122
+ process.stdout.write(enableSeq)
123
+ const result = await behaviorTest()
124
+ process.stdout.write(disableSeq)
125
+ return result
126
+ },
127
+ }
128
+ }
129
+
130
+ // ═══════════════════════════════════════════════════════════════════════════
23
131
  // ── Cursor probes ──
132
+ // ═══════════════════════════════════════════════════════════════════════════
24
133
 
25
134
  const cursorPositionReport: Probe = {
26
135
  id: "cursor.position-report",
@@ -41,9 +150,6 @@ const cursorShape: Probe = {
41
150
  id: "cursor.shape",
42
151
  name: "Cursor shape (DECSCUSR)",
43
152
  async run() {
44
- // Set cursor to bar shape, then query via DECRPM-like
45
- // Most terminals accept DECSCUSR but we can't read shape back via PTY
46
- // Instead, verify the sequence doesn't crash and cursor still responds
47
153
  process.stdout.write("\x1b[5 q") // blinking bar
48
154
  const pos = await queryCursorPosition()
49
155
  process.stdout.write("\x1b[0 q") // restore default
@@ -54,7 +160,42 @@ const cursorShape: Probe = {
54
160
  },
55
161
  }
56
162
 
163
+ const cursorSaveRestore: Probe = {
164
+ id: "cursor.save-restore",
165
+ name: "Cursor save/restore (DECSC/DECRC)",
166
+ async run() {
167
+ process.stdout.write("\x1b[3;5H") // Move to row 3, col 5
168
+ process.stdout.write("\x1b7") // DECSC — save cursor
169
+ process.stdout.write("\x1b[10;10H") // Move somewhere else
170
+ process.stdout.write("\x1b8") // DECRC — restore cursor
171
+ const pos = await queryCursorPosition()
172
+ if (!pos) return { pass: false, note: "No cursor response after restore" }
173
+ return {
174
+ pass: pos[0] === 3 && pos[1] === 5,
175
+ note: pos[0] === 3 && pos[1] === 5 ? undefined : `got ${pos[0]};${pos[1]}, expected 3;5`,
176
+ response: `${pos[0]};${pos[1]}`,
177
+ }
178
+ },
179
+ }
180
+
181
+ const cursorHide: Probe = {
182
+ id: "cursor.hide",
183
+ name: "Cursor hide/show (DECTCEM)",
184
+ async run() {
185
+ process.stdout.write("\x1b[?25l") // hide cursor
186
+ const posHidden = await queryCursorPosition()
187
+ process.stdout.write("\x1b[?25h") // show cursor
188
+ if (!posHidden) return { pass: false, note: "No cursor response while hidden" }
189
+ const posVisible = await queryCursorPosition()
190
+ if (!posVisible) return { pass: false, note: "No cursor response after show" }
191
+ // Both responses mean terminal processed hide/show without breaking DSR
192
+ return { pass: true }
193
+ },
194
+ }
195
+
196
+ // ═══════════════════════════════════════════════════════════════════════════
57
197
  // ── Device probes ──
198
+ // ═══════════════════════════════════════════════════════════════════════════
58
199
 
59
200
  const primaryDA: Probe = {
60
201
  id: "device.primary-da",
@@ -80,31 +221,143 @@ const deviceStatusReport: Probe = {
80
221
  },
81
222
  }
82
223
 
83
- // ── Mode probes (DECRPM) ──
224
+ const deviceDecrpm: Probe = {
225
+ id: "device.decrpm",
226
+ name: "DECRPM support (mode query)",
227
+ async run() {
228
+ // Query DECAWM (mode 7) — universally supported, good test for DECRPM itself
229
+ const result = await queryMode(7)
230
+ if (result === null) return { pass: false, note: "No DECRPM response" }
231
+ return {
232
+ pass: result !== "unknown",
233
+ note: result === "unknown" ? "Terminal does not support DECRPM" : `DECAWM is ${result}`,
234
+ response: result,
235
+ }
236
+ },
237
+ }
84
238
 
85
- function modeProbe(id: string, name: string, modeNum: number): Probe {
86
- return {
87
- id,
88
- name,
89
- async run() {
90
- const result = await queryMode(modeNum)
91
- if (result === null) return { pass: false, note: "No DECRPM response" }
92
- return {
93
- pass: result !== "unknown",
94
- note: result === "unknown" ? "Mode not recognized" : `Mode ${result}`,
95
- response: result,
96
- }
97
- },
98
- }
239
+ // ═══════════════════════════════════════════════════════════════════════════
240
+ // ── Text probes ──
241
+ // ═══════════════════════════════════════════════════════════════════════════
242
+
243
+ const textBasic: Probe = {
244
+ id: "text.basic",
245
+ name: "Basic text output",
246
+ async run() {
247
+ process.stdout.write("\x1b[1;1H\x1b[2K") // clear line, move to 1;1
248
+ process.stdout.write("Hello")
249
+ const pos = await queryCursorPosition()
250
+ if (!pos) return { pass: false, note: "No cursor response" }
251
+ return {
252
+ pass: pos[1] === 6,
253
+ note: pos[1] === 6 ? undefined : `cursor at col ${pos[1]}, expected 6`,
254
+ }
255
+ },
99
256
  }
100
257
 
101
- // ── Text width probes ──
258
+ const textWrap: Probe = {
259
+ id: "text.wrap",
260
+ name: "Text wrapping at terminal width",
261
+ async run() {
262
+ // Get terminal width
263
+ const cols = process.stdout.columns || 80
264
+ process.stdout.write("\x1b[1;1H\x1b[2K") // clear line, move to 1;1
265
+ // Write exactly cols characters to fill the line, then 1 more to wrap
266
+ const line = "W".repeat(cols) + "X"
267
+ process.stdout.write(line)
268
+ const pos = await queryCursorPosition()
269
+ if (!pos) return { pass: false, note: "No cursor response" }
270
+ // After wrapping, cursor should be on row 2, col 2 (the "X" + 1)
271
+ return {
272
+ pass: pos[0] === 2 && pos[1] === 2,
273
+ note: pos[0] === 2 && pos[1] === 2 ? undefined : `cursor at ${pos[0]};${pos[1]}, expected 2;2`,
274
+ }
275
+ },
276
+ }
277
+
278
+ const textCR: Probe = {
279
+ id: "text.cr",
280
+ name: "Carriage return (CR)",
281
+ async run() {
282
+ process.stdout.write("\x1b[1;1H\x1b[2K")
283
+ process.stdout.write("AB")
284
+ process.stdout.write("\r") // CR
285
+ const pos = await queryCursorPosition()
286
+ if (!pos) return { pass: false, note: "No cursor response" }
287
+ return {
288
+ pass: pos[1] === 1,
289
+ note: pos[1] === 1 ? undefined : `cursor at col ${pos[1]}, expected 1`,
290
+ }
291
+ },
292
+ }
293
+
294
+ const textNewline: Probe = {
295
+ id: "text.newline",
296
+ name: "Newline (LF)",
297
+ async run() {
298
+ process.stdout.write("\x1b[3;5H") // move to row 3, col 5
299
+ process.stdout.write("\n") // LF
300
+ const pos = await queryCursorPosition()
301
+ if (!pos) return { pass: false, note: "No cursor response" }
302
+ return {
303
+ pass: pos[0] === 4,
304
+ note: pos[0] === 4 ? undefined : `cursor at row ${pos[0]}, expected 4`,
305
+ }
306
+ },
307
+ }
308
+
309
+ const textOverwrite: Probe = {
310
+ id: "text.overwrite",
311
+ name: "Character overwrite",
312
+ async run() {
313
+ process.stdout.write("\x1b[1;1H\x1b[2K")
314
+ process.stdout.write("AB") // cursor at col 3
315
+ process.stdout.write("\x1b[1;2H") // move back to col 2
316
+ process.stdout.write("X") // overwrite B
317
+ const pos = await queryCursorPosition()
318
+ if (!pos) return { pass: false, note: "No cursor response" }
319
+ return {
320
+ pass: pos[1] === 3,
321
+ note: pos[1] === 3 ? undefined : `cursor at col ${pos[1]}, expected 3`,
322
+ }
323
+ },
324
+ }
325
+
326
+ const textIndex: Probe = {
327
+ id: "text.index",
328
+ name: "Index (IND — ESC D)",
329
+ async run() {
330
+ process.stdout.write("\x1b[3;5H") // move to row 3, col 5
331
+ process.stdout.write("\x1bD") // IND — move cursor down one line
332
+ const pos = await queryCursorPosition()
333
+ if (!pos) return { pass: false, note: "No cursor response" }
334
+ return {
335
+ pass: pos[0] === 4 && pos[1] === 5,
336
+ note: pos[0] === 4 && pos[1] === 5 ? undefined : `got ${pos[0]};${pos[1]}, expected 4;5`,
337
+ }
338
+ },
339
+ }
340
+
341
+ const textNextLine: Probe = {
342
+ id: "text.next-line",
343
+ name: "Next line (NEL — ESC E)",
344
+ async run() {
345
+ process.stdout.write("\x1b[3;5H") // move to row 3, col 5
346
+ process.stdout.write("\x1bE") // NEL — next line (col 1 of next row)
347
+ const pos = await queryCursorPosition()
348
+ if (!pos) return { pass: false, note: "No cursor response" }
349
+ return {
350
+ pass: pos[0] === 4 && pos[1] === 1,
351
+ note: pos[0] === 4 && pos[1] === 1 ? undefined : `got ${pos[0]};${pos[1]}, expected 4;1`,
352
+ }
353
+ },
354
+ }
102
355
 
103
356
  const wideCharCJK: Probe = {
104
357
  id: "text.wide.cjk",
105
358
  name: "CJK wide chars (2 cols)",
106
359
  async run() {
107
- const width = await measureRenderedWidth("")
360
+ const width = await measureRenderedWidth("\u4e2d")
108
361
  if (width === null) return { pass: false, note: "Cannot measure width" }
109
362
  return {
110
363
  pass: width === 2,
@@ -117,7 +370,7 @@ const wideCharEmoji: Probe = {
117
370
  id: "text.wide.emoji",
118
371
  name: "Emoji wide chars (2 cols)",
119
372
  async run() {
120
- const width = await measureRenderedWidth("😀")
373
+ const width = await measureRenderedWidth("\u{1F600}")
121
374
  if (width === null) return { pass: false, note: "Cannot measure width" }
122
375
  return {
123
376
  pass: width === 2,
@@ -126,48 +379,130 @@ const wideCharEmoji: Probe = {
126
379
  },
127
380
  }
128
381
 
129
- // ── SGR probes (write + cursor position to verify parsing) ──
382
+ const tabStop: Probe = {
383
+ id: "text.tab",
384
+ name: "Tab stop (default 8-col)",
385
+ async run() {
386
+ process.stdout.write("\x1b[1;1H\x1b[2K") // Clear line, move to col 1
387
+ process.stdout.write("\t") // Tab
388
+ const pos = await queryCursorPosition()
389
+ if (!pos) return { pass: false, note: "No cursor response" }
390
+ return {
391
+ pass: pos[1] === 9,
392
+ note: pos[1] === 9 ? undefined : `cursor at col ${pos[1]}, expected 9`,
393
+ }
394
+ },
395
+ }
130
396
 
131
- function sgrProbe(id: string, name: string, sequence: string): Probe {
132
- return {
133
- id,
134
- name,
135
- async run() {
136
- // Write SGR sequence + text, verify cursor advances (sequence was parsed, not printed)
137
- process.stdout.write("\x1b[1;1H\x1b[2K") // clear line
138
- process.stdout.write(sequence + "X\x1b[0m")
139
- const pos = await queryCursorPosition()
140
- if (!pos) return { pass: false, note: "No cursor response" }
141
- // Cursor should be at col 2 (wrote 1 char "X")
142
- return {
143
- pass: pos[1] === 2,
144
- note: pos[1] === 2 ? undefined : `cursor at col ${pos[1]}, expected 2`,
145
- }
146
- },
147
- }
397
+ const backspace: Probe = {
398
+ id: "text.backspace",
399
+ name: "Backspace (BS)",
400
+ async run() {
401
+ process.stdout.write("\x1b[1;5H") // Move to col 5
402
+ process.stdout.write("\b") // BS
403
+ const pos = await queryCursorPosition()
404
+ if (!pos) return { pass: false, note: "No cursor response" }
405
+ return {
406
+ pass: pos[1] === 4,
407
+ note: pos[1] === 4 ? undefined : `cursor at col ${pos[1]}, expected 4`,
408
+ }
409
+ },
148
410
  }
149
411
 
150
- // ── Cursor save/restore ──
412
+ // ═══════════════════════════════════════════════════════════════════════════
413
+ // ── Cursor movement probes ──
414
+ // ═══════════════════════════════════════════════════════════════════════════
151
415
 
152
- const cursorSaveRestore: Probe = {
153
- id: "cursor.save-restore",
154
- name: "Cursor save/restore (DECSC/DECRC)",
416
+ // CUP cursor absolute position
417
+ const cursorMoveAbsolute = cursorProbe(
418
+ "cursor.move.absolute",
419
+ "Cursor absolute position (CUP)",
420
+ "", // no setup needed
421
+ "\x1b[5;10H",
422
+ 5,
423
+ 10,
424
+ )
425
+
426
+ // CUU — cursor up
427
+ const cursorMoveUp = cursorProbe(
428
+ "cursor.move.up",
429
+ "Cursor up (CUU)",
430
+ "\x1b[5;5H", // move to 5;5
431
+ "\x1b[2A", // up 2
432
+ 3,
433
+ 5,
434
+ )
435
+
436
+ // CUD — cursor down
437
+ const cursorMoveDown = cursorProbe(
438
+ "cursor.move.down",
439
+ "Cursor down (CUD)",
440
+ "\x1b[5;5H", // move to 5;5
441
+ "\x1b[2B", // down 2
442
+ 7,
443
+ 5,
444
+ )
445
+
446
+ // CUF — cursor forward
447
+ const cursorMoveForward = cursorProbe(
448
+ "cursor.move.forward",
449
+ "Cursor forward (CUF)",
450
+ "\x1b[5;5H", // move to 5;5
451
+ "\x1b[3C", // forward 3
452
+ 5,
453
+ 8,
454
+ )
455
+
456
+ // CUB — cursor back
457
+ const cursorMoveBack = cursorProbe(
458
+ "cursor.move.back",
459
+ "Cursor back (CUB)",
460
+ "\x1b[5;5H", // move to 5;5
461
+ "\x1b[3D", // back 3
462
+ 5,
463
+ 2,
464
+ )
465
+
466
+ // CUP with no args — home
467
+ const cursorMoveHome = cursorProbe(
468
+ "cursor.move.home",
469
+ "Cursor home (CUP no args)",
470
+ "\x1b[5;5H", // move to 5;5
471
+ "\x1b[H", // home
472
+ 1,
473
+ 1,
474
+ )
475
+
476
+ // CHA — cursor horizontal absolute
477
+ const cursorHorizontalAbsolute: Probe = {
478
+ id: "cursor.horizontal-absolute",
479
+ name: "Cursor horizontal absolute (CHA)",
155
480
  async run() {
156
- process.stdout.write("\x1b[3;5H") // Move to row 3, col 5
157
- process.stdout.write("\x1b7") // DECSC save cursor
158
- process.stdout.write("\x1b[10;10H") // Move somewhere else
159
- process.stdout.write("\x1b8") // DECRC — restore cursor
481
+ process.stdout.write("\x1b[3;1H") // move to row 3
482
+ process.stdout.write("\x1b[15G") // CHA col 15
160
483
  const pos = await queryCursorPosition()
161
- if (!pos) return { pass: false, note: "No cursor response after restore" }
484
+ if (!pos) return { pass: false, note: "No cursor response" }
162
485
  return {
163
- pass: pos[0] === 3 && pos[1] === 5,
164
- note: pos[0] === 3 && pos[1] === 5 ? undefined : `got ${pos[0]};${pos[1]}, expected 3;5`,
486
+ pass: pos[0] === 3 && pos[1] === 15,
487
+ note: pos[0] === 3 && pos[1] === 15 ? undefined : `got ${pos[0]};${pos[1]}, expected 3;15`,
165
488
  response: `${pos[0]};${pos[1]}`,
166
489
  }
167
490
  },
168
491
  }
169
492
 
493
+ // CNL — cursor next line
494
+ const cursorNextLine = cursorProbe(
495
+ "cursor.next-line",
496
+ "Cursor next line (CNL)",
497
+ "\x1b[3;5H", // move to row 3, col 5
498
+ "\x1b[E", // CNL — next line
499
+ 4,
500
+ 1,
501
+ )
502
+
503
+ // ═══════════════════════════════════════════════════════════════════════════
170
504
  // ── Erase probes ──
505
+ // ═══════════════════════════════════════════════════════════════════════════
171
506
 
172
507
  const eraseLineRight: Probe = {
173
508
  id: "erase.line.right",
@@ -186,14 +521,51 @@ const eraseLineRight: Probe = {
186
521
  },
187
522
  }
188
523
 
189
- const eraseScreenScrollback: Probe = {
190
- id: "erase.screen.scrollback",
191
- name: "Erase scrollback (ED 3)",
524
+ const eraseLineLeft: Probe = {
525
+ id: "erase.line.left",
526
+ name: "Erase line left (EL 1)",
527
+ async run() {
528
+ process.stdout.write("\x1b[1;1H\x1b[2K")
529
+ process.stdout.write("ABCDE")
530
+ process.stdout.write("\x1b[1;3H") // Move to col 3
531
+ process.stdout.write("\x1b[1K") // EL 1 — erase to left
532
+ const pos = await queryCursorPosition()
533
+ if (!pos) return { pass: false, note: "No cursor response" }
534
+ // Cursor should stay at col 3
535
+ return {
536
+ pass: pos[1] === 3,
537
+ note: pos[1] === 3 ? undefined : `cursor at col ${pos[1]}, expected 3`,
538
+ }
539
+ },
540
+ }
541
+
542
+ const eraseLineAll: Probe = {
543
+ id: "erase.line.all",
544
+ name: "Erase entire line (EL 2)",
545
+ async run() {
546
+ process.stdout.write("\x1b[1;1H\x1b[2K")
547
+ process.stdout.write("ABCDE")
548
+ process.stdout.write("\x1b[1;3H") // Move to col 3
549
+ process.stdout.write("\x1b[2K") // EL 2 — erase entire line
550
+ const pos = await queryCursorPosition()
551
+ if (!pos) return { pass: false, note: "No cursor response" }
552
+ // Cursor should stay at col 3
553
+ return {
554
+ pass: pos[1] === 3,
555
+ note: pos[1] === 3 ? undefined : `cursor at col ${pos[1]}, expected 3`,
556
+ }
557
+ },
558
+ }
559
+
560
+ const eraseScreenAll: Probe = {
561
+ id: "erase.screen.all",
562
+ name: "Erase entire screen (ED 2)",
192
563
  async run() {
193
564
  process.stdout.write("\x1b[5;5H") // Move to known position
194
- process.stdout.write("\x1b[3J") // ED 3 — erase scrollback
565
+ process.stdout.write("\x1b[2J") // ED 2 — erase entire screen
195
566
  const pos = await queryCursorPosition()
196
- if (!pos) return { pass: false, note: "No cursor response after ED 3" }
567
+ if (!pos) return { pass: false, note: "No cursor response after ED 2" }
568
+ // Cursor should stay at 5;5 (ED 2 doesn't move cursor)
197
569
  return {
198
570
  pass: pos[0] === 5 && pos[1] === 5,
199
571
  note: pos[0] === 5 && pos[1] === 5 ? undefined : `cursor at ${pos[0]};${pos[1]}, expected 5;5`,
@@ -201,54 +573,72 @@ const eraseScreenScrollback: Probe = {
201
573
  },
202
574
  }
203
575
 
204
- // ── Scroll region ──
205
-
206
- const scrollRegion: Probe = {
207
- id: "scrollback.set-region",
208
- name: "Scroll region (DECSTBM)",
576
+ const eraseScreenBelow: Probe = {
577
+ id: "erase.screen.below",
578
+ name: "Erase screen below (ED 0)",
209
579
  async run() {
210
- process.stdout.write("\x1b[5;10r") // Set scroll region rows 5–10
211
- process.stdout.write("\x1b[r") // Reset scroll region
580
+ process.stdout.write("\x1b[5;5H") // Move to known position
581
+ process.stdout.write("\x1b[0J") // ED 0 — erase below
212
582
  const pos = await queryCursorPosition()
213
- if (!pos) return { pass: false, note: "No cursor response after DECSTBM" }
214
- // After reset, cursor should still respond — terminal didn't crash
215
- return { pass: true }
583
+ if (!pos) return { pass: false, note: "No cursor response after ED 0" }
584
+ return {
585
+ pass: pos[0] === 5 && pos[1] === 5,
586
+ note: pos[0] === 5 && pos[1] === 5 ? undefined : `cursor at ${pos[0]};${pos[1]}, expected 5;5`,
587
+ }
216
588
  },
217
589
  }
218
590
 
219
- // ── Tab and backspace ──
591
+ const eraseScreenAbove: Probe = {
592
+ id: "erase.screen.above",
593
+ name: "Erase screen above (ED 1)",
594
+ async run() {
595
+ process.stdout.write("\x1b[5;5H") // Move to known position
596
+ process.stdout.write("\x1b[1J") // ED 1 — erase above
597
+ const pos = await queryCursorPosition()
598
+ if (!pos) return { pass: false, note: "No cursor response after ED 1" }
599
+ return {
600
+ pass: pos[0] === 5 && pos[1] === 5,
601
+ note: pos[0] === 5 && pos[1] === 5 ? undefined : `cursor at ${pos[0]};${pos[1]}, expected 5;5`,
602
+ }
603
+ },
604
+ }
220
605
 
221
- const tabStop: Probe = {
222
- id: "text.tab",
223
- name: "Tab stop (default 8-col)",
606
+ const eraseScreenScrollback: Probe = {
607
+ id: "erase.screen.scrollback",
608
+ name: "Erase scrollback (ED 3)",
224
609
  async run() {
225
- process.stdout.write("\x1b[1;1H\x1b[2K") // Clear line, move to col 1
226
- process.stdout.write("\t") // Tab
610
+ process.stdout.write("\x1b[5;5H") // Move to known position
611
+ process.stdout.write("\x1b[3J") // ED 3 — erase scrollback
227
612
  const pos = await queryCursorPosition()
228
- if (!pos) return { pass: false, note: "No cursor response" }
613
+ if (!pos) return { pass: false, note: "No cursor response after ED 3" }
229
614
  return {
230
- pass: pos[1] === 9,
231
- note: pos[1] === 9 ? undefined : `cursor at col ${pos[1]}, expected 9`,
615
+ pass: pos[0] === 5 && pos[1] === 5,
616
+ note: pos[0] === 5 && pos[1] === 5 ? undefined : `cursor at ${pos[0]};${pos[1]}, expected 5;5`,
232
617
  }
233
618
  },
234
619
  }
235
620
 
236
- const backspace: Probe = {
237
- id: "text.backspace",
238
- name: "Backspace (BS)",
621
+ const eraseCharacter: Probe = {
622
+ id: "erase.character",
623
+ name: "Erase characters (ECH)",
239
624
  async run() {
240
- process.stdout.write("\x1b[1;5H") // Move to col 5
241
- process.stdout.write("\b") // BS
625
+ process.stdout.write("\x1b[1;1H\x1b[2K")
626
+ process.stdout.write("ABCD")
627
+ process.stdout.write("\x1b[1;2H") // Move to col 2
628
+ process.stdout.write("\x1b[2X") // ECH 2 — erase 2 chars at cursor
242
629
  const pos = await queryCursorPosition()
243
630
  if (!pos) return { pass: false, note: "No cursor response" }
631
+ // Cursor should stay at col 2 (ECH doesn't move cursor)
244
632
  return {
245
- pass: pos[1] === 4,
246
- note: pos[1] === 4 ? undefined : `cursor at col ${pos[1]}, expected 4`,
633
+ pass: pos[1] === 2,
634
+ note: pos[1] === 2 ? undefined : `cursor at col ${pos[1]}, expected 2`,
247
635
  }
248
636
  },
249
637
  }
250
638
 
251
- // ── Insert/delete character probes ──
639
+ // ═══════════════════════════════════════════════════════════════════════════
640
+ // ── Editing probes ──
641
+ // ═══════════════════════════════════════════════════════════════════════════
252
642
 
253
643
  const insertChars: Probe = {
254
644
  id: "editing.insert-chars",
@@ -286,7 +676,213 @@ const deleteChars: Probe = {
286
676
  },
287
677
  }
288
678
 
289
- // ── Reset ──
679
+ const insertLines: Probe = {
680
+ id: "editing.insert-lines",
681
+ name: "Insert lines (IL)",
682
+ async run() {
683
+ process.stdout.write("\x1b[3;5H") // Move to row 3, col 5
684
+ process.stdout.write("\x1b[1L") // IL 1 — insert 1 line
685
+ const pos = await queryCursorPosition()
686
+ if (!pos) return { pass: false, note: "No cursor response" }
687
+ // After IL, cursor should remain on same row (now blank), col reset to 1
688
+ // Behavior varies slightly but cursor should still be on row 3
689
+ return {
690
+ pass: pos[0] === 3,
691
+ note: pos[0] === 3 ? undefined : `cursor at row ${pos[0]}, expected 3`,
692
+ }
693
+ },
694
+ }
695
+
696
+ const deleteLines: Probe = {
697
+ id: "editing.delete-lines",
698
+ name: "Delete lines (DL)",
699
+ async run() {
700
+ process.stdout.write("\x1b[3;5H") // Move to row 3, col 5
701
+ process.stdout.write("\x1b[1M") // DL 1 — delete 1 line
702
+ const pos = await queryCursorPosition()
703
+ if (!pos) return { pass: false, note: "No cursor response" }
704
+ // After DL, cursor should remain on same row
705
+ return {
706
+ pass: pos[0] === 3,
707
+ note: pos[0] === 3 ? undefined : `cursor at row ${pos[0]}, expected 3`,
708
+ }
709
+ },
710
+ }
711
+
712
+ const repeatChar: Probe = {
713
+ id: "editing.repeat-char",
714
+ name: "Repeat character (REP)",
715
+ async run() {
716
+ process.stdout.write("\x1b[1;1H\x1b[2K") // clear line
717
+ process.stdout.write("X") // write X (cursor at col 2)
718
+ process.stdout.write("\x1b[4b") // REP 4 — repeat last char 4 times
719
+ const pos = await queryCursorPosition()
720
+ if (!pos) return { pass: false, note: "No cursor response" }
721
+ // 1 original "X" + 4 repeated = cursor at col 6
722
+ return {
723
+ pass: pos[1] === 6,
724
+ note: pos[1] === 6 ? undefined : `cursor at col ${pos[1]}, expected 6`,
725
+ }
726
+ },
727
+ }
728
+
729
+ // ═══════════════════════════════════════════════════════════════════════════
730
+ // ── Mode probes (behavioral, with DECRPM fallback) ──
731
+ // ═══════════════════════════════════════════════════════════════════════════
732
+
733
+ const modesAltScreen = behavioralModeProbe(
734
+ "modes.alt-screen",
735
+ "Alternate screen buffer (DECSET 1049)",
736
+ 1049,
737
+ "\x1b[?1049h", // enter alt screen
738
+ "\x1b[?1049l", // exit alt screen
739
+ async () => {
740
+ // In alt screen: write text, then exit — if alt screen works, cursor
741
+ // returns to saved position from before entering
742
+ process.stdout.write("\x1b[1;1H") // move to 1;1 in alt screen
743
+ process.stdout.write("TEST")
744
+ const pos = await queryCursorPosition()
745
+ if (!pos) return { pass: false, note: "No cursor response in alt screen" }
746
+ return { pass: true, note: "Behavioral: entered and responded" }
747
+ },
748
+ )
749
+
750
+ const modesAutoWrap = behavioralModeProbe(
751
+ "modes.auto-wrap",
752
+ "Auto-wrap mode (DECAWM)",
753
+ 7,
754
+ "\x1b[?7h", // enable auto-wrap
755
+ "", // don't disable — auto-wrap is normally on
756
+ async () => {
757
+ const cols = process.stdout.columns || 80
758
+ process.stdout.write("\x1b[1;1H\x1b[2K")
759
+ // Write exactly `cols` chars to fill line, then one more
760
+ process.stdout.write("A".repeat(cols) + "B")
761
+ const pos = await queryCursorPosition()
762
+ if (!pos) return { pass: false, note: "No cursor response" }
763
+ // Should have wrapped to row 2
764
+ return {
765
+ pass: pos[0] === 2,
766
+ note: pos[0] === 2 ? "Behavioral: wrap confirmed" : `cursor at row ${pos[0]}, expected 2`,
767
+ }
768
+ },
769
+ )
770
+
771
+ const modesBracketedPaste = behavioralModeProbe(
772
+ "modes.bracketed-paste",
773
+ "Bracketed paste mode (DECSET 2004)",
774
+ 2004,
775
+ "\x1b[?2004h", // enable
776
+ "\x1b[?2004l", // disable
777
+ async () => {
778
+ // Can't test paste behavior without pasting — just verify DA1 responds
779
+ const match = await query("\x1b[c", /\x1b\[\?([0-9;]+)c/, 1000)
780
+ if (!match) return { pass: false, note: "No DA1 response after enabling bracketed paste" }
781
+ return { pass: true, note: "Behavioral: terminal responsive after enable" }
782
+ },
783
+ )
784
+
785
+ const modesInsertReplace: Probe = {
786
+ id: "modes.insert-replace",
787
+ name: "Insert/replace mode (IRM)",
788
+ async run() {
789
+ process.stdout.write("\x1b[1;1H\x1b[2K")
790
+ process.stdout.write("ABCD")
791
+ process.stdout.write("\x1b[1;2H") // move to col 2
792
+ process.stdout.write("\x1b[4h") // enable insert mode (IRM)
793
+ process.stdout.write("X") // insert X, should shift BCD right
794
+ process.stdout.write("\x1b[4l") // disable insert mode
795
+ const pos = await queryCursorPosition()
796
+ if (!pos) return { pass: false, note: "No cursor response" }
797
+ // After inserting X at col 2, cursor should be at col 3
798
+ return {
799
+ pass: pos[1] === 3,
800
+ note: pos[1] === 3 ? undefined : `cursor at col ${pos[1]}, expected 3`,
801
+ }
802
+ },
803
+ }
804
+
805
+ const modesApplicationKeypad: Probe = {
806
+ id: "modes.application-keypad",
807
+ name: "Application keypad mode (DECKPAM/DECKPNM)",
808
+ async run() {
809
+ process.stdout.write("\x1b=") // DECKPAM — enable application keypad
810
+ const pos = await queryCursorPosition()
811
+ process.stdout.write("\x1b>") // DECKPNM — disable (normal keypad)
812
+ return {
813
+ pass: pos !== null,
814
+ note: pos ? undefined : "No cursor response after DECKPAM",
815
+ }
816
+ },
817
+ }
818
+
819
+ // ═══════════════════════════════════════════════════════════════════════════
820
+ // ── Scrollback probes ──
821
+ // ═══════════════════════════════════════════════════════════════════════════
822
+
823
+ const scrollRegion: Probe = {
824
+ id: "scrollback.set-region",
825
+ name: "Scroll region (DECSTBM)",
826
+ async run() {
827
+ process.stdout.write("\x1b[5;10r") // Set scroll region rows 5-10
828
+ process.stdout.write("\x1b[r") // Reset scroll region
829
+ const pos = await queryCursorPosition()
830
+ if (!pos) return { pass: false, note: "No cursor response after DECSTBM" }
831
+ return { pass: true }
832
+ },
833
+ }
834
+
835
+ const scrollUp: Probe = {
836
+ id: "scrollback.scroll-up",
837
+ name: "Scroll up (SU)",
838
+ async run() {
839
+ process.stdout.write("\x1b[5;5H") // Move to row 5, col 5
840
+ process.stdout.write("\x1b[1S") // SU 1 — scroll up 1 line
841
+ const pos = await queryCursorPosition()
842
+ if (!pos) return { pass: false, note: "No cursor response after SU" }
843
+ // Cursor position should remain at 5;5 (SU scrolls content, not cursor)
844
+ return {
845
+ pass: pos[0] === 5 && pos[1] === 5,
846
+ note: pos[0] === 5 && pos[1] === 5 ? undefined : `cursor at ${pos[0]};${pos[1]}, expected 5;5`,
847
+ }
848
+ },
849
+ }
850
+
851
+ const scrollDown: Probe = {
852
+ id: "scrollback.scroll-down",
853
+ name: "Scroll down (SD)",
854
+ async run() {
855
+ process.stdout.write("\x1b[5;5H") // Move to row 5, col 5
856
+ process.stdout.write("\x1b[1T") // SD 1 — scroll down 1 line
857
+ const pos = await queryCursorPosition()
858
+ if (!pos) return { pass: false, note: "No cursor response after SD" }
859
+ // Cursor position should remain at 5;5
860
+ return {
861
+ pass: pos[0] === 5 && pos[1] === 5,
862
+ note: pos[0] === 5 && pos[1] === 5 ? undefined : `cursor at ${pos[0]};${pos[1]}, expected 5;5`,
863
+ }
864
+ },
865
+ }
866
+
867
+ const reverseIndex: Probe = {
868
+ id: "scrollback.reverse-index",
869
+ name: "Reverse index (RI — ESC M)",
870
+ async run() {
871
+ // Move to row 1 and reverse-index — should stay at row 1 (scrolls content down)
872
+ process.stdout.write("\x1b[1;5H") // row 1, col 5
873
+ process.stdout.write("\x1bM") // RI — reverse index
874
+ const pos = await queryCursorPosition()
875
+ if (!pos) return { pass: false, note: "No cursor response after RI" }
876
+ return {
877
+ pass: pos[0] === 1 && pos[1] === 5,
878
+ note: pos[0] === 1 && pos[1] === 5 ? undefined : `got ${pos[0]};${pos[1]}, expected 1;5`,
879
+ }
880
+ },
881
+ }
882
+
883
+ // ═══════════════════════════════════════════════════════════════════════════
884
+ // ── Reset probes ──
885
+ // ═══════════════════════════════════════════════════════════════════════════
290
886
 
291
887
  const resetRIS: Probe = {
292
888
  id: "reset.ris",
@@ -303,7 +899,76 @@ const resetRIS: Probe = {
303
899
  },
304
900
  }
305
901
 
902
+ const resetSGR: Probe = {
903
+ id: "reset.sgr",
904
+ name: "SGR reset (SGR 0)",
905
+ async run() {
906
+ process.stdout.write("\x1b[1;1H\x1b[2K")
907
+ process.stdout.write("\x1b[1m") // bold
908
+ process.stdout.write("\x1b[0m") // reset
909
+ process.stdout.write("X")
910
+ const pos = await queryCursorPosition()
911
+ if (!pos) return { pass: false, note: "No cursor response" }
912
+ return {
913
+ pass: pos[1] === 2,
914
+ note: pos[1] === 2 ? undefined : `cursor at col ${pos[1]}, expected 2`,
915
+ }
916
+ },
917
+ }
918
+
919
+ const resetSoft: Probe = {
920
+ id: "reset.soft",
921
+ name: "Soft terminal reset (DECSTR)",
922
+ async run() {
923
+ process.stdout.write("\x1b[5;5H") // Move to known position
924
+ process.stdout.write("\x1b[!p") // DECSTR — soft reset
925
+ const pos = await queryCursorPosition()
926
+ if (!pos) return { pass: false, note: "No cursor response after DECSTR" }
927
+ // Soft reset doesn't move cursor to 1;1 (unlike RIS) — just verify response
928
+ return { pass: true }
929
+ },
930
+ }
931
+
932
+ // ═══════════════════════════════════════════════════════════════════════════
933
+ // ── Charset probes ──
934
+ // ═══════════════════════════════════════════════════════════════════════════
935
+
936
+ const charsetDecSpecial: Probe = {
937
+ id: "charsets.dec-special",
938
+ name: "DEC special graphics charset",
939
+ async run() {
940
+ process.stdout.write("\x1b[1;1H\x1b[2K")
941
+ process.stdout.write("\x1b(0") // Switch to DEC special graphics
942
+ process.stdout.write("q") // should render as horizontal line (U+2500)
943
+ process.stdout.write("\x1b(B") // Switch back to ASCII
944
+ const pos = await queryCursorPosition()
945
+ if (!pos) return { pass: false, note: "No cursor response" }
946
+ // Character should advance cursor by 1
947
+ return {
948
+ pass: pos[1] === 2,
949
+ note: pos[1] === 2 ? undefined : `cursor at col ${pos[1]}, expected 2`,
950
+ }
951
+ },
952
+ }
953
+
954
+ const charsetUtf8: Probe = {
955
+ id: "charsets.utf8",
956
+ name: "UTF-8 encoding",
957
+ async run() {
958
+ process.stdout.write("\x1b[1;1H\x1b[2K")
959
+ process.stdout.write("\u00e9") // e-acute (2-byte UTF-8, 1 column)
960
+ const pos = await queryCursorPosition()
961
+ if (!pos) return { pass: false, note: "No cursor response" }
962
+ return {
963
+ pass: pos[1] === 2,
964
+ note: pos[1] === 2 ? undefined : `cursor at col ${pos[1]}, expected 2`,
965
+ }
966
+ },
967
+ }
968
+
969
+ // ═══════════════════════════════════════════════════════════════════════════
306
970
  // ── Extensions probes ──
971
+ // ═══════════════════════════════════════════════════════════════════════════
307
972
 
308
973
  const kittyKeyboard: Probe = {
309
974
  id: "extensions.kitty-keyboard",
@@ -352,11 +1017,16 @@ const sixelRender: Probe = {
352
1017
 
353
1018
  const osc52Clipboard: Probe = {
354
1019
  id: "extensions.osc52-clipboard",
355
- name: "Clipboard query (OSC 52)",
1020
+ name: "Clipboard access (OSC 52)",
356
1021
  async run() {
357
- const match = await query("\x1b]52;c;?\x07", /\x1b\]52;([^\x07\x1b]+)[\x07\x1b]/, 1000)
358
- if (!match) return { pass: false, note: "No OSC 52 response" }
359
- return { pass: true, response: match[1] }
1022
+ // Write a small test value to clipboard, then verify terminal responds
1023
+ // Don't use "?" query it returns full clipboard as huge base64 that leaks into stdin
1024
+ const testData = btoa("terminfo-test")
1025
+ process.stdout.write(`\x1b]52;c;${testData}\x07`)
1026
+ // Verify terminal still responds after OSC 52 (didn't crash/ignore)
1027
+ const pos = await queryCursorPosition()
1028
+ if (!pos) return { pass: false, note: "No response after OSC 52" }
1029
+ return { pass: true }
360
1030
  },
361
1031
  }
362
1032
 
@@ -371,8 +1041,6 @@ const osc7Cwd: Probe = {
371
1041
  },
372
1042
  }
373
1043
 
374
- // ── OSC probes ──
375
-
376
1044
  const osc10FgColor: Probe = {
377
1045
  id: "extensions.osc10-fg-color",
378
1046
  name: "Foreground color query (OSC 10)",
@@ -405,53 +1073,151 @@ const osc2Title: Probe = {
405
1073
  },
406
1074
  }
407
1075
 
1076
+ const extTruecolor: Probe = {
1077
+ id: "extensions.truecolor",
1078
+ name: "24-bit truecolor (SGR 38;2)",
1079
+ async run() {
1080
+ process.stdout.write("\x1b[1;1H\x1b[2K")
1081
+ process.stdout.write("\x1b[38;2;255;0;128mX\x1b[0m")
1082
+ const pos = await queryCursorPosition()
1083
+ if (!pos) return { pass: false, note: "No cursor response" }
1084
+ return {
1085
+ pass: pos[1] === 2,
1086
+ note:
1087
+ pos[1] === 2
1088
+ ? undefined
1089
+ : `cursor at col ${pos[1]}, expected 2 (truecolor sequence may have been printed literally)`,
1090
+ }
1091
+ },
1092
+ }
1093
+
1094
+ const extOsc8Hyperlink: Probe = {
1095
+ id: "extensions.osc8",
1096
+ name: "Hyperlinks (OSC 8)",
1097
+ async run() {
1098
+ process.stdout.write("\x1b[1;1H\x1b[2K")
1099
+ // OSC 8 ; params ; uri ST text OSC 8 ;; ST
1100
+ process.stdout.write("\x1b]8;;http://example.com\x07link\x1b]8;;\x07")
1101
+ const pos = await queryCursorPosition()
1102
+ if (!pos) return { pass: false, note: "No cursor response" }
1103
+ // "link" is 4 chars — cursor should be at col 5
1104
+ return {
1105
+ pass: pos[1] === 5,
1106
+ note: pos[1] === 5 ? undefined : `cursor at col ${pos[1]}, expected 5 (4 visible chars)`,
1107
+ }
1108
+ },
1109
+ }
1110
+
1111
+ const extOsc0IconTitle: Probe = {
1112
+ id: "extensions.osc0-icon-title",
1113
+ name: "Set icon name and title (OSC 0)",
1114
+ async run() {
1115
+ process.stdout.write("\x1b]0;test-title\x07")
1116
+ const pos = await queryCursorPosition()
1117
+ process.stdout.write("\x1b]0;\x07") // reset
1118
+ return {
1119
+ pass: pos !== null,
1120
+ note: pos ? undefined : "No cursor response after OSC 0",
1121
+ }
1122
+ },
1123
+ }
1124
+
1125
+ const extSemanticPrompts: Probe = {
1126
+ id: "extensions.semantic-prompts",
1127
+ name: "Semantic prompts (OSC 133)",
1128
+ async run() {
1129
+ // OSC 133 ; A ST — mark prompt start
1130
+ process.stdout.write("\x1b]133;A\x07")
1131
+ const pos = await queryCursorPosition()
1132
+ return {
1133
+ pass: pos !== null,
1134
+ note: pos ? undefined : "No cursor response after OSC 133",
1135
+ }
1136
+ },
1137
+ }
1138
+
1139
+ // ═══════════════════════════════════════════════════════════════════════════
408
1140
  // ── All probes ──
1141
+ // ═══════════════════════════════════════════════════════════════════════════
409
1142
 
410
1143
  export const ALL_PROBES: Probe[] = [
411
- // Cursor
1144
+ // ── Cursor ──
412
1145
  cursorPositionReport,
413
1146
  cursorShape,
414
1147
  cursorSaveRestore,
1148
+ cursorHide,
1149
+ cursorMoveAbsolute,
1150
+ cursorMoveUp,
1151
+ cursorMoveDown,
1152
+ cursorMoveForward,
1153
+ cursorMoveBack,
1154
+ cursorMoveHome,
1155
+ cursorHorizontalAbsolute,
1156
+ cursorNextLine,
415
1157
 
416
- // Device
1158
+ // ── Device ──
417
1159
  primaryDA,
418
1160
  deviceStatusReport,
1161
+ deviceDecrpm,
419
1162
 
420
- // Modes via DECRPM
421
- modeProbe("modes.alt-screen.enter", "Alt screen (DECSET 1049)", 1049),
422
- modeProbe("modes.bracketed-paste", "Bracketed paste (DECSET 2004)", 2004),
423
- modeProbe("modes.mouse-tracking", "Mouse tracking (DECSET 1000)", 1000),
424
- modeProbe("modes.mouse-sgr", "SGR mouse (DECSET 1006)", 1006),
425
- modeProbe("modes.focus-tracking", "Focus tracking (DECSET 1004)", 1004),
426
- modeProbe("modes.auto-wrap", "Auto-wrap (DECAWM)", 7),
427
- modeProbe("modes.application-cursor", "App cursor keys (DECCKM)", 1),
428
- modeProbe("modes.origin", "Origin mode (DECOM)", 6),
429
- modeProbe("modes.reverse-video", "Reverse video (DECSCNM)", 5),
430
- modeProbe("modes.synchronized-output", "Synchronized output (DECSET 2026)", 2026),
431
-
432
- // Text width
1163
+ // ── Text ──
1164
+ textBasic,
1165
+ textWrap,
1166
+ textCR,
1167
+ textNewline,
1168
+ textOverwrite,
1169
+ textIndex,
1170
+ textNextLine,
433
1171
  wideCharCJK,
434
1172
  wideCharEmoji,
435
-
436
- // Text behavior
437
1173
  tabStop,
438
1174
  backspace,
439
1175
 
440
- // Erase
1176
+ // ── Erase ──
441
1177
  eraseLineRight,
1178
+ eraseLineLeft,
1179
+ eraseLineAll,
1180
+ eraseScreenAll,
1181
+ eraseScreenBelow,
1182
+ eraseScreenAbove,
442
1183
  eraseScreenScrollback,
1184
+ eraseCharacter,
443
1185
 
444
- // Editing
1186
+ // ── Editing ──
445
1187
  insertChars,
446
1188
  deleteChars,
1189
+ insertLines,
1190
+ deleteLines,
1191
+ repeatChar,
447
1192
 
448
- // Scroll region
1193
+ // ── Modes (behavioral with DECRPM fallback) ──
1194
+ modesAltScreen,
1195
+ modesAutoWrap,
1196
+ modesBracketedPaste,
1197
+ modesInsertReplace,
1198
+ modesApplicationKeypad,
1199
+
1200
+ // ── Modes (DECRPM only) ──
1201
+ modeProbe("modes.mouse-tracking", "Mouse tracking (DECSET 1000)", 1000),
1202
+ modeProbe("modes.mouse-sgr", "SGR mouse (DECSET 1006)", 1006),
1203
+ modeProbe("modes.focus-tracking", "Focus tracking (DECSET 1004)", 1004),
1204
+ modeProbe("modes.application-cursor", "App cursor keys (DECCKM)", 1),
1205
+ modeProbe("modes.origin", "Origin mode (DECOM)", 6),
1206
+ modeProbe("modes.reverse-video", "Reverse video (DECSCNM)", 5),
1207
+ modeProbe("modes.synchronized-output", "Synchronized output (DECSET 2026)", 2026),
1208
+
1209
+ // ── Scrollback ──
449
1210
  scrollRegion,
1211
+ scrollUp,
1212
+ scrollDown,
1213
+ reverseIndex,
450
1214
 
451
- // Reset
1215
+ // ── Reset ──
452
1216
  resetRIS,
1217
+ resetSGR,
1218
+ resetSoft,
453
1219
 
454
- // SGR (verify sequence is parsed, not printed)
1220
+ // ── SGR attributes (verify sequence is parsed, not printed) ──
455
1221
  sgrProbe("sgr.bold", "Bold (SGR 1)", "\x1b[1m"),
456
1222
  sgrProbe("sgr.faint", "Faint (SGR 2)", "\x1b[2m"),
457
1223
  sgrProbe("sgr.italic", "Italic (SGR 3)", "\x1b[3m"),
@@ -462,18 +1228,44 @@ export const ALL_PROBES: Probe[] = [
462
1228
  sgrProbe("sgr.underline.dashed", "Dashed underline (SGR 4:5)", "\x1b[4:5m"),
463
1229
  sgrProbe("sgr.blink", "Blink (SGR 5)", "\x1b[5m"),
464
1230
  sgrProbe("sgr.inverse", "Inverse (SGR 7)", "\x1b[7m"),
1231
+ sgrProbe("sgr.hidden", "Hidden (SGR 8)", "\x1b[8m"),
465
1232
  sgrProbe("sgr.strikethrough", "Strikethrough (SGR 9)", "\x1b[9m"),
466
1233
  sgrProbe("sgr.overline", "Overline (SGR 53)", "\x1b[53m"),
467
1234
 
468
- // Extensions
1235
+ // ── SGR colors ──
1236
+ sgrProbe("sgr.fg.standard", "Standard foreground (SGR 31 red)", "\x1b[31m"),
1237
+ sgrProbe("sgr.bg.standard", "Standard background (SGR 41 red)", "\x1b[41m"),
1238
+ sgrProbe("sgr.fg.bright", "Bright foreground (SGR 91)", "\x1b[91m"),
1239
+ sgrProbe("sgr.bg.bright", "Bright background (SGR 101)", "\x1b[101m"),
1240
+ sgrProbe("sgr.fg.default", "Default foreground (SGR 39)", "\x1b[39m"),
1241
+ sgrProbe("sgr.bg.default", "Default background (SGR 49)", "\x1b[49m"),
1242
+ sgrProbe("sgr.fg.256", "256-color foreground (SGR 38;5;196)", "\x1b[38;5;196m"),
1243
+ sgrProbe("sgr.bg.256", "256-color background (SGR 48;5;21)", "\x1b[48;5;21m"),
1244
+ sgrProbe("sgr.fg.truecolor", "Truecolor foreground (SGR 38;2;255;0;128)", "\x1b[38;2;255;0;128m"),
1245
+ sgrProbe("sgr.bg.truecolor", "Truecolor background (SGR 48;2;0;255;64)", "\x1b[48;2;0;255;64m"),
1246
+ sgrProbe("sgr.underline.color", "Underline color (SGR 58;2;255;0;0)", "\x1b[4m\x1b[58;2;255;0;0m"),
1247
+ sgrProbe("sgr.reset", "SGR reset (SGR 0)", "\x1b[1m\x1b[0m"),
1248
+
1249
+ // ── SGR selective resets ──
1250
+ sgrProbe("sgr.selective-reset.bold", "Reset bold (SGR 22)", "\x1b[1m\x1b[22m"),
1251
+ sgrProbe("sgr.selective-reset.underline", "Reset underline (SGR 24)", "\x1b[4m\x1b[24m"),
1252
+ sgrProbe("sgr.selective-reset.inverse", "Reset inverse (SGR 27)", "\x1b[7m\x1b[27m"),
1253
+
1254
+ // ── Charsets ──
1255
+ charsetDecSpecial,
1256
+ charsetUtf8,
1257
+
1258
+ // ── Extensions ──
469
1259
  kittyKeyboard,
470
1260
  sixelDA1,
471
1261
  sixelRender,
472
1262
  osc52Clipboard,
473
1263
  osc7Cwd,
474
-
475
- // OSC
476
1264
  osc10FgColor,
477
1265
  osc11BgColor,
478
1266
  osc2Title,
1267
+ extTruecolor,
1268
+ extOsc8Hyperlink,
1269
+ extOsc0IconTitle,
1270
+ extSemanticPrompts,
479
1271
  ]
package/src/submit.ts CHANGED
@@ -84,19 +84,19 @@ ${JSON.stringify(data, null, 2)}
84
84
  const bodyFile = join(tmpdir(), `terminfo-submit-${Date.now()}.md`)
85
85
  try {
86
86
  writeFileSync(bodyFile, body)
87
- const result = execFileSync("gh", [
88
- "issue", "create",
89
- "--repo", REPO,
90
- "--title", title,
91
- "--body-file", bodyFile,
92
- ], { encoding: "utf-8", timeout: 30000 })
87
+ const result = execFileSync("gh", ["issue", "create", "--repo", REPO, "--title", title, "--body-file", bodyFile], {
88
+ encoding: "utf-8",
89
+ timeout: 30000,
90
+ })
93
91
  return result.trim()
94
92
  } catch (err) {
95
93
  console.error(` \x1b[31mFailed to create issue\x1b[0m`)
96
94
  console.error(` ${err instanceof Error ? err.message : String(err)}`)
97
95
  return null
98
96
  } finally {
99
- try { unlinkSync(bodyFile) } catch {}
97
+ try {
98
+ unlinkSync(bodyFile)
99
+ } catch {}
100
100
  }
101
101
  }
102
102
 
@@ -112,15 +112,26 @@ function hasGhCli(): boolean {
112
112
  function checkDuplicate(terminal: string, version: string, os: string): string | null {
113
113
  try {
114
114
  const search = `[census] ${terminal}${version ? ` ${version}` : ""} on ${os}`
115
- const result = execFileSync("gh", [
116
- "issue", "list",
117
- "--repo", REPO,
118
- "--search", search,
119
- "--state", "all",
120
- "--limit", "1",
121
- "--json", "url,title",
122
- "--jq", ".[0] | .title + \" \" + .url",
123
- ], { encoding: "utf-8", timeout: 10000 })
115
+ const result = execFileSync(
116
+ "gh",
117
+ [
118
+ "issue",
119
+ "list",
120
+ "--repo",
121
+ REPO,
122
+ "--search",
123
+ search,
124
+ "--state",
125
+ "all",
126
+ "--limit",
127
+ "1",
128
+ "--json",
129
+ "url,title",
130
+ "--jq",
131
+ '.[0] | .title + " " + .url',
132
+ ],
133
+ { encoding: "utf-8", timeout: 10000 },
134
+ )
124
135
  return result.trim() || null
125
136
  } catch {
126
137
  return null
package/src/tty.ts CHANGED
@@ -95,7 +95,10 @@ export async function queryMode(modeNumber: number): Promise<"set" | "reset" | "
95
95
  */
96
96
  export async function drainStdin(ms = 300): Promise<void> {
97
97
  return new Promise((resolve) => {
98
- if (!process.stdin.readable) { resolve(); return }
98
+ if (!process.stdin.readable) {
99
+ resolve()
100
+ return
101
+ }
99
102
  process.stdin.resume()
100
103
  let timer = setTimeout(done, ms)
101
104
  function onData() {