@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
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@silvery/term",
3
+ "version": "0.3.0",
4
+ "description": "Terminal rendering target for silvery",
5
+ "license": "MIT",
6
+ "author": "Bjørn Stabell <bjorn@stabell.org>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/beorn/silvery.git",
10
+ "directory": "packages/term"
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "type": "module",
16
+ "main": "src/index.ts",
17
+ "types": "src/index.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./src/index.ts",
21
+ "import": "./src/index.ts"
22
+ },
23
+ "./runtime": {
24
+ "types": "./src/runtime/index.ts",
25
+ "import": "./src/runtime/index.ts"
26
+ },
27
+ "./toolbelt": {
28
+ "types": "./src/toolbelt/index.ts",
29
+ "import": "./src/toolbelt/index.ts"
30
+ },
31
+ "./pipeline": {
32
+ "types": "./src/pipeline/index.ts",
33
+ "import": "./src/pipeline/index.ts"
34
+ },
35
+ "./ansi": {
36
+ "types": "./src/ansi/index.ts",
37
+ "import": "./src/ansi/index.ts"
38
+ },
39
+ "./*": {
40
+ "types": "./src/*.ts",
41
+ "import": "./src/*.ts"
42
+ }
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "@silvery/react": "workspace:*",
49
+ "@silvery/tea": "workspace:*",
50
+ "@silvery/test": "workspace:*",
51
+ "@silvery/theme": "workspace:*",
52
+ "flexily": "github:beorn/flexily"
53
+ }
54
+ }
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Canvas Render Adapter
3
+ *
4
+ * Implements the RenderAdapter interface for HTML5 Canvas output.
5
+ * The layout engine operates in cell units (columns x rows). This adapter
6
+ * converts cell coordinates to pixel coordinates when drawing to the canvas,
7
+ * using charWidth (fontSize * 0.6) and cellHeight (fontSize * lineHeight).
8
+ */
9
+
10
+ import type {
11
+ BorderChars,
12
+ RenderAdapter,
13
+ RenderBuffer,
14
+ RenderStyle,
15
+ TextMeasureResult,
16
+ TextMeasureStyle,
17
+ TextMeasurer,
18
+ } from "../render-adapter"
19
+
20
+ // ============================================================================
21
+ // Configuration
22
+ // ============================================================================
23
+
24
+ export interface CanvasAdapterConfig {
25
+ /** Font size in pixels (default: 14) */
26
+ fontSize?: number
27
+ /** Font family (default: 'monospace') */
28
+ fontFamily?: string
29
+ /** Line height multiplier (default: 1.2) */
30
+ lineHeight?: number
31
+ /** Background color (default: '#1e1e1e') */
32
+ backgroundColor?: string
33
+ /** Default foreground color (default: '#d4d4d4') */
34
+ foregroundColor?: string
35
+ }
36
+
37
+ const DEFAULT_CONFIG: Required<CanvasAdapterConfig> = {
38
+ fontSize: 14,
39
+ fontFamily: "monospace",
40
+ lineHeight: 1.2,
41
+ backgroundColor: "#1e1e1e",
42
+ foregroundColor: "#d4d4d4",
43
+ }
44
+
45
+ // ============================================================================
46
+ // Border Characters (same as terminal for consistency)
47
+ // ============================================================================
48
+
49
+ const BORDER_CHARS: Record<string, BorderChars> = {
50
+ single: {
51
+ topLeft: "┌",
52
+ topRight: "┐",
53
+ bottomLeft: "└",
54
+ bottomRight: "┘",
55
+ horizontal: "─",
56
+ vertical: "│",
57
+ },
58
+ double: {
59
+ topLeft: "╔",
60
+ topRight: "╗",
61
+ bottomLeft: "╚",
62
+ bottomRight: "╝",
63
+ horizontal: "═",
64
+ vertical: "║",
65
+ },
66
+ round: {
67
+ topLeft: "╭",
68
+ topRight: "╮",
69
+ bottomLeft: "╰",
70
+ bottomRight: "╯",
71
+ horizontal: "─",
72
+ vertical: "│",
73
+ },
74
+ bold: {
75
+ topLeft: "┏",
76
+ topRight: "┓",
77
+ bottomLeft: "┗",
78
+ bottomRight: "┛",
79
+ horizontal: "━",
80
+ vertical: "┃",
81
+ },
82
+ }
83
+
84
+ // ============================================================================
85
+ // Canvas Measurer
86
+ // ============================================================================
87
+
88
+ function createCanvasMeasurer(_config: Required<CanvasAdapterConfig>): TextMeasurer {
89
+ // The layout engine operates in cell units (columns x rows), matching the
90
+ // terminal convention. For monospace fonts, text width = character count
91
+ // and line height = 1 row.
92
+ return {
93
+ measureText(text: string, _style?: TextMeasureStyle): TextMeasureResult {
94
+ // For monospace fonts, width is simply the character count (one cell per char)
95
+ return {
96
+ width: text.length,
97
+ height: 1,
98
+ }
99
+ },
100
+
101
+ getLineHeight(_style?: TextMeasureStyle): number {
102
+ return 1
103
+ },
104
+ }
105
+ }
106
+
107
+ // ============================================================================
108
+ // Color Conversion
109
+ // ============================================================================
110
+
111
+ // ANSI 256-color palette (standard 16 colors)
112
+ const ANSI_COLORS: Record<string, string> = {
113
+ black: "#000000",
114
+ red: "#cd0000",
115
+ green: "#00cd00",
116
+ yellow: "#cdcd00",
117
+ blue: "#0000ee",
118
+ magenta: "#cd00cd",
119
+ cyan: "#00cdcd",
120
+ white: "#e5e5e5",
121
+ gray: "#7f7f7f",
122
+ grey: "#7f7f7f",
123
+ brightBlack: "#7f7f7f",
124
+ brightRed: "#ff0000",
125
+ brightGreen: "#00ff00",
126
+ brightYellow: "#ffff00",
127
+ brightBlue: "#5c5cff",
128
+ brightMagenta: "#ff00ff",
129
+ brightCyan: "#00ffff",
130
+ brightWhite: "#ffffff",
131
+ }
132
+
133
+ function resolveColor(color: string | undefined, fallback: string): string {
134
+ if (!color) return fallback
135
+
136
+ // Already a CSS color (hex, rgb, etc.)
137
+ if (color.startsWith("#") || color.startsWith("rgb")) {
138
+ return color
139
+ }
140
+
141
+ // Named ANSI color
142
+ const named = ANSI_COLORS[color.toLowerCase()]
143
+ if (named) return named
144
+
145
+ // Pass through (might be a CSS color name like 'cyan')
146
+ return color
147
+ }
148
+
149
+ // ============================================================================
150
+ // Canvas Render Buffer
151
+ // ============================================================================
152
+
153
+ export class CanvasRenderBuffer implements RenderBuffer {
154
+ readonly width: number
155
+ readonly height: number
156
+ readonly canvas: OffscreenCanvas | HTMLCanvasElement
157
+ private ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
158
+ private config: Required<CanvasAdapterConfig>
159
+
160
+ // Cell-to-pixel conversion factors
161
+ private readonly charWidth: number
162
+ private readonly cellHeight: number
163
+
164
+ constructor(width: number, height: number, config: Required<CanvasAdapterConfig>) {
165
+ this.width = width
166
+ this.height = height
167
+ this.config = config
168
+
169
+ // Compute cell dimensions for coordinate conversion.
170
+ // Width/height are in cell units (cols/rows); drawing converts to pixels.
171
+ this.charWidth = config.fontSize * 0.6
172
+ this.cellHeight = config.fontSize * config.lineHeight
173
+
174
+ // Canvas pixel dimensions (convert cell units to pixels)
175
+ const pixelWidth = width * this.charWidth
176
+ const pixelHeight = height * this.cellHeight
177
+
178
+ // Use OffscreenCanvas for double buffering
179
+ if (typeof OffscreenCanvas !== "undefined") {
180
+ this.canvas = new OffscreenCanvas(pixelWidth, pixelHeight)
181
+ } else if (typeof document !== "undefined") {
182
+ this.canvas = document.createElement("canvas")
183
+ this.canvas.width = pixelWidth
184
+ this.canvas.height = pixelHeight
185
+ } else {
186
+ throw new Error("Canvas not available")
187
+ }
188
+
189
+ const ctx = this.canvas.getContext("2d")
190
+ if (!ctx) throw new Error("Could not get 2d context")
191
+ this.ctx = ctx as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
192
+
193
+ // Initialize with background
194
+ this.ctx.fillStyle = config.backgroundColor
195
+ this.ctx.fillRect(0, 0, pixelWidth, pixelHeight)
196
+ }
197
+
198
+ fillRect(x: number, y: number, width: number, height: number, style: RenderStyle): void {
199
+ if (style.bg) {
200
+ // Convert cell coordinates to pixel coordinates
201
+ const px = x * this.charWidth
202
+ const py = y * this.cellHeight
203
+ const pw = width * this.charWidth
204
+ const ph = height * this.cellHeight
205
+ this.ctx.fillStyle = resolveColor(style.bg, this.config.backgroundColor)
206
+ this.ctx.fillRect(px, py, pw, ph)
207
+ }
208
+ }
209
+
210
+ drawText(x: number, y: number, text: string, style: RenderStyle): void {
211
+ // Convert cell coordinates to pixel coordinates
212
+ const px = x * this.charWidth
213
+ const py = y * this.cellHeight
214
+
215
+ const attrs = style.attrs ?? {}
216
+
217
+ // Build font string
218
+ const weight = attrs.bold ? "bold" : "normal"
219
+ const fontStyle = attrs.italic ? "italic" : "normal"
220
+ this.ctx.font = `${fontStyle} ${weight} ${this.config.fontSize}px ${this.config.fontFamily}`
221
+
222
+ // Set colors
223
+ this.ctx.fillStyle = resolveColor(style.fg, this.config.foregroundColor)
224
+ this.ctx.textBaseline = "top"
225
+
226
+ // Draw text
227
+ this.ctx.fillText(text, px, py)
228
+
229
+ // Handle underline
230
+ if (attrs.underline) {
231
+ this.drawUnderline(px, py, text, style)
232
+ }
233
+
234
+ // Handle strikethrough
235
+ if (attrs.strikethrough) {
236
+ const metrics = this.ctx.measureText(text)
237
+ const textWidth = metrics.width
238
+ const strikeY = py + this.config.fontSize * 0.5
239
+
240
+ this.ctx.strokeStyle = resolveColor(style.fg, this.config.foregroundColor)
241
+ this.ctx.lineWidth = 1
242
+ this.ctx.beginPath()
243
+ this.ctx.moveTo(px, strikeY)
244
+ this.ctx.lineTo(px + textWidth, strikeY)
245
+ this.ctx.stroke()
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Draw underline decorations at pixel coordinates.
251
+ * Note: px, py are already in pixel coordinates.
252
+ */
253
+ private drawUnderline(px: number, py: number, text: string, style: RenderStyle): void {
254
+ const attrs = style.attrs ?? {}
255
+ const metrics = this.ctx.measureText(text)
256
+ const textWidth = metrics.width
257
+ const underlineY = py + this.config.fontSize * 0.9
258
+
259
+ const underlineColor = resolveColor(attrs.underlineColor ?? style.fg, this.config.foregroundColor)
260
+
261
+ this.ctx.strokeStyle = underlineColor
262
+ this.ctx.lineWidth = 1
263
+
264
+ const underlineStyle = attrs.underlineStyle ?? "single"
265
+
266
+ switch (underlineStyle) {
267
+ case "double":
268
+ // Two parallel lines
269
+ this.ctx.beginPath()
270
+ this.ctx.moveTo(px, underlineY - 1)
271
+ this.ctx.lineTo(px + textWidth, underlineY - 1)
272
+ this.ctx.moveTo(px, underlineY + 1)
273
+ this.ctx.lineTo(px + textWidth, underlineY + 1)
274
+ this.ctx.stroke()
275
+ break
276
+
277
+ case "curly":
278
+ // Wavy line using bezier curves
279
+ this.ctx.beginPath()
280
+ this.ctx.moveTo(px, underlineY)
281
+ const waveLength = 4
282
+ const amplitude = 2
283
+ for (let wx = 0; wx < textWidth; wx += waveLength * 2) {
284
+ this.ctx.quadraticCurveTo(px + wx + waveLength / 2, underlineY - amplitude, px + wx + waveLength, underlineY)
285
+ this.ctx.quadraticCurveTo(
286
+ px + wx + (waveLength * 3) / 2,
287
+ underlineY + amplitude,
288
+ px + wx + waveLength * 2,
289
+ underlineY,
290
+ )
291
+ }
292
+ this.ctx.stroke()
293
+ break
294
+
295
+ case "dotted":
296
+ this.ctx.setLineDash([2, 2])
297
+ this.ctx.beginPath()
298
+ this.ctx.moveTo(px, underlineY)
299
+ this.ctx.lineTo(px + textWidth, underlineY)
300
+ this.ctx.stroke()
301
+ this.ctx.setLineDash([])
302
+ break
303
+
304
+ case "dashed":
305
+ this.ctx.setLineDash([4, 2])
306
+ this.ctx.beginPath()
307
+ this.ctx.moveTo(px, underlineY)
308
+ this.ctx.lineTo(px + textWidth, underlineY)
309
+ this.ctx.stroke()
310
+ this.ctx.setLineDash([])
311
+ break
312
+
313
+ default: // 'single'
314
+ this.ctx.beginPath()
315
+ this.ctx.moveTo(px, underlineY)
316
+ this.ctx.lineTo(px + textWidth, underlineY)
317
+ this.ctx.stroke()
318
+ }
319
+ }
320
+
321
+ drawChar(x: number, y: number, char: string, style: RenderStyle): void {
322
+ // For canvas, drawChar is essentially drawText for single chars
323
+ this.drawText(x, y, char, style)
324
+ }
325
+
326
+ inBounds(x: number, y: number): boolean {
327
+ return x >= 0 && x < this.width && y >= 0 && y < this.height
328
+ }
329
+ }
330
+
331
+ // ============================================================================
332
+ // Canvas Adapter Factory
333
+ // ============================================================================
334
+
335
+ export function createCanvasAdapter(config: CanvasAdapterConfig = {}): RenderAdapter {
336
+ const cfg = { ...DEFAULT_CONFIG, ...config }
337
+ const measurer = createCanvasMeasurer(cfg)
338
+
339
+ return {
340
+ name: "canvas",
341
+ measurer,
342
+
343
+ createBuffer(width: number, height: number): RenderBuffer {
344
+ return new CanvasRenderBuffer(width, height, cfg)
345
+ },
346
+
347
+ flush(_buffer: RenderBuffer, _prevBuffer: RenderBuffer | null): void {
348
+ // Canvas draws directly to the buffer during render.
349
+ // The caller (renderToCanvas) copies the buffer to the visible canvas.
350
+ },
351
+
352
+ getBorderChars(style: string): BorderChars {
353
+ return BORDER_CHARS[style] ?? BORDER_CHARS.single!
354
+ },
355
+ }
356
+ }