@silvery/term 0.3.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.
Files changed (92) hide show
  1. package/package.json +54 -0
  2. package/src/adapters/canvas-adapter.ts +356 -0
  3. package/src/adapters/dom-adapter.ts +452 -0
  4. package/src/adapters/flexily-zero-adapter.ts +368 -0
  5. package/src/adapters/terminal-adapter.ts +305 -0
  6. package/src/adapters/yoga-adapter.ts +370 -0
  7. package/src/ansi/ansi.ts +251 -0
  8. package/src/ansi/constants.ts +76 -0
  9. package/src/ansi/detection.ts +441 -0
  10. package/src/ansi/hyperlink.ts +38 -0
  11. package/src/ansi/index.ts +201 -0
  12. package/src/ansi/patch-console.ts +159 -0
  13. package/src/ansi/sgr-codes.ts +34 -0
  14. package/src/ansi/storybook.ts +209 -0
  15. package/src/ansi/term.ts +724 -0
  16. package/src/ansi/types.ts +202 -0
  17. package/src/ansi/underline.ts +156 -0
  18. package/src/ansi/utils.ts +65 -0
  19. package/src/ansi-sanitize.ts +509 -0
  20. package/src/app.ts +571 -0
  21. package/src/bound-term.ts +94 -0
  22. package/src/bracketed-paste.ts +75 -0
  23. package/src/browser-renderer.ts +174 -0
  24. package/src/buffer.ts +1984 -0
  25. package/src/clipboard.ts +74 -0
  26. package/src/cursor-query.ts +85 -0
  27. package/src/device-attrs.ts +228 -0
  28. package/src/devtools.ts +123 -0
  29. package/src/dom/index.ts +194 -0
  30. package/src/errors.ts +39 -0
  31. package/src/focus-reporting.ts +48 -0
  32. package/src/hit-registry-core.ts +228 -0
  33. package/src/hit-registry.ts +176 -0
  34. package/src/index.ts +458 -0
  35. package/src/input.ts +119 -0
  36. package/src/inspector.ts +155 -0
  37. package/src/kitty-detect.ts +95 -0
  38. package/src/kitty-manager.ts +160 -0
  39. package/src/layout-engine.ts +296 -0
  40. package/src/layout.ts +26 -0
  41. package/src/measurer.ts +74 -0
  42. package/src/mode-query.ts +106 -0
  43. package/src/mouse-events.ts +419 -0
  44. package/src/mouse.ts +83 -0
  45. package/src/non-tty.ts +223 -0
  46. package/src/osc-markers.ts +32 -0
  47. package/src/osc-palette.ts +169 -0
  48. package/src/output.ts +406 -0
  49. package/src/pane-manager.ts +248 -0
  50. package/src/pipeline/CLAUDE.md +587 -0
  51. package/src/pipeline/content-phase-adapter.ts +976 -0
  52. package/src/pipeline/content-phase.ts +1765 -0
  53. package/src/pipeline/helpers.ts +42 -0
  54. package/src/pipeline/index.ts +416 -0
  55. package/src/pipeline/layout-phase.ts +686 -0
  56. package/src/pipeline/measure-phase.ts +198 -0
  57. package/src/pipeline/measure-stats.ts +21 -0
  58. package/src/pipeline/output-phase.ts +2593 -0
  59. package/src/pipeline/render-box.ts +343 -0
  60. package/src/pipeline/render-helpers.ts +243 -0
  61. package/src/pipeline/render-text.ts +1255 -0
  62. package/src/pipeline/types.ts +161 -0
  63. package/src/pipeline.ts +29 -0
  64. package/src/pixel-size.ts +119 -0
  65. package/src/render-adapter.ts +179 -0
  66. package/src/renderer.ts +1330 -0
  67. package/src/runtime/create-app.tsx +1845 -0
  68. package/src/runtime/create-buffer.ts +18 -0
  69. package/src/runtime/create-runtime.ts +325 -0
  70. package/src/runtime/diff.ts +56 -0
  71. package/src/runtime/event-handlers.ts +254 -0
  72. package/src/runtime/index.ts +119 -0
  73. package/src/runtime/keys.ts +8 -0
  74. package/src/runtime/layout.ts +164 -0
  75. package/src/runtime/run.tsx +318 -0
  76. package/src/runtime/term-provider.ts +399 -0
  77. package/src/runtime/terminal-lifecycle.ts +246 -0
  78. package/src/runtime/tick.ts +219 -0
  79. package/src/runtime/types.ts +210 -0
  80. package/src/scheduler.ts +723 -0
  81. package/src/screenshot.ts +57 -0
  82. package/src/scroll-region.ts +69 -0
  83. package/src/scroll-utils.ts +97 -0
  84. package/src/term-def.ts +267 -0
  85. package/src/terminal-caps.ts +5 -0
  86. package/src/terminal-colors.ts +216 -0
  87. package/src/termtest.ts +224 -0
  88. package/src/text-sizing.ts +109 -0
  89. package/src/toolbelt/index.ts +72 -0
  90. package/src/unicode.ts +1763 -0
  91. package/src/xterm/index.ts +491 -0
  92. package/src/xterm/xterm-provider.ts +204 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Console patching with subscribable store for useSyncExternalStore.
