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 +1 -1
- package/src/detect.ts +20 -10
- package/src/index.ts +1 -1
- package/src/probes/index.ts +906 -114
- package/src/submit.ts +27 -16
- package/src/tty.ts +4 -1
package/package.json
CHANGED
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) {
|
|
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
|
-
})
|
|
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(
|
|
126
|
-
"
|
|
127
|
-
`${appPath}/Contents/Info.plist
|
|
128
|
-
|
|
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":
|
|
139
|
-
|
|
140
|
-
case "
|
|
141
|
-
|
|
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
package/src/probes/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
//
|
|
412
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
413
|
+
// ── Cursor movement probes ──
|
|
414
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
151
415
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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;
|
|
157
|
-
process.stdout.write("\
|
|
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
|
|
484
|
+
if (!pos) return { pass: false, note: "No cursor response" }
|
|
162
485
|
return {
|
|
163
|
-
pass: pos[0] === 3 && pos[1] ===
|
|
164
|
-
note: pos[0] === 3 && pos[1] ===
|
|
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
|
|
190
|
-
id: "erase.
|
|
191
|
-
name: "Erase
|
|
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[
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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;
|
|
211
|
-
process.stdout.write("\x1b[
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
222
|
-
id: "
|
|
223
|
-
name: "
|
|
606
|
+
const eraseScreenScrollback: Probe = {
|
|
607
|
+
id: "erase.screen.scrollback",
|
|
608
|
+
name: "Erase scrollback (ED 3)",
|
|
224
609
|
async run() {
|
|
225
|
-
process.stdout.write("\x1b[
|
|
226
|
-
process.stdout.write("\
|
|
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] ===
|
|
231
|
-
note: pos[1] ===
|
|
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
|
|
237
|
-
id: "
|
|
238
|
-
name: "
|
|
621
|
+
const eraseCharacter: Probe = {
|
|
622
|
+
id: "erase.character",
|
|
623
|
+
name: "Erase characters (ECH)",
|
|
239
624
|
async run() {
|
|
240
|
-
process.stdout.write("\x1b[1;
|
|
241
|
-
process.stdout.write("
|
|
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] ===
|
|
246
|
-
note: pos[1] ===
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1020
|
+
name: "Clipboard access (OSC 52)",
|
|
356
1021
|
async run() {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 {
|
|
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(
|
|
116
|
-
"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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) {
|
|
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() {
|