@gridland/web 0.2.15 → 0.2.17

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 (42) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +70 -13
  3. package/dist/index.js.map +2 -2
  4. package/dist/{next-CxYMg6AG.d.ts → next-BWTklBmN.d.ts} +22 -3
  5. package/dist/next.d.ts +1 -1
  6. package/dist/next.js +70 -13
  7. package/dist/next.js.map +2 -2
  8. package/dist/vite-plugin.js +73 -38
  9. package/dist/vite-plugin.js.map +1 -1
  10. package/package.json +7 -2
  11. package/src/browser-buffer.ts +715 -0
  12. package/src/core-shims/index.ts +269 -0
  13. package/src/core-shims/renderable-types.ts +4 -0
  14. package/src/core-shims/rgba.ts +195 -0
  15. package/src/core-shims/types.ts +132 -0
  16. package/src/shims/bun-ffi-structs.ts +20 -0
  17. package/src/shims/bun-ffi.ts +28 -0
  18. package/src/shims/console-stub.ts +13 -0
  19. package/src/shims/console.ts +3 -0
  20. package/src/shims/devtools-polyfill-stub.ts +3 -0
  21. package/src/shims/edit-buffer-stub.ts +475 -0
  22. package/src/shims/editor-view-stub.ts +388 -0
  23. package/src/shims/events-shim.ts +81 -0
  24. package/src/shims/filters-stub.ts +4 -0
  25. package/src/shims/hast-stub.ts +8 -0
  26. package/src/shims/native-span-feed-stub.ts +7 -0
  27. package/src/shims/node-buffer.ts +39 -0
  28. package/src/shims/node-fs.ts +20 -0
  29. package/src/shims/node-os.ts +6 -0
  30. package/src/shims/node-path.ts +35 -0
  31. package/src/shims/node-stream.ts +10 -0
  32. package/src/shims/node-url.ts +8 -0
  33. package/src/shims/node-util.ts +33 -0
  34. package/src/shims/renderer-stub.ts +21 -0
  35. package/src/shims/slider-deps.ts +8 -0
  36. package/src/shims/syntax-style-shim.ts +23 -0
  37. package/src/shims/text-buffer-shim.ts +3 -0
  38. package/src/shims/text-buffer-view-shim.ts +2 -0
  39. package/src/shims/timeline-stub.ts +43 -0
  40. package/src/shims/tree-sitter-stub.ts +47 -0
  41. package/src/shims/tree-sitter-styled-text-stub.ts +8 -0
  42. package/src/shims/zig-stub.ts +20 -0
