@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,452 @@
1
+ /**
2
+ * DOM Render Adapter
3
+ *
4
+ * Implements the RenderAdapter interface for browser DOM output.
5
+ * Uses a line-based approach: one <div> per row, <span> elements for styled text runs.
6
+ * The layout engine operates in cell units (columns x rows). This adapter
7
+ * converts cell coordinates to pixel coordinates when rendering to the DOM,
8
+ * using charWidth (fontSize * 0.6) and cellHeight (fontSize * lineHeight).
9
+ *
10
+ * Advantages over Canvas:
11
+ * - Native text selection and copying
12
+ * - Screen reader accessibility
13
+ * - Browser font rendering (subpixel antialiasing, ligatures)
14
+ * - CSS integration (theming, hover states)
15
+ * - DevTools inspection
16
+ *
17
+ * Architecture follows xterm.js DOM renderer approach.
18
+ * @see https://github.com/xtermjs/xterm.js/issues/3271
19
+ */
20
+
21
+ import type {
22
+ BorderChars,
23
+ RenderAdapter,
24
+ RenderBuffer,
25
+ RenderStyle,
26
+ TextMeasureResult,
27
+ TextMeasureStyle,
28
+ TextMeasurer,
29
+ } from "../render-adapter"
30
+
31
+ // ============================================================================
32
+ // Configuration
33
+ // ============================================================================
34
+
35
+ export interface DOMAdapterConfig {
36
+ /** Font size in pixels (default: 14) */
37
+ fontSize?: number
38
+ /** Font family (default: 'monospace') */
39
+ fontFamily?: string
40
+ /** Line height multiplier (default: 1.2) */
41
+ lineHeight?: number
42
+ /** Background color (default: '#1e1e1e') */
43
+ backgroundColor?: string
44
+ /** Default foreground color (default: '#d4d4d4') */
45
+ foregroundColor?: string
46
+ /** CSS class prefix (default: 'silvery') */
47
+ classPrefix?: string
48
+ }
49
+
50
+ const DEFAULT_CONFIG: Required<DOMAdapterConfig> = {
51
+ fontSize: 14,
52
+ fontFamily: "monospace",
53
+ lineHeight: 1.2,
54
+ backgroundColor: "#1e1e1e",
55
+ foregroundColor: "#d4d4d4",
56
+ classPrefix: "silvery",
57
+ }
58
+
59
+ // ============================================================================
60
+ // Border Characters (same as terminal/canvas for consistency)
61
+ // ============================================================================
62
+
63
+ const BORDER_CHARS: Record<string, BorderChars> = {
64
+ single: {
65
+ topLeft: "┌",
66
+ topRight: "┐",
67
+ bottomLeft: "└",
68
+ bottomRight: "┘",
69
+ horizontal: "─",
70
+ vertical: "│",
71
+ },
72
+ double: {
73
+ topLeft: "╔",
74
+ topRight: "╗",
75
+ bottomLeft: "╚",
76
+ bottomRight: "╝",
77
+ horizontal: "═",
78
+ vertical: "║",
79
+ },
80
+ round: {
81
+ topLeft: "╭",
82
+ topRight: "╮",
83
+ bottomLeft: "╰",
84
+ bottomRight: "╯",
85
+ horizontal: "─",
86
+ vertical: "│",
87
+ },
88
+ bold: {
89
+ topLeft: "┏",
90
+ topRight: "┓",
91
+ bottomLeft: "┗",
92
+ bottomRight: "┛",
93
+ horizontal: "━",
94
+ vertical: "┃",
95
+ },
96
+ }
97
+
98
+ // ============================================================================
99
+ // Color Conversion
100
+ // ============================================================================
101
+
102
+ const ANSI_COLORS: Record<string, string> = {
103
+ black: "#000000",
104
+ red: "#cd0000",
105
+ green: "#00cd00",
106
+ yellow: "#cdcd00",
107
+ blue: "#0000ee",
108
+ magenta: "#cd00cd",
109
+ cyan: "#00cdcd",
110
+ white: "#e5e5e5",
111
+ gray: "#7f7f7f",
112
+ grey: "#7f7f7f",
113
+ brightblack: "#7f7f7f",
114
+ brightred: "#ff0000",
115
+ brightgreen: "#00ff00",
116
+ brightyellow: "#ffff00",
117
+ brightblue: "#5c5cff",
118
+ brightmagenta: "#ff00ff",
119
+ brightcyan: "#00ffff",
120
+ brightwhite: "#ffffff",
121
+ }
122
+
123
+ function resolveColor(color: string | undefined, fallback: string): string {
124
+ if (!color) return fallback
125
+ if (color.startsWith("#") || color.startsWith("rgb")) return color
126
+ const named = ANSI_COLORS[color.toLowerCase()]
127
+ return named ?? color
128
+ }
129
+
130
+ // ============================================================================
131
+ // DOM Measurer
132
+ // ============================================================================
133
+
134
+ function createDOMMeasurer(_config: Required<DOMAdapterConfig>): TextMeasurer {
135
+ // The layout engine operates in cell units (columns x rows), matching the
136
+ // terminal convention. For monospace fonts, text width = character count
137
+ // and line height = 1 row.
138
+ return {
139
+ measureText(text: string, _style?: TextMeasureStyle): TextMeasureResult {
140
+ // For monospace fonts, width is simply the character count (one cell per char)
141
+ return {
142
+ width: text.length,
143
+ height: 1,
144
+ }
145
+ },
146
+
147
+ getLineHeight(_style?: TextMeasureStyle): number {
148
+ return 1
149
+ },
150
+ }
151
+ }
152
+
153
+ // ============================================================================
154
+ // Styled Text Run
155
+ // ============================================================================
156
+
157
+ interface TextRun {
158
+ text: string
159
+ style: RenderStyle
160
+ x: number
161
+ }
162
+
163
+ // ============================================================================
164
+ // DOM Render Buffer
165
+ // ============================================================================
166
+
167
+ export class DOMRenderBuffer implements RenderBuffer {
168
+ readonly width: number
169
+ readonly height: number
170
+
171
+ private config: Required<DOMAdapterConfig>
172
+ private lines: Map<number, TextRun[]>
173
+ private backgrounds: Map<string, { x: number; y: number; w: number; h: number; color: string }>
174
+
175
+ // Cell-to-pixel conversion factors
176
+ private readonly charWidth: number
177
+ private readonly cellHeight: number
178
+
179
+ // Container element (set when flushing)
180
+ private container: HTMLElement | null = null
181
+
182
+ constructor(width: number, height: number, config: Required<DOMAdapterConfig>) {
183
+ this.width = width
184
+ this.height = height
185
+ this.config = config
186
+ this.lines = new Map()
187
+ this.backgrounds = new Map()
188
+
189
+ // Compute cell dimensions for coordinate conversion.
190
+ // Width/height are in cell units (cols/rows); rendering converts to pixels.
191
+ this.charWidth = config.fontSize * 0.6
192
+ this.cellHeight = config.fontSize * config.lineHeight
193
+ }
194
+
195
+ /**
196
+ * Set the container element for rendering.
197
+ */
198
+ setContainer(container: HTMLElement): void {
199
+ this.container = container
200
+ }
201
+
202
+ /**
203
+ * Get the container element.
204
+ */
205
+ getContainer(): HTMLElement | null {
206
+ return this.container
207
+ }
208
+
209
+ fillRect(x: number, y: number, width: number, height: number, style: RenderStyle): void {
210
+ if (style.bg) {
211
+ const key = `${x},${y},${width},${height}`
212
+ this.backgrounds.set(key, {
213
+ x,
214
+ y,
215
+ w: width,
216
+ h: height,
217
+ color: resolveColor(style.bg, this.config.backgroundColor),
218
+ })
219
+ }
220
+ }
221
+
222
+ drawText(x: number, y: number, text: string, style: RenderStyle): void {
223
+ if (!this.lines.has(y)) {
224
+ this.lines.set(y, [])
225
+ }
226
+ this.lines.get(y)!.push({ text, style, x })
227
+ }
228
+
229
+ drawChar(x: number, y: number, char: string, style: RenderStyle): void {
230
+ this.drawText(x, y, char, style)
231
+ }
232
+
233
+ inBounds(x: number, y: number): boolean {
234
+ return x >= 0 && x < this.width && y >= 0 && y < this.height
235
+ }
236
+
237
+ /**
238
+ * Render the buffer to the container element.
239
+ * Coordinates in the buffer are in cell units (cols/rows).
240
+ * This method converts them to pixel coordinates for DOM positioning.
241
+ */
242
+ render(): void {
243
+ if (!this.container) {
244
+ throw new Error("DOMRenderBuffer: No container set. Call setContainer() first.")
245
+ }
246
+
247
+ const container = this.container
248
+ const cw = this.charWidth
249
+ const ch = this.cellHeight
250
+
251
+ // Container dimensions in pixels (convert cell units back to pixels)
252
+ const containerWidthPx = this.width * cw
253
+ const containerHeightPx = this.height * ch
254
+
255
+ // Clear previous content
256
+ container.innerHTML = ""
257
+
258
+ // Set container styles
259
+ container.style.cssText = `
260
+ position: relative;
261
+ font-family: ${this.config.fontFamily};
262
+ font-size: ${this.config.fontSize}px;
263
+ line-height: ${this.config.lineHeight};
264
+ background-color: ${this.config.backgroundColor};
265
+ color: ${this.config.foregroundColor};
266
+ white-space: pre;
267
+ overflow: hidden;
268
+ width: ${containerWidthPx}px;
269
+ height: ${containerHeightPx}px;
270
+ `
271
+
272
+ // Render background rectangles (convert cell coords to pixels)
273
+ for (const bg of this.backgrounds.values()) {
274
+ const bgDiv = document.createElement("div")
275
+ bgDiv.className = `${this.config.classPrefix}-bg`
276
+ bgDiv.style.cssText = `
277
+ position: absolute;
278
+ left: ${bg.x * cw}px;
279
+ top: ${bg.y * ch}px;
280
+ width: ${bg.w * cw}px;
281
+ height: ${bg.h * ch}px;
282
+ background-color: ${bg.color};
283
+ `
284
+ container.appendChild(bgDiv)
285
+ }
286
+
287
+ // Render text lines (convert cell coords to pixels)
288
+ const sortedLines = Array.from(this.lines.entries()).sort((a, b) => a[0] - b[0])
289
+
290
+ for (const [y, runs] of sortedLines) {
291
+ const lineDiv = document.createElement("div")
292
+ lineDiv.className = `${this.config.classPrefix}-line`
293
+ lineDiv.style.cssText = `
294
+ position: absolute;
295
+ left: 0;
296
+ top: ${y * ch}px;
297
+ height: ${ch}px;
298
+ white-space: pre;
299
+ `
300
+
301
+ // Sort runs by x position
302
+ const sortedRuns = runs.sort((a, b) => a.x - b.x)
303
+
304
+ for (const run of sortedRuns) {
305
+ const span = document.createElement("span")
306
+ span.className = `${this.config.classPrefix}-text`
307
+ span.textContent = run.text
308
+
309
+ // Apply styles (convert cell x to pixel x)
310
+ const styles: string[] = [`position: absolute`, `left: ${run.x * cw}px`]
311
+
312
+ if (run.style.fg) {
313
+ styles.push(`color: ${resolveColor(run.style.fg, this.config.foregroundColor)}`)
314
+ }
315
+ if (run.style.bg) {
316
+ styles.push(`background-color: ${resolveColor(run.style.bg, this.config.backgroundColor)}`)
317
+ }
318
+
319
+ const attrs = run.style.attrs
320
+ if (attrs) {
321
+ if (attrs.bold) styles.push("font-weight: bold")
322
+ if (attrs.dim) styles.push("opacity: 0.5")
323
+ if (attrs.italic) styles.push("font-style: italic")
324
+
325
+ // Underline handling
326
+ if (attrs.underline || attrs.underlineStyle) {
327
+ const underlineStyle = attrs.underlineStyle ?? "single"
328
+ const underlineColor = attrs.underlineColor
329
+ ? resolveColor(attrs.underlineColor, this.config.foregroundColor)
330
+ : "currentColor"
331
+
332
+ switch (underlineStyle) {
333
+ case "double":
334
+ styles.push(`text-decoration: underline double ${underlineColor}`)
335
+ break
336
+ case "curly":
337
+ styles.push(`text-decoration: underline wavy ${underlineColor}`)
338
+ break
339
+ case "dotted":
340
+ styles.push(`text-decoration: underline dotted ${underlineColor}`)
341
+ break
342
+ case "dashed":
343
+ styles.push(`text-decoration: underline dashed ${underlineColor}`)
344
+ break
345
+ default:
346
+ styles.push(`text-decoration: underline solid ${underlineColor}`)
347
+ }
348
+ }
349
+
350
+ if (attrs.strikethrough) {
351
+ const existing = styles.find((s) => s.startsWith("text-decoration:"))
352
+ if (existing) {
353
+ const idx = styles.indexOf(existing)
354
+ styles[idx] = existing.replace("underline", "underline line-through")
355
+ } else {
356
+ styles.push("text-decoration: line-through")
357
+ }
358
+ }
359
+
360
+ if (attrs.inverse) {
361
+ // Swap foreground/background
362
+ const fg = run.style.fg
363
+ ? resolveColor(run.style.fg, this.config.foregroundColor)
364
+ : this.config.foregroundColor
365
+ const bg = run.style.bg
366
+ ? resolveColor(run.style.bg, this.config.backgroundColor)
367
+ : this.config.backgroundColor
368
+ styles.push(`color: ${bg}`, `background-color: ${fg}`)
369
+ }
370
+ }
371
+
372
+ span.style.cssText = styles.join("; ")
373
+ lineDiv.appendChild(span)
374
+ }
375
+
376
+ container.appendChild(lineDiv)
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Clear the buffer.
382
+ */
383
+ clear(): void {
384
+ this.lines.clear()
385
+ this.backgrounds.clear()
386
+ }
387
+ }
388
+
389
+ // ============================================================================
390
+ // DOM Adapter Factory
391
+ // ============================================================================
392
+
393
+ export function createDOMAdapter(config: DOMAdapterConfig = {}): RenderAdapter {
394
+ const cfg = { ...DEFAULT_CONFIG, ...config }
395
+ const measurer = createDOMMeasurer(cfg)
396
+
397
+ return {
398
+ name: "dom",
399
+ measurer,
400
+
401
+ createBuffer(width: number, height: number): RenderBuffer {
402
+ return new DOMRenderBuffer(width, height, cfg)
403
+ },
404
+
405
+ flush(buffer: RenderBuffer, _prevBuffer: RenderBuffer | null): void {
406
+ // DOM buffer renders directly when render() is called
407
+ const domBuffer = buffer as DOMRenderBuffer
408
+ if (domBuffer.getContainer()) {
409
+ domBuffer.render()
410
+ }
411
+ },
412
+
413
+ getBorderChars(style: string): BorderChars {
414
+ return BORDER_CHARS[style] ?? BORDER_CHARS.single!
415
+ },
416
+ }
417
+ }
418
+
419
+ // ============================================================================
420
+ // Inject Global Styles (Optional)
421
+ // ============================================================================
422
+
423
+ let stylesInjected = false
424
+
425
+ /**
426
+ * Inject global CSS styles for silvery DOM rendering.
427
+ * Call once at application startup if you want default styling.
428
+ */
429
+ export function injectDOMStyles(classPrefix = "silvery"): void {
430
+ if (stylesInjected || typeof document === "undefined") return
431
+
432
+ const style = document.createElement("style")
433
+ style.textContent = `
434
+ .${classPrefix}-container {
435
+ font-family: monospace;
436
+ white-space: pre;
437
+ overflow: hidden;
438
+ }
439
+ .${classPrefix}-line {
440
+ white-space: pre;
441
+ }
442
+ .${classPrefix}-text {
443
+ white-space: pre;
444
+ }
445
+ /* Selection styling */
446
+ .${classPrefix}-text::selection {
447
+ background-color: rgba(100, 150, 255, 0.3);
448
+ }
449
+ `
450
+ document.head.appendChild(style)
451
+ stylesInjected = true
452
+ }