3
+ */
4
+
5
+ import type { ConsoleEntry, ConsoleMethod } from "./types"
6
+
7
+ /**
8
+ * Aggregate counts of console output by severity.
9
+ */
10
+ export interface ConsoleStats {
11
+ total: number
12
+ errors: number
13
+ warnings: number
14
+ }
15
+
16
+ /**
17
+ * A patched console that intercepts methods and accumulates entries.
18
+ * Compatible with React's useSyncExternalStore.
19
+ */
20
+ export interface PatchedConsole extends Disposable {
21
+ /** Read current entries (for useSyncExternalStore). Empty when capture=false. */
22
+ getSnapshot(): readonly ConsoleEntry[]
23
+
24
+ /** Get aggregate counts (total, errors, warnings). Works in all modes. */
25
+ getStats(): ConsoleStats
26
+
27
+ /** Subscribe to changes - called when new entry arrives. Returns unsubscribe function. */
28
+ subscribe(onStoreChange: () => void): () => void
29
+
30
+ dispose(): void
31
+ [Symbol.dispose](): void
32
+ }
33
+
34
+ const METHODS: ConsoleMethod[] = ["log", "info", "warn", "error", "debug"]
35
+
36
+ const STDERR_METHODS = new Set<ConsoleMethod>(["error", "warn"])
37
+
38
+ export interface PatchConsoleOptions {
39
+ /**
40
+ * Suppress original console output when true.
41
+ * Use in TUI mode where you want console output only in a component.
42
+ */
43
+ suppress?: boolean
44
+
45
+ /**
46
+ * Store full entries in memory (default: true).
47
+ * Set to false for count-only mode — getSnapshot() returns empty array,
48
+ * but getStats() still tracks counts. Avoids unbounded memory growth.
49
+ */
50
+ capture?: boolean
51
+ }
52
+
53
+ /**
54
+ * Patch console methods to intercept and track output.
55
+ * Returns a disposable that restores original methods.
56
+ *
57
+ * @param console - The console object to patch
58
+ * @param options - Configuration options
59
+ * @param options.suppress - If true, don't call original methods (for TUI mode)
60
+ * @param options.capture - If false, only count entries (no memory storage)
61
+ */
62
+ export function patchConsole(console: Console, options?: PatchConsoleOptions): PatchedConsole {
63
+ const suppress = options?.suppress ?? false
64
+ const capture = options?.capture ?? true
65
+
66
+ // Entry storage (only when capture=true)
67
+ const entries: ConsoleEntry[] = []
68
+ // Snapshot must be a new reference on each change for useSyncExternalStore
69
+ // (React uses Object.is to detect changes — same reference = no re-render)
70
+ let snapshot: readonly ConsoleEntry[] = entries
71
+ const EMPTY: readonly ConsoleEntry[] = Object.freeze([])
72
+
73
+ // Stats (always tracked)
74
+ const stats: ConsoleStats = { total: 0, errors: 0, warnings: 0 }
75
+
76
+ const subscribers = new Set<() => void>()
77
+
78
+ // Save original methods
79
+ const originals = new Map<ConsoleMethod, Console[ConsoleMethod]>()
80
+ for (const method of METHODS) {
81
+ originals.set(method, console[method].bind(console))
82
+ }
83
+
84
+ // Batch subscriber notifications to prevent synchronous feedback loops.
85
+ // Without batching, console.debug() during a React render triggers
86
+ // useSyncExternalStore → re-render → more console output → infinite loop.
87
+ let notifyPending = false
88
+
89
+ function scheduleNotify() {
90
+ if (notifyPending) return
91
+ notifyPending = true
92
+ queueMicrotask(() => {
93
+ notifyPending = false
94
+ subscribers.forEach((subscriber) => subscriber())
95
+ })
96
+ }
97
+
98
+ // Replace with interceptors
99
+ for (const method of METHODS) {
100
+ const original = originals.get(method)!
101
+ console[method] = (...args: unknown[]) => {
102
+ // Update stats
103
+ stats.total++
104
+ if (method === "error") stats.errors++
105
+ else if (method === "warn") stats.warnings++
106
+
107
+ // Store entry if capturing
108
+ if (capture) {
109
+ const entry: ConsoleEntry = {
110
+ method,
111
+ args,
112
+ stream: STDERR_METHODS.has(method) ? "stderr" : "stdout",
113
+ }
114
+ entries.push(entry)
115
+ snapshot = entries.slice()
116
+ }
117
+
118
+ // Call original unless suppressed (TUI mode)
119
+ if (!suppress) {
120
+ original(...args)
121
+ }
122
+
123
+ // Notify subscribers (batched via microtask)
124
+ scheduleNotify()
125
+ }
126
+ }
127
+
128
+ function restore() {
129
+ for (const method of METHODS) {
130
+ console[method] = originals.get(method)!
131
+ }
132
+ }
133
+
134
+ return {
135
+ getSnapshot(): readonly ConsoleEntry[] {
136
+ return capture ? snapshot : EMPTY
137
+ },
138
+
139
+ getStats(): ConsoleStats {
140
+ return { ...stats }
141
+ },
142
+
143
+ subscribe(onStoreChange: () => void): () => void {
144
+ subscribers.add(onStoreChange)
145
+ return () => {
146
+ subscribers.delete(onStoreChange)
147
+ }
148
+ },
149
+
150
+ dispose() {
151
+ restore()
152
+ subscribers.clear()
153
+ },
154
+
155
+ [Symbol.dispose]() {
156
+ this.dispose()
157
+ },
158
+ }
159
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * SGR (Select Graphic Rendition) color code helpers.
3
+ *
4
+ * Shared by buffer.ts (styleToAnsiCodes) and output-phase.ts (styleTransition).
5
+ * Emits the shortest possible SGR code string for a given color.
6
+ */
7
+
8
+ /**
9
+ * Emit the shortest SGR code string for a foreground color.
10
+ * - Basic 0-7: 4-bit code (30+N)
11
+ * - Extended 8-255: 256-color (38;5;N)
12
+ * - RGB: true color (38;2;R;G;B)
13
+ */
14
+ export function fgColorCode(color: number | { r: number; g: number; b: number }): string {
15
+ if (typeof color === "number") {
16
+ if (color >= 0 && color <= 7) return `${30 + color}`
17
+ return `38;5;${color}`
18
+ }
19
+ return `38;2;${color.r};${color.g};${color.b}`
20
+ }
21
+
22
+ /**
23
+ * Emit the shortest SGR code string for a background color.
24
+ * - Basic 0-7: 4-bit code (40+N)
25
+ * - Extended 8-255: 256-color (48;5;N)
26
+ * - RGB: true color (48;2;R;G;B)
27
+ */
28
+ export function bgColorCode(color: number | { r: number; g: number; b: number }): string {
29
+ if (typeof color === "number") {
30
+ if (color >= 0 && color <= 7) return `${40 + color}`
31
+ return `48;5;${color}`
32
+ }
33
+ return `48;2;${color.r};${color.g};${color.b}`
34
+ }
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * chalk-x Storybook - Visual Feature Catalog
4
+ *
5
+ * Renders all chalk-x features for visual inspection.
6
+ * Run: bun vendor/silvery/packages/ansi/src/storybook.ts
7
+ *
8
+ * NOTE: Extended underline styles only render in modern terminals
9
+ * (Ghostty, Kitty, WezTerm, iTerm2). Other terminals will show
10
+ * regular underlines as fallback.
11
+ */
12
+
13
+ import {
14
+ createTerm,
15
+ curlyUnderline,
16
+ dottedUnderline,
17
+ dashedUnderline,
18
+ doubleUnderline,
19
+ underlineColor,
20
+ styledUnderline,
21
+ hyperlink,
22
+ detectExtendedUnderline,
23
+ displayLength,
24
+ stripAnsi,
25
+ } from "./index"
26
+
27
+ using term = createTerm({ color: "truecolor" })
28
+
29
+ const divider = "═".repeat(60)
30
+ const subDivider = "─".repeat(40)
31
+
32
+ function section(title: string): void {
33
+ console.log()
34
+ console.log(term.bold.cyan(divider))
35
+ console.log(term.bold.cyan(` ${title}`))
36
+ console.log(term.bold.cyan(divider))
37
+ console.log()
38
+ }
39
+
40
+ function subsection(title: string): void {
41
+ console.log(term.dim(subDivider))
42
+ console.log(term.bold(title))
43
+ console.log()
44
+ }
45
+
46
+ // =============================================================================
47
+ // Terminal Info
48
+ // =============================================================================
49
+
50
+ section("Terminal Information")
51
+
52
+ console.log(` TERM: ${process.env.TERM ?? "(not set)"}`)
53
+ console.log(` TERM_PROGRAM: ${process.env.TERM_PROGRAM ?? "(not set)"}`)
54
+ console.log(
55
+ ` Extended underline support: ${detectExtendedUnderline() ? term.green("Yes") : term.red("No (fallback mode)")}`,
56
+ )
57
+ console.log()
58
+ console.log(term.dim(" Note: This storybook forces extended mode for display."))
59
+ console.log(term.dim(" Your terminal may show fallbacks if not supported."))
60
+
61
+ // =============================================================================
62
+ // Extended Underline Styles
63
+ // =============================================================================
64
+
65
+ section("Extended Underline Styles")
66
+
67
+ subsection("Comparison with standard underline")
68
+
69
+ console.log(` Standard: ${term.underline("regular underline")}`)
70
+ console.log(` Double: ${doubleUnderline("double underline")}`)
71
+ console.log(` Curly: ${curlyUnderline("curly/wavy underline")}`)
72
+ console.log(` Dotted: ${dottedUnderline("dotted underline")}`)
73
+ console.log(` Dashed: ${dashedUnderline("dashed underline")}`)
74
+ console.log()
75
+
76
+ subsection("Side by side")
77
+
78
+ console.log(
79
+ ` ${term.underline("standard")} | ${doubleUnderline("double")} | ${curlyUnderline("curly")} | ${dottedUnderline("dotted")} | ${dashedUnderline("dashed")}`,
80
+ )
81
+
82
+ // =============================================================================
83
+ // Underline Color
84
+ // =============================================================================
85
+
86
+ section("Independent Underline Color")
87
+
88
+ subsection("Basic colors")
89
+
90
+ console.log(` Red: ${underlineColor(255, 0, 0, "error message")}`)
91
+ console.log(` Orange: ${underlineColor(255, 165, 0, "warning message")}`)
92
+ console.log(` Yellow: ${underlineColor(255, 255, 0, "caution message")}`)
93
+ console.log(` Green: ${underlineColor(0, 255, 0, "success message")}`)
94
+ console.log(` Blue: ${underlineColor(0, 128, 255, "info message")}`)
95
+ console.log(` Purple: ${underlineColor(128, 0, 255, "special message")}`)
96
+ console.log()
97
+
98
+ subsection("With text color (independent)")
99
+
100
+ console.log(` ${term.red(underlineColor(0, 255, 0, "Red text, green underline"))}`)
101
+ console.log(` ${term.blue(underlineColor(255, 165, 0, "Blue text, orange underline"))}`)
102
+ console.log(` ${term.white(underlineColor(255, 0, 0, "White text, red underline"))}`)
103
+
104
+ // =============================================================================
105
+ // Combined Style + Color
106
+ // =============================================================================
107
+
108
+ section("Combined Style + Color")
109
+
110
+ subsection("Curly with colors (like spell-check)")
111
+
112
+ console.log(` Spelling error: ${styledUnderline("curly", [255, 0, 0], "teh")} → the`)
113
+ console.log(` Grammar issue: ${styledUnderline("curly", [0, 128, 255], "alot")} → a lot`)
114
+ console.log(` Style warning: ${styledUnderline("curly", [0, 180, 0], "very unique")} → unique`)
115
+ console.log()
116
+
117
+ subsection("Dotted with colors (embedded content)")
118
+
119
+ console.log(` From inbox: ${styledUnderline("dotted", [100, 149, 237], "Review docs")}`)
120
+ console.log(` From projects: ${styledUnderline("dotted", [147, 112, 219], "Sprint planning")}`)
121
+ console.log()
122
+
123
+ subsection("Dashed with colors (drafts/tentative)")
124
+
125
+ console.log(` Draft: ${styledUnderline("dashed", [128, 128, 128], "WIP: New feature")}`)
126
+ console.log(` Tentative: ${styledUnderline("dashed", [169, 169, 169], "Maybe: Refactor auth")}`)
127
+
128
+ // =============================================================================
129
+ // Hyperlinks
130
+ // =============================================================================
131
+
132
+ section("OSC 8 Hyperlinks")
133
+
134
+ subsection("Basic hyperlinks (click to open in terminal)")
135
+
136
+ console.log(` Website: ${hyperlink("Google", "https://google.com")}`)
137
+ console.log(` File: ${hyperlink("README.md", "file:///Users/beorn/README.md")}`)
138
+ console.log(` Custom: ${hyperlink("Open in VSCode", "vscode://file/path/to/file")}`)
139
+ console.log()
140
+
141
+ subsection("Styled hyperlinks")
142
+
143
+ console.log(` Underlined: ${term.underline(hyperlink("Underlined link", "https://example.com"))}`)
144
+ console.log(` Colored: ${term.blue(hyperlink("Blue link", "https://example.com"))}`)
145
+ console.log(` Bold: ${term.bold(hyperlink("Bold link", "https://example.com"))}`)
146
+ console.log(` Combined: ${term.bold.blue.underline(hyperlink("Styled link", "https://example.com"))}`)
147
+
148
+ // =============================================================================
149
+ // ANSI Utilities
150
+ // =============================================================================
151
+
152
+ section("ANSI Utilities")
153
+
154
+ subsection("stripAnsi() - Remove escape codes")
155
+
156
+ const styled = curlyUnderline("Hello ") + term.bold.red("World")
157
+ console.log(` Styled: "${styled}"`)
158
+ console.log(` Stripped: "${stripAnsi(styled)}"`)
159
+ console.log()
160
+
161
+ subsection("displayLength() - Visual character count")
162
+
163
+ const coloredText = term.red("Red") + " and " + term.blue("Blue")
164
+ console.log(` Text: "${coloredText}"`)
165
+ console.log(` string.length: ${coloredText.length}`)
166
+ console.log(` displayLength: ${displayLength(coloredText)}`)
167
+
168
+ // =============================================================================
169
+ // Use Cases
170
+ // =============================================================================
171
+
172
+ section("Practical Use Cases")
173
+
174
+ subsection("IDE-style error highlighting")
175
+
176
+ console.log(` const ${styledUnderline("curly", [255, 0, 0], "x")} = undefined;`)
177
+ console.log(` ${term.red("^")} ${term.dim("Variable 'x' is declared but never used")}`)
178
+ console.log()
179
+
180
+ subsection("Task manager styling")
181
+
182
+ console.log(` ${term.green("✓")} ${term.dim.strikethrough("Completed task")}`)
183
+ console.log(` ${term.yellow("◐")} ${styledUnderline("curly", [255, 180, 0], "Due today: Submit report")}`)
184
+ console.log(` ${term.red("○")} ${styledUnderline("curly", [255, 80, 80], "Overdue: Fix critical bug")}`)
185
+ console.log(` ${term.blue("○")} ${dottedUnderline("Embedded from [[Projects]]")}`)
186
+ console.log(` ${term.gray("○")} ${dashedUnderline("Draft: New feature idea")}`)
187
+ console.log()
188
+
189
+ subsection("Documentation with clickable links")
190
+
191
+ console.log(` See ${hyperlink("API Reference", "https://docs.example.com/api")} for details.`)
192
+ console.log(` Implementation in ${hyperlink("src/index.ts", "file:///path/src/index.ts")}`)
193
+
194
+ // =============================================================================
195
+ // Summary
196
+ // =============================================================================
197
+
198
+ section("Summary")
199
+
200
+ console.log(" chalk-x provides:")
201
+ console.log(" • Extended underline styles (curly, dotted, dashed, double)")
202
+ console.log(" • Independent underline color (RGB)")
203
+ console.log(" • Combined style + color")
204
+ console.log(" • OSC 8 hyperlinks")
205
+ console.log(" • ANSI utilities (stripAnsi, displayLength)")
206
+ console.log(" • Graceful fallback for unsupported terminals")
207
+ console.log()
208
+ console.log(term.dim(" All features degrade gracefully in basic terminals."))
209
+ console.log()