@@ -0,0 +1,715 @@
1
+ import type { RGBA } from "./core-shims/rgba"
2
+ import type { CapturedLine, CapturedSpan, CursorStyle } from "./core-shims/types"
3
+ import { attributesWithLink } from "./core-shims/index"
4
+
5
+ // Attribute flags matching TextAttributes from opentui core
6
+ const CONTINUATION = 0xc0000000
7
+
8
+ interface ScissorRect {
9
+ x: number
10
+ y: number
11
+ width: number
12
+ height: number
13
+ }
14
+
15
+ export type WidthMethod = "wcwidth" | "unicode"
16
+
17
+ export interface BorderDrawOptions {
18
+ x: number
19
+ y: number
20
+ width: number
21
+ height: number
22
+ borderStyle?: string
23
+ customBorderChars?: Uint32Array
24
+ border: boolean | string[]
25
+ borderColor: RGBA
26
+ backgroundColor: RGBA
27
+ shouldFill?: boolean
28
+ title?: string
29
+ titleAlignment?: "left" | "center" | "right"
30
+ }
31
+
32
+ export class BrowserBuffer {
33
+ public id: string
34
+ public respectAlpha: boolean
35
+
36
+ private _width: number
37
+ private _height: number
38
+ private _widthMethod: WidthMethod
39
+
40
+ // Cell data - same layout as native OptimizedBuffer
41
+ public char: Uint32Array
42
+ public fg: Float32Array
43
+ public bg: Float32Array
44
+ public attributes: Uint32Array
45
+
46
+ private scissorStack: ScissorRect[] = []
47
+ private opacityStack: number[] = []
48
+
49
+ // Link registry for clickable links
50
+ public linkRegistry: Map<number, string> = new Map()
51
+ private nextLinkId: number = 1
52
+ /** Cursor rendering config -- set by renderer before pipeline, read by drawEditorView */
53
+ public cursorColor: RGBA | null = null
54
+ public cursorStyleType: CursorStyle = "block"
55
+ /** Line cursor position -- set by drawEditorView during pipeline, read by renderer after */
56
+ public lineCursorPosition: { x: number; y: number } | null = null
57
+
58
+ constructor(
59
+ width: number,
60
+ height: number,
61
+ options: { respectAlpha?: boolean; id?: string; widthMethod?: WidthMethod } = {},
62
+ ) {
63
+ this._width = width
64
+ this._height = height
65
+ this._widthMethod = options.widthMethod ?? "wcwidth"
66
+ this.respectAlpha = options.respectAlpha ?? false
67
+ this.id = options.id ?? `browser-buffer-${Math.random().toString(36).slice(2, 8)}`
68
+
69
+ const size = width * height
70
+ this.char = new Uint32Array(size)
71
+ this.fg = new Float32Array(size * 4)
72
+ this.bg = new Float32Array(size * 4)
73
+ this.attributes = new Uint32Array(size)
74
+
75
+ // Fill with spaces
76
+ this.char.fill(0x20) // space
77
+ }
78
+
79
+ static create(
80
+ width: number,
81
+ height: number,
82
+ widthMethod: WidthMethod,
83
+ options?: { respectAlpha?: boolean; id?: string },
84
+ ): BrowserBuffer {
85
+ return new BrowserBuffer(width, height, { ...options, widthMethod })
86
+ }
87
+
88
+ get width(): number {
89
+ return this._width
90
+ }
91
+
92
+ get height(): number {
93
+ return this._height
94
+ }
95
+
96
+ get widthMethod(): WidthMethod {
97
+ return this._widthMethod
98
+ }
99
+
100
+ get ptr(): number {
101
+ return 0
102
+ }
103
+
104
+ get buffers() {
105
+ return {
106
+ char: this.char,
107
+ fg: this.fg,
108
+ bg: this.bg,
109
+ attributes: this.attributes,
110
+ }
111
+ }
112
+
113
+ setRespectAlpha(respectAlpha: boolean): void {
114
+ this.respectAlpha = respectAlpha
115
+ }
116
+
117
+ getNativeId(): string {
118
+ return this.id
119
+ }
120
+
121
+ registerLink(url: string): number {
122
+ const id = this.nextLinkId++
123
+ this.linkRegistry.set(id, url)
124
+ return id
125
+ }
126
+
127
+ getLinkUrl(linkId: number): string | undefined {
128
+ return this.linkRegistry.get(linkId)
129
+ }
130
+
131
+ private isInScissor(x: number, y: number): boolean {
132
+ if (this.scissorStack.length === 0) return true
133
+ const rect = this.scissorStack[this.scissorStack.length - 1]
134
+ return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height
135
+ }
136
+
137
+ private getCurrentOpacityMultiplier(): number {
138
+ if (this.opacityStack.length === 0) return 1
139
+ return this.opacityStack[this.opacityStack.length - 1]
140
+ }
141
+
142
+ private applyOpacity(color: RGBA): RGBA {
143
+ const multiplier = this.getCurrentOpacityMultiplier()
144
+ if (multiplier >= 1) return color
145
+ return {
146
+ r: color.r,
147
+ g: color.g,
148
+ b: color.b,
149
+ a: color.a * multiplier,
150
+ buffer: new Float32Array([color.r, color.g, color.b, color.a * multiplier]),
151
+ toInts: color.toInts,
152
+ equals: color.equals,
153
+ map: color.map,
154
+ toString: color.toString,
155
+ } as RGBA
156
+ }
157
+
158
+ clear(bg?: RGBA): void {
159
+ const size = this._width * this._height
160
+ this.char.fill(0x20) // space
161
+ this.attributes.fill(0)
162
+ this.linkRegistry.clear()
163
+ this.nextLinkId = 1
164
+
165
+ if (bg) {
166
+ for (let i = 0; i < size; i++) {
167
+ const offset = i * 4
168
+ this.bg[offset] = bg.r
169
+ this.bg[offset + 1] = bg.g
170
+ this.bg[offset + 2] = bg.b
171
+ this.bg[offset + 3] = bg.a
172
+ // Clear fg
173
+ this.fg[offset] = 0
174
+ this.fg[offset + 1] = 0
175
+ this.fg[offset + 2] = 0
176
+ this.fg[offset + 3] = 0
177
+ }
178
+ } else {
179
+ this.fg.fill(0)
180
+ this.bg.fill(0)
181
+ }
182
+ }
183
+
184
+ setCell(x: number, y: number, char: string, fgColor: RGBA, bgColor: RGBA, attr: number = 0): void {
185
+ if (x < 0 || x >= this._width || y < 0 || y >= this._height) return
186
+ if (!this.isInScissor(x, y)) return
187
+
188
+ const idx = y * this._width + x
189
+ const offset = idx * 4
190
+
191
+ const effectiveBg = this.applyOpacity(bgColor)
192
+ const effectiveFg = this.applyOpacity(fgColor)
193
+
194
+ this.char[idx] = char.codePointAt(0) ?? 0x20
195
+ this.attributes[idx] = attr
196
+
197
+ this.fg[offset] = effectiveFg.r
198
+ this.fg[offset + 1] = effectiveFg.g
199
+ this.fg[offset + 2] = effectiveFg.b
200
+ this.fg[offset + 3] = effectiveFg.a
201
+
202
+ this.bg[offset] = effectiveBg.r
203
+ this.bg[offset + 1] = effectiveBg.g
204
+ this.bg[offset + 2] = effectiveBg.b
205
+ this.bg[offset + 3] = effectiveBg.a
206
+ }
207
+
208
+ setCellWithAlphaBlending(
209
+ x: number,
210
+ y: number,
211
+ char: string,
212
+ fgColor: RGBA,
213
+ bgColor: RGBA,
214
+ attr: number = 0,
215
+ ): void {
216
+ // For the PoC, same as setCell
217
+ this.setCell(x, y, char, fgColor, bgColor, attr)
218
+ }
219
+
220
+ drawChar(charCode: number, x: number, y: number, fgColor: RGBA, bgColor: RGBA, attr: number = 0): void {
221
+ if (x < 0 || x >= this._width || y < 0 || y >= this._height) return
222
+ if (!this.isInScissor(x, y)) return
223
+
224
+ const idx = y * this._width + x
225
+ const offset = idx * 4
226
+
227
+ const effectiveBg = this.applyOpacity(bgColor)
228
+ const effectiveFg = this.applyOpacity(fgColor)
229
+
230
+ this.char[idx] = charCode
231
+ this.attributes[idx] = attr
232
+
233
+ this.fg[offset] = effectiveFg.r
234
+ this.fg[offset + 1] = effectiveFg.g
235
+ this.fg[offset + 2] = effectiveFg.b
236
+ this.fg[offset + 3] = effectiveFg.a
237
+
238
+ this.bg[offset] = effectiveBg.r
239
+ this.bg[offset + 1] = effectiveBg.g
240
+ this.bg[offset + 2] = effectiveBg.b
241
+ this.bg[offset + 3] = effectiveBg.a
242
+ }
243
+
244
+ drawText(
245
+ text: string,
246
+ x: number,
247
+ y: number,
248
+ fgColor: RGBA,
249
+ bgColor?: RGBA,
250
+ attr: number = 0,
251
+ _selection?: { start: number; end: number; bgColor?: RGBA; fgColor?: RGBA } | null,
252
+ ): void {
253
+ const transparentBg: RGBA = {
254
+ r: 0, g: 0, b: 0, a: 0,
255
+ buffer: new Float32Array([0, 0, 0, 0]),
256
+ } as RGBA
257
+ const bg = bgColor ?? transparentBg
258
+
259
+ let curX = x
260
+ for (const ch of text) {
261
+ if (curX >= this._width) break
262
+ if (curX >= 0) {
263
+ this.setCell(curX, y, ch, fgColor, bg, attr)
264
+ }
265
+ curX++
266
+ }
267
+ }
268
+
269
+ fillRect(x: number, y: number, width: number, height: number, bgColor: RGBA): void {
270
+ for (let row = y; row < y + height && row < this._height; row++) {
271
+ for (let col = x; col < x + width && col < this._width; col++) {
272
+ if (col < 0 || row < 0) continue
273
+ if (!this.isInScissor(col, row)) continue
274
+
275
+ const idx = row * this._width + col
276
+ const offset = idx * 4
277
+ const effectiveBg = this.applyOpacity(bgColor)
278
+
279
+ this.char[idx] = 0x20
280
+ this.bg[offset] = effectiveBg.r
281
+ this.bg[offset + 1] = effectiveBg.g
282
+ this.bg[offset + 2] = effectiveBg.b
283
+ this.bg[offset + 3] = effectiveBg.a
284
+ }
285
+ }
286
+ }
287
+
288
+ drawBox(options: BorderDrawOptions): void {
289
+ const {
290
+ x,
291
+ y,
292
+ width,
293
+ height,
294
+ border,
295
+ borderColor,
296
+ backgroundColor,
297
+ shouldFill = true,
298
+ title,
299
+ titleAlignment = "left",
300
+ } = options
301
+
302
+ if (width <= 0 || height <= 0) return
303
+
304
+ // Parse border sides
305
+ const sides = {
306
+ top: border === true || (Array.isArray(border) && border.includes("top")),
307
+ right: border === true || (Array.isArray(border) && border.includes("right")),
308
+ bottom: border === true || (Array.isArray(border) && border.includes("bottom")),
309
+ left: border === true || (Array.isArray(border) && border.includes("left")),
310
+ }
311
+
312
+ // Get border chars (use customBorderChars or default rounded)
313
+ const borderChars = options.customBorderChars ?? this.getDefaultBorderChars(options.borderStyle)
314
+
315
+ // Fill background
316
+ if (shouldFill) {
317
+ const fillStartX = x + (sides.left ? 1 : 0)
318
+ const fillStartY = y + (sides.top ? 1 : 0)
319
+ const fillWidth = width - (sides.left ? 1 : 0) - (sides.right ? 1 : 0)
320
+ const fillHeight = height - (sides.top ? 1 : 0) - (sides.bottom ? 1 : 0)
321
+ if (fillWidth > 0 && fillHeight > 0) {
322
+ this.fillRect(fillStartX, fillStartY, fillWidth, fillHeight, backgroundColor)
323
+ }
324
+ }
325
+
326
+ if (!border) return
327
+
328
+ const transparent: RGBA = { r: 0, g: 0, b: 0, a: 0, buffer: new Float32Array([0, 0, 0, 0]) } as RGBA
329
+
330
+ // Draw borders
331
+ // borderChars layout: [topLeft, topRight, bottomLeft, bottomRight, horizontal, vertical, topT, bottomT, leftT, rightT, cross]
332
+ const topLeft = borderChars[0]
333
+ const topRight = borderChars[1]
334
+ const bottomLeft = borderChars[2]
335
+ const bottomRight = borderChars[3]
336
+ const horizontal = borderChars[4]
337
+ const vertical = borderChars[5]
338
+
339
+ // Top border
340
+ if (sides.top) {
341
+ if (sides.left) this.drawChar(topLeft, x, y, borderColor, transparent)
342
+ for (let col = 1; col < width - 1; col++) {
343
+ this.drawChar(horizontal, x + col, y, borderColor, transparent)
344
+ }
345
+ if (sides.right && width > 1) this.drawChar(topRight, x + width - 1, y, borderColor, transparent)
346
+ }
347
+
348
+ // Bottom border
349
+ if (sides.bottom && height > 1) {
350
+ if (sides.left) this.drawChar(bottomLeft, x, y + height - 1, borderColor, transparent)
351
+ for (let col = 1; col < width - 1; col++) {
352
+ this.drawChar(horizontal, x + col, y + height - 1, borderColor, transparent)
353
+ }
354
+ if (sides.right && width > 1)
355
+ this.drawChar(bottomRight, x + width - 1, y + height - 1, borderColor, transparent)
356
+ }
357
+
358
+ // Left border
359
+ if (sides.left) {
360
+ for (let row = 1; row < height - 1; row++) {
361
+ this.drawChar(vertical, x, y + row, borderColor, transparent)
362
+ }
363
+ }
364
+
365
+ // Right border
366
+ if (sides.right && width > 1) {
367
+ for (let row = 1; row < height - 1; row++) {
368
+ this.drawChar(vertical, x + width - 1, y + row, borderColor, transparent)
369
+ }
370
+ }
371
+
372
+ // Draw title on top border
373
+ if (title && sides.top && width > 4) {
374
+ const maxTitleLen = width - 4
375
+ const truncatedTitle = title.length > maxTitleLen ? title.slice(0, maxTitleLen) : title
376
+ let titleX: number
377
+ if (titleAlignment === "center") {
378
+ titleX = x + Math.floor((width - truncatedTitle.length) / 2)
379
+ } else if (titleAlignment === "right") {
380
+ titleX = x + width - truncatedTitle.length - 2
381
+ } else {
382
+ titleX = x + 2
383
+ }
384
+ this.drawText(truncatedTitle, titleX, y, borderColor, transparent)
385
+ }
386
+ }
387
+
388
+ private getDefaultBorderChars(borderStyle?: string): Uint32Array {
389
+ // Rounded border chars by default
390
+ const styles: Record<string, number[]> = {
391
+ rounded: [0x256d, 0x256e, 0x2570, 0x256f, 0x2500, 0x2502, 0x252c, 0x2534, 0x251c, 0x2524, 0x253c],
392
+ single: [0x250c, 0x2510, 0x2514, 0x2518, 0x2500, 0x2502, 0x252c, 0x2534, 0x251c, 0x2524, 0x253c],
393
+ double: [0x2554, 0x2557, 0x255a, 0x255d, 0x2550, 0x2551, 0x2566, 0x2569, 0x2560, 0x2563, 0x256c],
394
+ heavy: [0x250f, 0x2513, 0x2517, 0x251b, 0x2501, 0x2503, 0x2533, 0x253b, 0x2523, 0x252b, 0x254b],
395
+ }
396
+ const chars = styles[borderStyle ?? "rounded"] ?? styles.rounded
397
+ return new Uint32Array(chars)
398
+ }
399
+
400
+ pushScissorRect(x: number, y: number, width: number, height: number): void {
401
+ if (this.scissorStack.length > 0) {
402
+ // Intersect with current scissor
403
+ const current = this.scissorStack[this.scissorStack.length - 1]
404
+ const nx = Math.max(x, current.x)
405
+ const ny = Math.max(y, current.y)
406
+ const nw = Math.min(x + width, current.x + current.width) - nx
407
+ const nh = Math.min(y + height, current.y + current.height) - ny
408
+ this.scissorStack.push({ x: nx, y: ny, width: Math.max(0, nw), height: Math.max(0, nh) })
409
+ } else {
410
+ this.scissorStack.push({ x, y, width, height })
411
+ }
412
+ }
413
+
414
+ popScissorRect(): void {
415
+ this.scissorStack.pop()
416
+ }
417
+
418
+ clearScissorRects(): void {
419
+ this.scissorStack = []
420
+ }
421
+
422
+ pushOpacity(opacity: number): void {
423
+ const current = this.getCurrentOpacityMultiplier()
424
+ this.opacityStack.push(current * opacity)
425
+ }
426
+
427
+ popOpacity(): void {
428
+ this.opacityStack.pop()
429
+ }
430
+
431
+ getCurrentOpacity(): number {
432
+ return this.getCurrentOpacityMultiplier()
433
+ }
434
+
435
+ clearOpacity(): void {
436
+ this.opacityStack = []
437
+ }
438
+
439
+ resize(width: number, height: number): void {
440
+ this._width = width
441
+ this._height = height
442
+ const size = width * height
443
+ this.char = new Uint32Array(size)
444
+ this.fg = new Float32Array(size * 4)
445
+ this.bg = new Float32Array(size * 4)
446
+ this.attributes = new Uint32Array(size)
447
+ this.char.fill(0x20)
448
+ }
449
+
450
+ // Read buffer into CapturedLine[] for testing
451
+ getSpanLines(): CapturedLine[] {
452
+ const lines: CapturedLine[] = []
453
+
454
+ for (let row = 0; row < this._height; row++) {
455
+ const spans: CapturedSpan[] = []
456
+ let currentSpan: CapturedSpan | null = null
457
+
458
+ for (let col = 0; col < this._width; col++) {
459
+ const idx = row * this._width + col
460
+ const offset = idx * 4
461
+
462
+ // Skip continuation chars
463
+ if (this.attributes[idx] & CONTINUATION) continue
464
+
465
+ const charCode = this.char[idx]
466
+ const ch = charCode === 0 ? " " : String.fromCodePoint(charCode)
467
+ const fgR = this.fg[offset]
468
+ const fgG = this.fg[offset + 1]
469
+ const fgB = this.fg[offset + 2]
470
+ const fgA = this.fg[offset + 3]
471
+ const bgR = this.bg[offset]
472
+ const bgG = this.bg[offset + 1]
473
+ const bgB = this.bg[offset + 2]
474
+ const bgA = this.bg[offset + 3]
475
+ const attr = this.attributes[idx] & 0xff
476
+
477
+ const fg: RGBA = {
478
+ r: fgR, g: fgG, b: fgB, a: fgA,
479
+ buffer: new Float32Array([fgR, fgG, fgB, fgA]),
480
+ } as RGBA
481
+ const bg: RGBA = {
482
+ r: bgR, g: bgG, b: bgB, a: bgA,
483
+ buffer: new Float32Array([bgR, bgG, bgB, bgA]),
484
+ } as RGBA
485
+
486
+ if (
487
+ currentSpan &&
488
+ currentSpan.fg.r === fgR &&
489
+ currentSpan.fg.g === fgG &&
490
+ currentSpan.fg.b === fgB &&
491
+ currentSpan.fg.a === fgA &&
492
+ currentSpan.bg.r === bgR &&
493
+ currentSpan.bg.g === bgG &&
494
+ currentSpan.bg.b === bgB &&
495
+ currentSpan.bg.a === bgA &&
496
+ currentSpan.attributes === attr
497
+ ) {
498
+ currentSpan.text += ch
499
+ currentSpan.width += 1
500
+ } else {
501
+ if (currentSpan) spans.push(currentSpan)
502
+ currentSpan = { text: ch, fg, bg, attributes: attr, width: 1 }
503
+ }
504
+ }
505
+
506
+ if (currentSpan) spans.push(currentSpan)
507
+ lines.push({ spans })
508
+ }
509
+
510
+ return lines
511
+ }
512
+
513
+ // Draw a text buffer view into the buffer
514
+ drawTextBufferView(view: any, x: number, y: number): void {
515
+ if (!view || !view.getVisibleLines) return
516
+
517
+ const lines = view.getVisibleLines()
518
+ if (!lines) return
519
+
520
+ const textAlign = view.textAlign as string | undefined
521
+ const viewWidth = view._viewportWidth as number | undefined
522
+
523
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
524
+ const line = lines[lineIdx]
525
+ if (!line) continue
526
+
527
+ let curX = x
528
+ if (textAlign && textAlign !== "left" && viewWidth) {
529
+ const lineWidth = line.chunks.reduce((sum: number, c: any) => sum + c.text.length, 0)
530
+ if (textAlign === "center") {
531
+ curX = x + Math.floor((viewWidth - lineWidth) / 2)
532
+ } else if (textAlign === "right") {
533
+ curX = x + viewWidth - lineWidth
534
+ }
535
+ }
536
+ for (const chunk of line.chunks) {
537
+ const text = chunk.text
538
+ const fgColor = chunk.fg
539
+ const bgColor = chunk.bg
540
+ let attr = chunk.attributes ?? 0
541
+
542
+ // Encode link ID into attributes if chunk has a link
543
+ if (chunk.link && chunk.link.url) {
544
+ const linkId = this.registerLink(chunk.link.url)
545
+ attr = attributesWithLink(attr, linkId)
546
+ }
547
+
548
+ for (const ch of text) {
549
+ if (curX >= this._width) break
550
+ if (curX >= 0 && y + lineIdx >= 0 && y + lineIdx < this._height) {
551
+ this.setCell(curX, y + lineIdx, ch, fgColor, bgColor, attr)
552
+ }
553
+ curX++
554
+ }
555
+ }
556
+ }
557
+ }
558
+
559
+ drawTextBuffer(textBufferView: any, x: number, y: number): void {
560
+ this.drawTextBufferView(textBufferView, x, y)
561
+ }
562
+
563
+ drawFrameBuffer(
564
+ destX: number,
565
+ destY: number,
566
+ frameBuffer: BrowserBuffer,
567
+ sourceX: number = 0,
568
+ sourceY: number = 0,
569
+ sourceWidth?: number,
570
+ sourceHeight?: number,
571
+ ): void {
572
+ const sw = sourceWidth ?? frameBuffer.width
573
+ const sh = sourceHeight ?? frameBuffer.height
574
+ const srcChar = frameBuffer.char
575
+ const srcFg = frameBuffer.fg
576
+ const srcBg = frameBuffer.bg
577
+ const srcAttr = frameBuffer.attributes
578
+ const srcCols = frameBuffer.width
579
+
580
+ for (let row = 0; row < sh; row++) {
581
+ const srcRow = sourceY + row
582
+ const dstRow = destY + row
583
+ if (srcRow < 0 || srcRow >= frameBuffer.height) continue
584
+ if (dstRow < 0 || dstRow >= this._height) continue
585
+
586
+ for (let col = 0; col < sw; col++) {
587
+ const srcCol = sourceX + col
588
+ const dstCol = destX + col
589
+ if (srcCol < 0 || srcCol >= frameBuffer.width) continue
590
+ if (dstCol < 0 || dstCol >= this._width) continue
591
+ if (!this.isInScissor(dstCol, dstRow)) continue
592
+
593
+ const srcIdx = srcRow * srcCols + srcCol
594
+ const dstIdx = dstRow * this._width + dstCol
595
+ const srcOffset = srcIdx * 4
596
+ const dstOffset = dstIdx * 4
597
+
598
+ this.char[dstIdx] = srcChar[srcIdx]
599
+ this.attributes[dstIdx] = srcAttr[srcIdx]
600
+
601
+ // Apply opacity to fg
602
+ const fgA = srcFg[srcOffset + 3]
603
+ const opacityMul = this.getCurrentOpacityMultiplier()
604
+ this.fg[dstOffset] = srcFg[srcOffset]
605
+ this.fg[dstOffset + 1] = srcFg[srcOffset + 1]
606
+ this.fg[dstOffset + 2] = srcFg[srcOffset + 2]
607
+ this.fg[dstOffset + 3] = fgA * opacityMul
608
+
609
+ // Apply opacity to bg
610
+ const bgA = srcBg[srcOffset + 3]
611
+ this.bg[dstOffset] = srcBg[srcOffset]
612
+ this.bg[dstOffset + 1] = srcBg[srcOffset + 1]
613
+ this.bg[dstOffset + 2] = srcBg[srcOffset + 2]
614
+ this.bg[dstOffset + 3] = bgA * opacityMul
615
+ }
616
+ }
617
+ }
618
+ drawEditorView(editorView: any, x: number, y: number): void {
619
+ if (!editorView) return
620
+
621
+ const viewport = editorView.getViewport()
622
+ const text = editorView.getText()
623
+ const lines = text.split("\n")
624
+
625
+ // Default colors
626
+ const dfg = editorView.editBuffer?._defaultFg ?? {
627
+ r: 1, g: 1, b: 1, a: 1,
628
+ buffer: new Float32Array([1, 1, 1, 1]),
629
+ } as RGBA
630
+ const dbg = editorView.editBuffer?._defaultBg ?? {
631
+ r: 0, g: 0, b: 0, a: 0,
632
+ buffer: new Float32Array([0, 0, 0, 0]),
633
+ } as RGBA
634
+
635
+ const visibleRows = viewport.height > 0 ? viewport.height : this._height - y
636
+
637
+ if (text === "" && editorView._placeholderChunks && editorView._placeholderChunks.length > 0) {
638
+ // Draw placeholder text; offset by 1 cell for line cursor so it doesn't overlap
639
+ let curX = this.cursorStyleType === "line" ? x + 1 : x
640
+ for (const chunk of editorView._placeholderChunks) {
641
+ const chunkFg = chunk.fg ?? dfg
642
+ const chunkBg = chunk.bg ?? dbg
643
+ const attr = (chunk.attributes ?? 0) | 2 // DIM attribute = 1 << 1
644
+ for (const ch of chunk.text) {
645
+ if (curX >= this._width) break
646
+ if (curX >= 0 && y >= 0 && y < this._height) {
647
+ this.setCell(curX, y, ch, chunkFg, chunkBg, attr)
648
+ }
649
+ curX++
650
+ }
651
+ }
652
+ } else {
653
+ // Draw text lines
654
+ for (let row = 0; row < visibleRows; row++) {
655
+ const lineIdx = viewport.offsetY + row
656
+ if (lineIdx < 0 || lineIdx >= lines.length) continue
657
+ const dstRow = y + row
658
+ if (dstRow < 0 || dstRow >= this._height) continue
659
+
660
+ const line = lines[lineIdx]
661
+ for (let col = 0; col < line.length; col++) {
662
+ const srcCol = viewport.offsetX + col
663
+ if (srcCol < 0 || srcCol >= line.length) continue
664
+ const dstCol = x + col
665
+ if (dstCol < 0 || dstCol >= this._width) break
666
+
667
+ this.setCell(dstCol, dstRow, line[srcCol], dfg, dbg, 0)
668
+ }
669
+ }
670
+ }
671
+
672
+ // Draw cursor
673
+ const cursor = editorView.getVisualCursor()
674
+ if (cursor) {
675
+ const cursorX = x + cursor.visualCol
676
+ const cursorY = y + cursor.visualRow
677
+ if (cursorX >= 0 && cursorX < this._width && cursorY >= 0 && cursorY < this._height) {
678
+ const cursorFg = this.cursorColor ?? dfg
679
+ if (this.cursorStyleType === "line") {
680
+ // Line cursor: store position for renderer to build cursor overlay
681
+ this.lineCursorPosition = { x: cursorX, y: cursorY }
682
+ } else {
683
+ // Block cursor: overwrite cell with INVERSE
684
+ const idx = cursorY * this._width + cursorX
685
+ const charCode = this.char[idx]
686
+ const ch = charCode === 0 || charCode === 0x20 ? " " : String.fromCodePoint(charCode)
687
+ const offset = idx * 4
688
+ const cellBg = {
689
+ r: this.bg[offset],
690
+ g: this.bg[offset + 1],
691
+ b: this.bg[offset + 2],
692
+ a: this.bg[offset + 3] || 1,
693
+ } as RGBA
694
+ this.setCell(cursorX, cursorY, ch, cursorFg, cellBg, 32) // INVERSE = 1 << 5 = 32
695
+ }
696
+ }
697
+ }
698
+ }
699
+ drawSuperSampleBuffer(): void {}
700
+ drawPackedBuffer(): void {}
701
+ drawGrayscaleBuffer(): void {}
702
+ drawGrayscaleBufferSupersampled(): void {}
703
+ drawGrid(): void {}
704
+ encodeUnicode(_text: string): null {
705
+ return null
706
+ }
707
+ freeUnicode(): void {}
708
+ getRealCharBytes(): Uint8Array {
709
+ return new Uint8Array(0)
710
+ }
711
+ destroy(): void {}
712
+ }
713
+
714
+ // Export as OptimizedBuffer so opentui source code imports work directly
715
+ export { BrowserBuffer as OptimizedBuffer }