terminfo.dev 1.4.0 → 1.6.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.4.0",
3
+ "version": "1.6.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
@@ -52,15 +52,19 @@ export function detectTerminal(): TerminalInfo {
52
52
  let name = "unknown"
53
53
  let version = ""
54
54
 
55
- // Check specific env vars first
56
- for (const { env, name: n } of ENV_DETECTORS) {
57
- if (process.env[env]) {
58
- name = n
59
- break
55
+ // On macOS, __CFBundleIdentifier is the most reliable — set by the actual running app
56
+ const bundleId = process.env.__CFBundleIdentifier
57
+ if (bundleId && os === "macos") {
58
+ for (const [termName, bid] of Object.entries(BUNDLE_IDS)) {
59
+ if (bundleId === bid) { name = termName; break }
60
+ }
61
+ // If bundle ID didn't match known terminals, use it as-is
62
+ if (name === "unknown" && bundleId) {
63
+ name = bundleId.split(".").pop() ?? bundleId
60
64
  }
61
65
  }
62
66
 
63
- // Check $TERM_PROGRAM
67
+ // Check $TERM_PROGRAM (more reliable than env var detectors for cross-app scenarios)
64
68
  if (name === "unknown") {
65
69
  const termProgram = process.env.TERM_PROGRAM
66
70
  if (termProgram) {
@@ -69,6 +73,16 @@ export function detectTerminal(): TerminalInfo {
69
73
  }
70
74
  }
71
75
 
76
+ // Check specific env vars (may be inherited from parent, so lower priority)
77
+ if (name === "unknown") {
78
+ for (const { env, name: n } of ENV_DETECTORS) {
79
+ if (process.env[env]) {
80
+ name = n
81
+ break
82
+ }
83
+ }
84
+ }
85
+
72
86
  // Check $TERMINAL_EMULATOR (Linux)
73
87
  if (name === "unknown") {
74
88
  const termEmu = process.env.TERMINAL_EMULATOR
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ import { dirname, join } from "node:path"
18
18
  import { fileURLToPath } from "node:url"
19
19
  import { detectTerminal } from "./detect.ts"
20
20
  import { ALL_PROBES } from "./probes/index.ts"
21
- import { withRawMode } from "./tty.ts"
21
+ import { withRawMode, drainStdin } from "./tty.ts"
22
22
  import { submitResults } from "./submit.ts"
23
23
 
24
24
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -76,21 +76,11 @@ async function runProbes(): Promise<ProbeResults> {
76
76
  notes[probe.id] = `error: ${err instanceof Error ? err.message : String(err)}`
77
77
  }
78
78
  }
79
+ // Drain all pending responses before exiting alt screen (still in raw mode)
80
+ await drainStdin(500)
79
81
  process.stdout.write("\x1b[?1049l")
80
82
  })
81
83
 
82
- // Drain any late-arriving escape sequence responses before output
83
- await new Promise<void>((resolve) => {
84
- process.stdin.resume()
85
- const timer = setTimeout(() => {
86
- process.stdin.pause()
87
- process.stdin.removeAllListeners("readable")
88
- resolve()
89
- }, 100)
90
- process.stdin.on("readable", () => { while (process.stdin.read() !== null) {} })
91
- timer.unref()
92
- })
93
-
94
84
  return { terminal, results, notes, responses, passed, total: ALL_PROBES.length }
95
85
  }
96
86
 
@@ -183,21 +173,45 @@ program
183
173
  program
184
174
  .command("submit")
185
175
  .description("Run all probes and submit results to terminfo.dev via GitHub issue")
186
- .action(async () => {
176
+ .option("--terminal-name <name>", "Override detected terminal name")
177
+ .option("--terminal-version <version>", "Override detected terminal version")
178
+ .action(async (opts) => {
179
+ // Confirm details BEFORE probes (stdin is still clean)
180
+ const terminal = detectTerminal()
181
+ const name = opts.terminalName ?? terminal.name
182
+ const version = opts.terminalVersion ?? terminal.version
183
+
184
+ printHeader(terminal)
185
+ console.log(``)
186
+ console.log(` Will submit results for \x1b[1m${name}${version ? ` ${version}` : ""}\x1b[0m on ${terminal.os}`)
187
+ if (!version) {
188
+ console.log(` \x1b[33m⚠ No version detected. Use --terminal-version to specify.\x1b[0m`)
189
+ }
190
+
191
+ const { createInterface } = await import("node:readline")
192
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
193
+ const proceed = await new Promise<string>((resolve) => {
194
+ rl.question(`\n Press Enter to run probes and submit (or Ctrl+C to cancel) `, (answer) => {
195
+ rl.close()
196
+ resolve(answer)
197
+ })
198
+ })
199
+
200
+ // Now run probes (stdin goes to raw mode, no conflict)
187
201
  const data = await runProbes()
188
202
  printResults(data)
189
203
 
190
204
  console.log(`\nSubmitting results to terminfo.dev...`)
191
205
  const url = await submitResults({
192
- terminal: data.terminal.name,
193
- terminalVersion: data.terminal.version,
206
+ terminal: name,
207
+ terminalVersion: version,
194
208
  os: data.terminal.os,
195
209
  osVersion: data.terminal.osVersion,
196
210
  results: data.results,
197
211
  notes: data.notes,
198
212
  responses: data.responses,
199
213
  generated: new Date().toISOString(),
200
- cliVersion: "1.3.0",
214
+ cliVersion: "1.6.0",
201
215
  probeCount: ALL_PROBES.length,
202
216
  })
203
217
  if (url) {
package/src/submit.ts CHANGED
@@ -6,7 +6,6 @@ import { writeFileSync, unlinkSync } from "node:fs"
6
6
  import { tmpdir } from "node:os"
7
7
  import { join } from "node:path"
8
8
  import { execFileSync } from "node:child_process"
9
- import { createInterface } from "node:readline"
10
9
 
11
10
  const REPO = "beorn/terminfo.dev"
12
11
 
@@ -23,65 +22,28 @@ interface SubmitData {
23
22
  probeCount?: number
24
23
  }
25
24
 
26
- /** Drain any leftover bytes from stdin (e.g., late-arriving escape sequence responses) */
27
- async function drainStdin(): Promise<void> {
28
- return new Promise((resolve) => {
29
- if (!process.stdin.readable) { resolve(); return }
30
- process.stdin.resume()
31
- const timer = setTimeout(() => {
32
- process.stdin.pause()
33
- process.stdin.removeAllListeners("readable")
34
- resolve()
35
- }, 200)
36
- process.stdin.on("readable", () => {
37
- while (process.stdin.read() !== null) {} // discard
38
- })
39
- timer.unref()
40
- })
41
- }
42
-
43
- async function prompt(question: string, defaultValue?: string): Promise<string> {
44
- await drainStdin()
45
- const rl = createInterface({ input: process.stdin, output: process.stdout })
46
- const suffix = defaultValue ? ` [${defaultValue}]` : ""
47
- return new Promise((resolve) => {
48
- rl.question(`${question}${suffix}: `, (answer) => {
49
- rl.close()
50
- resolve(answer.trim() || defaultValue || "")
51
- })
52
- })
53
- }
54
-
55
25
  export async function submitResults(data: SubmitData): Promise<string | null> {
56
- // Confirm/fill terminal info
57
- console.log(`\n\x1b[1mConfirm submission details:\x1b[0m`)
58
- data.terminal = await prompt(" Terminal name", data.terminal)
59
- data.terminalVersion = await prompt(" Terminal version", data.terminalVersion || undefined)
60
- data.os = await prompt(" Operating system", data.os)
26
+ const passed = Object.values(data.results).filter(Boolean).length
27
+ const total = Object.keys(data.results).length
28
+ const pct = Math.round((passed / total) * 100)
29
+ const ver = data.terminalVersion ? ` ${data.terminalVersion}` : ""
30
+
31
+ console.log(`\n Submitting: \x1b[1m${data.terminal}${ver}\x1b[0m on ${data.os} — ${pct}% (${passed}/${total})`)
61
32
 
62
33
  if (!data.terminalVersion) {
63
- console.log(`\x1b[33m ⚠ No version specifiedresults will be less useful\x1b[0m`)
34
+ console.log(` \x1b[33m⚠ No version detecteduse --terminal-version to specify\x1b[0m`)
64
35
  }
65
36
 
66
37
  // Check for duplicates
67
38
  if (hasGhCli()) {
68
39
  const existing = checkDuplicate(data.terminal, data.terminalVersion, data.os)
69
40
  if (existing) {
70
- console.log(`\n\x1b[33m⚠ A submission already exists for ${data.terminal}${data.terminalVersion ? ` ${data.terminalVersion}` : ""} on ${data.os}:\x1b[0m`)
71
- console.log(` ${existing}`)
72
- const proceed = await prompt(" Submit anyway? (y/N)", "N")
73
- if (proceed.toLowerCase() !== "y") {
74
- console.log(`Skipped.`)
75
- return null
76
- }
41
+ console.log(` \x1b[33m⚠ Similar submission exists: ${existing}\x1b[0m`)
42
+ console.log(` Submitting anyway (different probe version may have new results)`)
77
43
  }
78
44
  }
79
45
 
80
- const passed = Object.values(data.results).filter(Boolean).length
81
- const total = Object.keys(data.results).length
82
- const pct = Math.round((passed / total) * 100)
83
-
84
- const title = `[census] ${data.terminal}${data.terminalVersion ? ` ${data.terminalVersion}` : ""} on ${data.os} — ${pct}% (${passed}/${total})`
46
+ const title = `[census] ${data.terminal}${ver} on ${data.os} — ${pct}% (${passed}/${total})`
85
47
 
86
48
  const body = `## Community Census Result
87
49
 
@@ -114,8 +76,8 @@ ${JSON.stringify(data, null, 2)}
114
76
  if (!hasGhCli()) {
115
77
  const filename = `terminfo-${data.terminal}-${data.os}-${Date.now()}.json`
116
78
  writeFileSync(filename, JSON.stringify(data, null, 2))
117
- console.log(`\n\x1b[33mgh CLI not found. Results saved to ${filename}\x1b[0m`)
118
- console.log(`To submit: https://github.com/${REPO}/issues/new`)
79
+ console.log(`\n \x1b[33mgh CLI not found. Results saved to ${filename}\x1b[0m`)
80
+ console.log(` To submit: https://github.com/${REPO}/issues/new`)
119
81
  return null
120
82
  }
121
83
 
@@ -130,8 +92,8 @@ ${JSON.stringify(data, null, 2)}
130
92
  ], { encoding: "utf-8", timeout: 30000 })
131
93
  return result.trim()
132
94
  } catch (err) {
133
- console.error(`\x1b[31mFailed to create issue\x1b[0m`)
134
- console.error(err instanceof Error ? err.message : String(err))
95
+ console.error(` \x1b[31mFailed to create issue\x1b[0m`)
96
+ console.error(` ${err instanceof Error ? err.message : String(err)}`)
135
97
  return null
136
98
  } finally {
137
99
  try { unlinkSync(bodyFile) } catch {}
@@ -159,8 +121,7 @@ function checkDuplicate(terminal: string, version: string, os: string): string |
159
121
  "--json", "url,title",
160
122
  "--jq", ".[0] | .title + \" \" + .url",
161
123
  ], { encoding: "utf-8", timeout: 10000 })
162
- const trimmed = result.trim()
163
- return trimmed || null
124
+ return result.trim() || null
164
125
  } catch {
165
126
  return null
166
127
  }
package/src/tty.ts CHANGED
@@ -89,6 +89,28 @@ export async function queryMode(modeNumber: number): Promise<"set" | "reset" | "
89
89
  }
90
90
  }
91
91
 
92
+ /**
93
+ * Drain all pending bytes from stdin (late-arriving escape sequence responses).
94
+ * Waits up to `ms` milliseconds for bytes to stop arriving.
95
+ */
96
+ export async function drainStdin(ms = 300): Promise<void> {
97
+ return new Promise((resolve) => {
98
+ if (!process.stdin.readable) { resolve(); return }
99
+ process.stdin.resume()
100
+ let timer = setTimeout(done, ms)
101
+ function onData() {
102
+ while (process.stdin.read() !== null) {} // discard
103
+ clearTimeout(timer)
104
+ timer = setTimeout(done, ms) // reset timer on each new data
105
+ }
106
+ function done() {
107
+ process.stdin.removeListener("readable", onData)
108
+ resolve()
109
+ }
110
+ process.stdin.on("readable", onData)
111
+ })
112
+ }
113
+
92
114
  /**
93
115
  * Run a function with stdin in raw mode.
94
116
  * Restores original mode on exit.