terminfo.dev 0.6.0 → 1.0.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": "0.6.0",
3
+ "version": "1.0.0",
4
4
  "description": "Test your terminal's feature support and contribute to terminfo.dev",
5
5
  "keywords": [
6
6
  "ansi",
@@ -32,6 +32,9 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
+ "dependencies": {
36
+ "silvery": "^0.4.3"
37
+ },
35
38
  "engines": {
36
39
  "node": ">=23.6.0"
37
40
  }
package/src/index.ts CHANGED
@@ -13,11 +13,37 @@
13
13
  * ```
14
14
  */
15
15
 
16
+ import { readFileSync } from "node:fs"
17
+ import { dirname, join } from "node:path"
18
+ import { fileURLToPath } from "node:url"
16
19
  import { detectTerminal } from "./detect.ts"
17
20
  import { ALL_PROBES } from "./probes/index.ts"
18
21
  import { withRawMode } from "./tty.ts"
19
22
  import { submitResults } from "./submit.ts"
20
23
 
24
+ const __dirname = dirname(fileURLToPath(import.meta.url))
25
+
26
+ /** Load feature slugs from features.json for OSC 8 hyperlinks */
27
+ function loadFeatureSlugs(): Record<string, string> {
28
+ // Try repo-local path first, then npm-installed path
29
+ const candidates = [
30
+ join(__dirname, "..", "..", "features.json"), // repo: cli/src/ -> features.json
31
+ join(__dirname, "..", "..", "..", "features.json"), // npm: node_modules/terminfo.dev/src/ -> features.json
32
+ ]
33
+ for (const path of candidates) {
34
+ try {
35
+ const raw = JSON.parse(readFileSync(path, "utf-8"))
36
+ delete raw.$comment
37
+ const slugs: Record<string, string> = {}
38
+ for (const [id, entry] of Object.entries(raw) as [string, any][]) {
39
+ slugs[id] = entry.slug ?? id.replaceAll(".", "-")
40
+ }
41
+ return slugs
42
+ } catch {}
43
+ }
44
+ return {} // fallback: featureSlug() will use id.replaceAll(".", "-")
45
+ }
46
+
21
47
  interface ResultEntry {
22
48
  terminal: string
23
49
  terminalVersion: string
@@ -38,19 +64,6 @@ async function main() {
38
64
  // Detect terminal
39
65
  const terminal = detectTerminal()
40
66
 
41
- if (!jsonMode) {
42
- console.log(`\x1b[1mterminfo.dev\x1b[0m — can your terminal do that?\n`)
43
- console.log(` Terminal: \x1b[1m${terminal.name}\x1b[0m${terminal.version ? ` ${terminal.version}` : ""}`)
44
- console.log(` Platform: ${terminal.os}${terminal.osVersion ? ` ${terminal.osVersion}` : ""}`)
45
- console.log(` Probes: ${ALL_PROBES.length} features across ${new Set(ALL_PROBES.map(p => p.id.split(".")[0])).size} categories`)
46
- console.log(` Website: https://terminfo.dev`)
47
- console.log(``)
48
- console.log(`\x1b[2mResults are compared against ${ALL_PROBES.length} terminal features from the`)
49
- console.log(`ECMA-48, VT100/VT510, xterm, and Kitty specifications.`)
50
- console.log(`Run with --submit to contribute your results to the database.\x1b[0m\n`)
51
- console.log(`Running probes...`)
52
- }
53
-
54
67
  const results: Record<string, boolean> = {}
55
68
  const notes: Record<string, string> = {}
56
69
  const responses: Record<string, string> = {}
@@ -103,10 +116,8 @@ async function main() {
103
116
  return
104
117
  }
105
118
 
106
- // Display results
107
- console.log(`\n\x1b[1mResults: ${passed}/${total} (${pct}%)\x1b[0m\n`)
108
-
109
- // Show categories
119
+ // Build category data for report
120
+ const slugs = loadFeatureSlugs()
110
121
  const categories = new Map<string, Array<{ id: string; name: string; pass: boolean; note?: string }>>()
111
122
  for (const probe of ALL_PROBES) {
112
123
  const cat = probe.id.split(".")[0]!
@@ -119,16 +130,22 @@ async function main() {
119
130
  })
120
131
  }
121
132
 
122
- for (const [cat, probes] of categories) {
123
- const catPassed = probes.filter((p) => p.pass).length
124
- const color = catPassed === probes.length ? "\x1b[32m" : catPassed > 0 ? "\x1b[33m" : "\x1b[31m"
125
- console.log(`${color}${cat}\x1b[0m (${catPassed}/${probes.length})`)
126
- for (const p of probes) {
127
- const icon = p.pass ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"
128
- const note = p.note ? ` \x1b[2m— ${p.note}\x1b[0m` : ""
129
- console.log(` ${icon} ${p.name}${note}`)
130
- }
131
- }
133
+ // Render with silvery
134
+ const { renderReport } = await import("./report.tsx")
135
+ const output = await renderReport({
136
+ terminal: terminal.name,
137
+ terminalVersion: terminal.version,
138
+ os: terminal.os,
139
+ osVersion: terminal.osVersion,
140
+ probeCount: total,
141
+ categoryCount: new Set(ALL_PROBES.map(p => p.id.split(".")[0])).size,
142
+ passed,
143
+ total,
144
+ categories,
145
+ slugs,
146
+ submitMode,
147
+ })
148
+ console.log(output)
132
149
 
133
150
  if (submitMode) {
134
151
  console.log(`\nSubmitting results to terminfo.dev...`)
@@ -136,9 +153,6 @@ async function main() {
136
153
  if (url) {
137
154
  console.log(`\x1b[32m✓ Issue created:\x1b[0m ${url}`)
138
155
  }
139
- } else {
140
- console.log(`\n\x1b[2mSubmit results to terminfo.dev: npx terminfo --submit\x1b[0m`)
141
- console.log(`\x1b[2mJSON output: npx terminfo --json\x1b[0m`)
142
156
  }
143
157
  }
144
158
 
package/src/report.tsx ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Silvery-rendered CLI report for terminfo probe results.
3
+ */
4
+ import React from "react"
5
+ import { Box, Text } from "silvery"
6
+ import { renderString } from "silvery"
7
+
8
+ interface ProbeResult {
9
+ id: string
10
+ name: string
11
+ pass: boolean
12
+ note?: string
13
+ }
14
+
15
+ interface ReportProps {
16
+ terminal: string
17
+ terminalVersion: string
18
+ os: string
19
+ osVersion: string
20
+ probeCount: number
21
+ categoryCount: number
22
+ passed: number
23
+ total: number
24
+ categories: Map<string, ProbeResult[]>
25
+ }
26
+
27
+ /** OSC 8 hyperlink wrapper */
28
+ function osc8(url: string, text: string): string {
29
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
30
+ }
31
+
32
+ function featureUrl(id: string, slug: string): string {
33
+ const cat = id.split(".")[0]!
34
+ return `https://terminfo.dev/${cat}/${slug}`
35
+ }
36
+
37
+ function Header({ terminal, terminalVersion, os, osVersion, probeCount, categoryCount, passed, total }: ReportProps) {
38
+ const pct = Math.round((passed / total) * 100)
39
+ return (
40
+ <Box flexDirection="column" marginBottom={1}>
41
+ <Text bold>{osc8("https://terminfo.dev", "terminfo.dev")}</Text>
42
+ <Text dimColor>Can your terminal do that?</Text>
43
+ <Text> </Text>
44
+ <Box flexDirection="row" gap={2}>
45
+ <Box flexDirection="column" width={12}>
46
+ <Text dimColor>Terminal</Text>
47
+ <Text dimColor>Platform</Text>
48
+ <Text dimColor>Probes</Text>
49
+ <Text dimColor>Score</Text>
50
+ </Box>
51
+ <Box flexDirection="column">
52
+ <Text bold>{terminal}{terminalVersion ? ` ${terminalVersion}` : ""}</Text>
53
+ <Text>{os} {osVersion}</Text>
54
+ <Text>{probeCount} features, {categoryCount} categories</Text>
55
+ <Text bold color={pct === 100 ? "green" : pct >= 90 ? "yellow" : "red"}>{passed}/{total} ({pct}%)</Text>
56
+ </Box>
57
+ </Box>
58
+ </Box>
59
+ )
60
+ }
61
+
62
+ function CategorySection({ name, probes, slugs }: { name: string; probes: ProbeResult[]; slugs: Record<string, string> }) {
63
+ const catPassed = probes.filter(p => p.pass).length
64
+ const allPassed = catPassed === probes.length
65
+ const catUrl = `https://terminfo.dev/${name}`
66
+
67
+ return (
68
+ <Box flexDirection="column">
69
+ <Text color={allPassed ? "green" : catPassed > 0 ? "yellow" : "red"}>
70
+ {osc8(catUrl, name)} ({catPassed}/{probes.length})
71
+ </Text>
72
+ {probes.map(p => {
73
+ const slug = slugs[p.id] ?? p.id.replaceAll(".", "-")
74
+ const url = featureUrl(p.id, slug)
75
+ const icon = p.pass ? "✓" : "✗"
76
+ return (
77
+ <Box key={p.id} flexDirection="row" paddingLeft={2}>
78
+ <Text color={p.pass ? "green" : "red"}>{icon} </Text>
79
+ <Text>{osc8(url, p.name)}</Text>
80
+ {p.note && <Text dimColor> — {p.note}</Text>}
81
+ </Box>
82
+ )
83
+ })}
84
+ </Box>
85
+ )
86
+ }
87
+
88
+ function Footer({ submitMode }: { submitMode: boolean }) {
89
+ if (submitMode) return null
90
+ return (
91
+ <Box flexDirection="column" marginTop={1}>
92
+ <Text dimColor>Submit: npx terminfo.dev --submit</Text>
93
+ <Text dimColor>JSON: npx terminfo.dev --json</Text>
94
+ </Box>
95
+ )
96
+ }
97
+
98
+ function Report(props: ReportProps & { slugs: Record<string, string>; submitMode: boolean }) {
99
+ return (
100
+ <Box flexDirection="column">
101
+ <Header {...props} />
102
+ {[...props.categories.entries()].map(([name, probes]) => (
103
+ <CategorySection key={name} name={name} probes={probes} slugs={props.slugs} />
104
+ ))}
105
+ <Footer submitMode={props.submitMode} />
106
+ </Box>
107
+ )
108
+ }
109
+
110
+ export async function renderReport(props: ReportProps & { slugs: Record<string, string>; submitMode: boolean }): Promise<string> {
111
+ const width = process.stdout.columns ?? 80
112
+ return renderString(<Report {...props} />, { width })
113
+ }