@vibecook/ghosttea-react 0.7.0 → 0.9.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 (73) hide show
  1. package/README.md +10 -0
  2. package/THIRD_PARTY_NOTICES.md +62 -0
  3. package/dist/appearance/AdvancedConfigSettings.d.ts +12 -0
  4. package/dist/appearance/AdvancedConfigSettings.d.ts.map +1 -0
  5. package/dist/appearance/AdvancedConfigSettings.js +362 -0
  6. package/dist/appearance/AdvancedConfigSettings.js.map +1 -0
  7. package/dist/appearance/AppearanceSettings.d.ts +21 -0
  8. package/dist/appearance/AppearanceSettings.d.ts.map +1 -0
  9. package/dist/appearance/AppearanceSettings.js +175 -0
  10. package/dist/appearance/AppearanceSettings.js.map +1 -0
  11. package/dist/appearance/catalog.d.ts +39 -0
  12. package/dist/appearance/catalog.d.ts.map +1 -0
  13. package/dist/appearance/catalog.js +56 -0
  14. package/dist/appearance/catalog.js.map +1 -0
  15. package/dist/appearance/config-draft.d.ts +35 -0
  16. package/dist/appearance/config-draft.d.ts.map +1 -0
  17. package/dist/appearance/config-draft.js +289 -0
  18. package/dist/appearance/config-draft.js.map +1 -0
  19. package/dist/appearance/shaders.d.ts +14 -0
  20. package/dist/appearance/shaders.d.ts.map +1 -0
  21. package/dist/appearance/shaders.js +71 -0
  22. package/dist/appearance/shaders.js.map +1 -0
  23. package/dist/appearance/theme-catalog.generated.json +16260 -0
  24. package/dist/appearance/types.d.ts +61 -0
  25. package/dist/appearance/types.d.ts.map +1 -0
  26. package/dist/appearance/types.js +2 -0
  27. package/dist/appearance/types.js.map +1 -0
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +23 -4
  30. package/dist/config.js.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/renderers/canvas-renderer.d.ts.map +1 -1
  36. package/dist/renderers/canvas-renderer.js +9 -1
  37. package/dist/renderers/canvas-renderer.js.map +1 -1
  38. package/dist/renderers/shader-effects.d.ts +2 -0
  39. package/dist/renderers/shader-effects.d.ts.map +1 -0
  40. package/dist/renderers/shader-effects.js +206 -0
  41. package/dist/renderers/shader-effects.js.map +1 -0
  42. package/dist/renderers/types.d.ts +7 -0
  43. package/dist/renderers/types.d.ts.map +1 -1
  44. package/dist/renderers/types.js +11 -0
  45. package/dist/renderers/types.js.map +1 -1
  46. package/dist/renderers/webgpu-renderer.d.ts +2 -1
  47. package/dist/renderers/webgpu-renderer.d.ts.map +1 -1
  48. package/dist/renderers/webgpu-renderer.js +250 -159
  49. package/dist/renderers/webgpu-renderer.js.map +1 -1
  50. package/dist/terminal-render.worker.js +479 -165
  51. package/dist/terminal-render.worker.js.map +3 -3
  52. package/dist/workspace/Workspace.d.ts +32 -4
  53. package/dist/workspace/Workspace.d.ts.map +1 -1
  54. package/dist/workspace/Workspace.js +45 -17
  55. package/dist/workspace/Workspace.js.map +1 -1
  56. package/dist/workspace/index.d.ts +5 -2
  57. package/dist/workspace/index.d.ts.map +1 -1
  58. package/dist/workspace/index.js +3 -1
  59. package/dist/workspace/index.js.map +1 -1
  60. package/dist/workspace/pane-layout.d.ts +9 -2
  61. package/dist/workspace/pane-layout.d.ts.map +1 -1
  62. package/dist/workspace/pane-layout.js +37 -11
  63. package/dist/workspace/pane-layout.js.map +1 -1
  64. package/dist/workspace/themes.d.ts +4 -0
  65. package/dist/workspace/themes.d.ts.map +1 -1
  66. package/dist/workspace/themes.js +4 -0
  67. package/dist/workspace/themes.js.map +1 -1
  68. package/dist/workspace/workspace-model.d.ts +2 -0
  69. package/dist/workspace/workspace-model.d.ts.map +1 -1
  70. package/dist/workspace/workspace-model.js +6 -1
  71. package/dist/workspace/workspace-model.js.map +1 -1
  72. package/dist/workspace.css +1122 -5
  73. package/package.json +6 -5
@@ -254,6 +254,13 @@ function emptyRenderMetrics() {
254
254
  }
255
255
 
256
256
  // src/renderers/types.ts
257
+ var ANIMATED_SHADER_EFFECTS = /* @__PURE__ */ new Set([
258
+ "ghosttea:vhs",
259
+ "ghosttea:sparks-from-fire"
260
+ ]);
261
+ function terminalEffectsNeedAnimation(effects) {
262
+ return effects.animate === true && (effects.shaderEffects?.some((effect) => ANIMATED_SHADER_EFFECTS.has(effect)) ?? false);
263
+ }
257
264
  function effectiveCursorStyle(view) {
258
265
  if (!view.cursor.visible) return null;
259
266
  if (!view.focused) return CursorStyle.HollowBlock;
@@ -270,11 +277,15 @@ var DEFAULT_THEME = {
270
277
  background: [40 / 255, 44 / 255, 52 / 255, 1],
271
278
  foreground: [1, 1, 1, 1],
272
279
  cursor: [1, 1, 1, 1],
280
+ cursorText: [40 / 255, 44 / 255, 52 / 255, 1],
273
281
  selection: [1, 1, 1, 1],
274
- selectionForeground: [40 / 255, 44 / 255, 52 / 255, 1]
282
+ selectionForeground: [40 / 255, 44 / 255, 52 / 255, 1],
283
+ backgroundOpacityCells: false
275
284
  };
276
285
  var DEFAULT_EFFECTS = {
277
- postProcess: "none"
286
+ postProcess: "none",
287
+ shaderEffects: [],
288
+ animate: false
278
289
  };
279
290
  var CELL_WIDTH = 7.83;
280
291
  var LINE_HEIGHT = 19;
@@ -299,7 +310,7 @@ var CanvasTerminalRenderer = class {
299
310
  this.#performanceMeasurementEnabled = enabled;
300
311
  }
301
312
  mount(id, canvas) {
302
- const context = canvas.getContext("2d", { alpha: false });
313
+ const context = canvas.getContext("2d", { alpha: true });
303
314
  if (!context) throw new Error("Canvas2D worker backend unavailable");
304
315
  this.#surfaces.set(id, { canvas, context, width: 1, height: 1, dpr: 1 });
305
316
  }
@@ -319,6 +330,7 @@ var CanvasTerminalRenderer = class {
319
330
  const surface2 = this.#surfaces.get(id);
320
331
  if (!surface2) return;
321
332
  const { context, width, height, dpr } = surface2;
333
+ context.clearRect(0, 0, surface2.canvas.width, surface2.canvas.height);
322
334
  context.save();
323
335
  context.setTransform(dpr, 0, 0, dpr, 0, 0);
324
336
  context.fillStyle = css(view.theme.background);
@@ -379,6 +391,13 @@ var CanvasTerminalRenderer = class {
379
391
  context.strokeRect(x + 0.5, y + 0.5, CELL_WIDTH - 1, LINE_HEIGHT - 1);
380
392
  } else {
381
393
  context.fillRect(x, y, CELL_WIDTH, LINE_HEIGHT);
394
+ context.save();
395
+ context.beginPath();
396
+ context.rect(x, y, CELL_WIDTH, LINE_HEIGHT);
397
+ context.clip();
398
+ context.fillStyle = css(view.theme.cursorText ?? view.theme.background);
399
+ context.fillText(view.rows[view.cursor.y] ?? "", ORIGIN_X, ORIGIN_Y + view.cursor.y * LINE_HEIGHT);
400
+ context.restore();
382
401
  }
383
402
  }
384
403
  context.restore();
@@ -416,10 +435,226 @@ function rowsForDamage(rowCount, damage, sceneValid) {
416
435
  return { full: false, rows: [...rows].sort((left, right) => left - right) };
417
436
  }
418
437
 
438
+ // src/renderers/shader-effects.ts
439
+ var SHADER_EFFECT_WGSL = (
440
+ /* wgsl */
441
+ `
442
+ @group(0) @binding(0) var terminal_image: texture_2d<f32>;
443
+ @group(0) @binding(1) var terminal_sampler: sampler;
444
+
445
+ struct EffectConfig {
446
+ mode: u32,
447
+ frame: u32,
448
+ effect_index: u32,
449
+ effect_count: u32,
450
+ resolution: vec2f,
451
+ time: f32,
452
+ time_delta: f32,
453
+ cursor: vec4f,
454
+ }
455
+ @group(0) @binding(2) var<uniform> effect: EffectConfig;
456
+
457
+ @vertex fn vertex_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4f {
458
+ let positions = array<vec2f, 3>(
459
+ vec2f(-1.0, -1.0),
460
+ vec2f(3.0, -1.0),
461
+ vec2f(-1.0, 3.0),
462
+ );
463
+ return vec4f(positions[vertex_index], 0.0, 1.0);
464
+ }
465
+
466
+ fn sample_image(uv: vec2f) -> vec4f {
467
+ return textureSample(terminal_image, terminal_sampler, clamp(uv, vec2f(0.0), vec2f(1.0)));
468
+ }
469
+
470
+ fn straight_rgb(sampled: vec4f) -> vec3f {
471
+ if (sampled.a <= 0.00001) { return vec3f(0.0); }
472
+ return sampled.rgb / sampled.a;
473
+ }
474
+
475
+ fn premultiplied(rgb: vec3f, alpha: f32) -> vec4f {
476
+ return vec4f(max(rgb, vec3f(0.0)) * alpha, alpha);
477
+ }
478
+
479
+ fn hash21(p0: vec2f) -> f32 {
480
+ var p = fract(p0 * vec2f(443.8975, 397.2973));
481
+ p += dot(p, p.yx + vec2f(19.19));
482
+ return fract(p.x * p.y);
483
+ }
484
+
485
+ // Existing Better CRT compatibility port. Its source carries the upstream
486
+ // Shadertoy default CC BY-NC-SA terms; this mode preserves the already-bundled
487
+ // behavior while keeping alpha intact.
488
+ fn better_crt(position: vec2f, uv0: vec2f) -> vec4f {
489
+ var uv = uv0;
490
+ var dc = abs(vec2f(0.5) - uv);
491
+ dc *= dc;
492
+ uv.x = (uv.x - 0.5) * (1.0 + dc.y * 0.075) + 0.5;
493
+ uv.y = (uv.y - 0.5) * (1.0 + dc.x * 0.10) + 0.5;
494
+ let scanline = abs(sin(position.y) * 0.125);
495
+ let sampled = sample_image(uv);
496
+ return vec4f(sampled.rgb * (1.0 - scanline), sampled.a);
497
+ }
498
+
499
+ fn from_srgb1(value: f32) -> f32 {
500
+ return select(pow(value / 1.055 + 0.055 / 1.055, 2.4), value / 12.92, value <= 0.04045);
501
+ }
502
+
503
+ fn to_srgb1(value: f32) -> f32 {
504
+ return select(1.055 * pow(max(value, 0.0), 0.41666) - 0.055, value * 12.92, value < 0.0031308);
505
+ }
506
+
507
+ fn from_srgb(value: vec3f) -> vec3f {
508
+ return vec3f(from_srgb1(value.r), from_srgb1(value.g), from_srgb1(value.b));
509
+ }
510
+
511
+ fn to_srgb(value: vec3f) -> vec3f {
512
+ return vec3f(to_srgb1(value.r), to_srgb1(value.g), to_srgb1(value.b));
513
+ }
514
+
515
+ // WebGPU adaptation of Timothy Lottes' public-domain CRT scalar. The source
516
+ // port is Unlicensed/public domain. This retains its warp, scan blur, shadow
517
+ // mask, linear-light processing, and corner vignette in a compact GPU form.
518
+ fn public_domain_crt(position: vec2f, uv0: vec2f) -> vec4f {
519
+ let aspect = effect.resolution.x / max(1.0, effect.resolution.y);
520
+ var centered = uv0 * 2.0 - 1.0;
521
+ centered *= vec2f(
522
+ 1.0 + centered.y * centered.y / (50.0 * aspect),
523
+ 1.0 + centered.x * centered.x / 50.0,
524
+ );
525
+ let uv = centered * 0.5 + 0.5;
526
+ let pixel = 1.0 / effect.resolution;
527
+ let center_sample = sample_image(uv);
528
+ let left = from_srgb(straight_rgb(sample_image(uv - vec2f(pixel.x, 0.0))));
529
+ let center = from_srgb(straight_rgb(center_sample));
530
+ let right = from_srgb(straight_rgb(sample_image(uv + vec2f(pixel.x, 0.0))));
531
+ var color = left * 0.18 + center * 0.64 + right * 0.18;
532
+ let scan = 0.58 + 0.42 * pow(max(0.0, cos(position.y * 2.0943951)), 2.0);
533
+ let mask_index = u32(position.x + position.y * 3.0) % 6u;
534
+ var mask = vec3f(0.65);
535
+ if (mask_index < 2u) { mask.r = 1.0; }
536
+ else if (mask_index < 4u) { mask.g = 1.0; }
537
+ else { mask.b = 1.0; }
538
+ color *= scan * mask;
539
+ let vignette_axis = clamp(1.0 - centered * centered, vec2f(0.0), vec2f(1.0));
540
+ color *= mix(0.5, 1.0, vignette_axis.x * vignette_axis.y);
541
+ let peak = max(0.00001, max(color.r, max(color.g, color.b)));
542
+ let tone = peak / (peak * 0.72 + 0.28);
543
+ color *= tone / peak;
544
+ let inside = all(uv >= vec2f(0.0)) && all(uv <= vec2f(1.0));
545
+ let alpha = select(0.0, center_sample.a, inside);
546
+ return premultiplied(to_srgb(color), alpha);
547
+ }
548
+
549
+ // MIT-licensed VHS effect by Alex Brinsmead, adapted from the upstream GLSL
550
+ // to premultiplied WebGPU output.
551
+ fn vhs(position: vec2f, uv0: vec2f) -> vec4f {
552
+ let distortion = 1.0;
553
+ let tape_wear = 1.75;
554
+ let color_bleed = 2.0;
555
+ var cc = uv0 - 0.5;
556
+ var warped = clamp(uv0 + cc * (dot(cc, cc) * 0.02), vec2f(0.0), vec2f(1.0));
557
+ warped.x += (
558
+ sin(warped.y * 40.0 + effect.time * 0.5) * 0.000375 +
559
+ sin(warped.y * 80.0 + effect.time * 0.2) * 0.0001875 +
560
+ (hash21(vec2f(floor(position.y), floor(effect.time * 20.0))) - 0.5) * 0.0006
561
+ ) * distortion;
562
+ let tracking_y = fract(effect.time * 0.04);
563
+ let band_distance = min(abs(warped.y - tracking_y), 1.0 - abs(warped.y - tracking_y));
564
+ let band = smoothstep(0.05, 0.0, band_distance);
565
+ let horizontal_offset = band * (hash21(vec2f(floor(position.y * 0.5), effect.time * 3.0)) - 0.5) * 0.006;
566
+ let burst_cycle = floor(effect.time / 30.0);
567
+ let burst_start = hash21(vec2f(burst_cycle, 88.0)) * 25.0;
568
+ let burst_time = effect.time - floor(effect.time / 30.0) * 30.0;
569
+ let burst = burst_time > burst_start && burst_time < burst_start + 5.0;
570
+ let jump = select(0.0, (hash21(vec2f(floor(effect.time * 2.0), 11.3)) - 0.5) * 0.08, burst);
571
+ let sample_uv = fract(warped + vec2f(horizontal_offset, jump));
572
+ let aberration = (0.0003 + band * 0.0015) * color_bleed;
573
+ let center_sample = sample_image(sample_uv);
574
+ let red_sample = straight_rgb(sample_image(sample_uv + vec2f(aberration, 0.0)));
575
+ let blue_sample = straight_rgb(sample_image(sample_uv - vec2f(aberration, 0.0)));
576
+ var color = vec3f(red_sample.r, straight_rgb(center_sample).g, blue_sample.b);
577
+ let pixel = 1.0 / effect.resolution.x;
578
+ let neighbor = straight_rgb(sample_image(sample_uv + vec2f(pixel, 0.0))) * 0.4 +
579
+ straight_rgb(sample_image(sample_uv + vec2f(pixel * 3.0, 0.0))) * 0.6;
580
+ let luma_weights = vec3f(0.299, 0.587, 0.114);
581
+ let luma = dot(color, luma_weights);
582
+ color = vec3f(luma) + mix(color - vec3f(luma), neighbor - vec3f(dot(neighbor, luma_weights)), 0.5 * color_bleed);
583
+ color *= 1.8;
584
+ let grain_time = fract(effect.time);
585
+ let noise1 = hash21(position + vec2f(grain_time * 100.0)) - 0.5;
586
+ let noise2 = hash21(position + vec2f(grain_time * 200.0)) - 0.5;
587
+ let gray = dot(color, luma_weights);
588
+ color += (color - vec3f(gray)) * vec3f(noise1, noise2, -noise1) * 0.15 * tape_wear;
589
+ color += vec3f(noise1 * 0.05 * tape_wear);
590
+ color *= mix(vec3f(1.0), vec3f(1.03, 1.01, 0.96), tape_wear);
591
+ let vignette = uv0 * (1.0 - uv0);
592
+ color *= mix(1.0, clamp(vignette.x * vignette.y * 15.0, 0.0, 1.0), 0.35);
593
+ let static_time = fract(effect.time * 20.0);
594
+ let static_hash = hash21(vec2f(floor(position.x / 10.0), floor(position.y / 2.0) + static_time * 500.0) + vec2f(static_time * 77.0));
595
+ color = mix(color, vec3f(1.0), step(0.9991, static_hash) * 0.135);
596
+ return premultiplied(color, center_sample.a);
597
+ }
598
+
599
+ // Ember overlay inspired by Jan Mr\xF3z's CC BY 3.0 shader, adapted for the
600
+ // Ghostty pipeline. Attribution is retained in THIRD_PARTY_NOTICES.md.
601
+ fn sparks(position: vec2f, uv: vec2f) -> vec4f {
602
+ let terminal = sample_image(uv);
603
+ var spark = vec3f(0.0);
604
+ var spark_alpha = 0.0;
605
+ for (var layer = 0u; layer < 8u; layer += 1u) {
606
+ let layer_value = f32(layer);
607
+ let scale = 18.0 + layer_value * 7.0;
608
+ let drift = vec2f(effect.time * (0.35 + layer_value * 0.025), -effect.time * (0.7 + layer_value * 0.05));
609
+ let grid = uv * vec2f(scale, scale * 1.6) + drift;
610
+ let cell = floor(grid);
611
+ let random = hash21(cell + vec2f(layer_value * 13.7, layer_value * 5.1));
612
+ let local = fract(grid) - vec2f(random, fract(random * 17.17));
613
+ let radius = 0.018 + 0.045 * hash21(cell + vec2f(31.0));
614
+ let intensity = smoothstep(radius, 0.0, length(local)) * step(0.79, random);
615
+ let flicker = 0.55 + 0.45 * sin(effect.time * (5.0 + random * 4.0) + random * 31.0);
616
+ let value = intensity * flicker;
617
+ spark += vec3f(1.5, 0.42, 0.05) * value;
618
+ spark_alpha = max(spark_alpha, value * 0.78);
619
+ }
620
+ let terminal_rgb = straight_rgb(terminal);
621
+ let background_weight = 1.0 - smoothstep(0.12, 0.55, dot(terminal_rgb, vec3f(0.2126, 0.7152, 0.0722)));
622
+ let overlay_alpha = spark_alpha * background_weight;
623
+ let alpha = overlay_alpha + terminal.a * (1.0 - overlay_alpha);
624
+ let rgb = select(
625
+ vec3f(0.0),
626
+ (spark * overlay_alpha + terminal_rgb * terminal.a * (1.0 - overlay_alpha)) / max(alpha, 0.00001),
627
+ alpha > 0.00001,
628
+ );
629
+ return premultiplied(rgb, alpha);
630
+ }
631
+
632
+ @fragment fn fragment_main(@builtin(position) position: vec4f) -> @location(0) vec4f {
633
+ let uv = position.xy / effect.resolution;
634
+ switch effect.mode {
635
+ case 1u: { return better_crt(position.xy, uv); }
636
+ case 2u: { return public_domain_crt(position.xy, uv); }
637
+ case 3u: { return vhs(position.xy, uv); }
638
+ case 4u: { return sparks(position.xy, uv); }
639
+ default: { return sample_image(uv); }
640
+ }
641
+ }
642
+ `
643
+ );
644
+
419
645
  // src/renderers/webgpu-renderer.ts
420
646
  var ATLAS_SIZE = 2048;
421
647
  var GEOMETRY_CACHE_LIMIT = 8;
422
648
  var FONT_STACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
649
+ var EFFECT_MODES = {
650
+ "ghosttea:better-crt": 1,
651
+ "ghosttea:crt": 2,
652
+ "ghosttea:vhs": 3,
653
+ "ghosttea:sparks-from-fire": 4
654
+ };
655
+ function effectIntermediateTextureCount(effectPassCount) {
656
+ return Math.min(2, Math.max(0, Math.trunc(effectPassCount) - 1));
657
+ }
423
658
  var RECT_SHADER = (
424
659
  /* wgsl */
425
660
  `
@@ -553,54 +788,13 @@ struct VertexOutput {
553
788
  }
554
789
  `
555
790
  );
556
- var BETTER_CRT_SHADER = (
557
- /* wgsl */
558
- `
559
- @group(0) @binding(0) var terminal_image: texture_2d<f32>;
560
- @group(0) @binding(1) var terminal_sampler: sampler;
561
- struct PostProcessConfig {
562
- // A vec4 keeps the WGSL uniform layout equal to the host's 16-byte buffer.
563
- enabled: vec4u,
564
- }
565
- @group(0) @binding(2) var<uniform> post_process: PostProcessConfig;
566
-
567
- @vertex fn vertex_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4f {
568
- let positions = array<vec2f, 3>(
569
- vec2f(-1.0, -1.0),
570
- vec2f(3.0, -1.0),
571
- vec2f(-1.0, 3.0),
572
- );
573
- return vec4f(positions[vertex_index], 0.0, 1.0);
574
- }
575
-
576
- @fragment fn fragment_main(@builtin(position) position: vec4f) -> @location(0) vec4f {
577
- let dimensions = vec2f(textureDimensions(terminal_image));
578
- var uv = position.xy / dimensions;
579
- if (post_process.enabled.x == 0u) {
580
- return textureSample(terminal_image, terminal_sampler, uv);
581
- }
582
- var dc = abs(vec2f(0.5) - uv);
583
- dc *= dc;
584
-
585
- // These constants are the active Ghostty shader's warp = 0.25 and
586
- // scan = 0.50 values.
587
- uv.x -= 0.5;
588
- uv.x *= 1.0 + dc.y * 0.075;
589
- uv.x += 0.5;
590
- uv.y -= 0.5;
591
- uv.y *= 1.0 + dc.x * 0.10;
592
- uv.y += 0.5;
593
-
594
- let scanline = abs(sin(position.y) * 0.125);
595
- let color = textureSample(terminal_image, terminal_sampler, uv).rgb;
596
- return vec4f(mix(color, vec3f(0.0), scanline), 1.0);
597
- }
598
- `
599
- );
600
791
  var premultipliedBlend = {
601
792
  color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
602
793
  alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
603
794
  };
795
+ function premultipliedClear(color) {
796
+ return { r: color[0] * color[3], g: color[1] * color[3], b: color[2] * color[3], a: color[3] };
797
+ }
604
798
  var DynamicVertexBuffer = class {
605
799
  constructor(device, label) {
606
800
  this.device = device;
@@ -890,16 +1084,20 @@ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
890
1084
  [
891
1085
  ...view.theme.background,
892
1086
  ...view.theme.foreground,
1087
+ ...view.theme.cursor,
1088
+ ...view.theme.cursorText ?? view.theme.background,
893
1089
  ...view.theme.selection,
894
1090
  ...view.theme.selectionForeground
895
1091
  ].join(","),
1092
+ view.theme.backgroundOpacityCells ? "opacity-cells" : "opaque-cells",
896
1093
  selection,
1094
+ effectiveCursorStyle(view) === CursorStyle.Block ? `${view.cursor.x},${view.cursor.y}` : "-",
897
1095
  atlasGenerations.join(",")
898
1096
  ].join("|");
899
1097
  }
900
1098
  function pushCursorVertices(vertices, view, renderRowSet, scale, viewportWidth, viewportHeight) {
901
1099
  const cursorStyle = effectiveCursorStyle(view);
902
- if (cursorStyle === null || !renderRowSet.has(view.cursor.y)) return;
1100
+ if (cursorStyle === null || cursorStyle === CursorStyle.Block || !renderRowSet.has(view.cursor.y)) return;
903
1101
  const x = (ORIGIN_X + view.cursor.x * CELL_WIDTH) * scale;
904
1102
  const y = (ORIGIN_Y + view.cursor.y * LINE_HEIGHT) * scale;
905
1103
  const width = CELL_WIDTH * scale;
@@ -1090,6 +1288,22 @@ function selectionContains(selection, row, column) {
1090
1288
  const last = row === end.row ? end.column : Number.MAX_SAFE_INTEGER;
1091
1289
  return column >= first && column <= last;
1092
1290
  }
1291
+ function blockCursorContains(view, row, column, span = 1) {
1292
+ return effectiveCursorStyle(view) === CursorStyle.Block && view.cursor.y === row && view.cursor.x >= column && view.cursor.x < column + Math.max(1, span);
1293
+ }
1294
+ function pushBlockCursorBackground(vertices, view, renderRows, scale, viewportWidth, viewportHeight) {
1295
+ if (effectiveCursorStyle(view) !== CursorStyle.Block || !renderRows.includes(view.cursor.y)) return;
1296
+ pushRectangle(
1297
+ vertices,
1298
+ (ORIGIN_X + view.cursor.x * CELL_WIDTH) * scale,
1299
+ (ORIGIN_Y + view.cursor.y * LINE_HEIGHT) * scale,
1300
+ CELL_WIDTH * scale,
1301
+ LINE_HEIGHT * scale,
1302
+ view.theme.cursor,
1303
+ viewportWidth,
1304
+ viewportHeight
1305
+ );
1306
+ }
1093
1307
  function resolveStyle(style, theme) {
1094
1308
  let foreground = style?.foreground ? rgb(style.foreground) : theme.foreground;
1095
1309
  let background = style?.background ? rgb(style.background) : null;
@@ -1098,6 +1312,9 @@ function resolveStyle(style, theme) {
1098
1312
  foreground = background ?? theme.background;
1099
1313
  background = originalForeground;
1100
1314
  }
1315
+ if (background && theme.backgroundOpacityCells) {
1316
+ background = [background[0], background[1], background[2], theme.background[3]];
1317
+ }
1101
1318
  if (style?.faint) foreground = [foreground[0], foreground[1], foreground[2], foreground[3] * 0.55];
1102
1319
  return {
1103
1320
  foreground,
@@ -1241,7 +1458,10 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1241
1458
  label: "terminal color glyph shader",
1242
1459
  code: COLOR_GLYPH_SHADER
1243
1460
  });
1244
- const postProcessModule = device.createShaderModule({ label: "Ghostty bettercrt shader", code: BETTER_CRT_SHADER });
1461
+ const effectModule = device.createShaderModule({
1462
+ label: "Ghosttea shader effect registry",
1463
+ code: SHADER_EFFECT_WGSL
1464
+ });
1245
1465
  this.#rectanglePipeline = device.createRenderPipeline({
1246
1466
  label: "terminal rectangle pipeline",
1247
1467
  layout: "auto",
@@ -1268,6 +1488,28 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1268
1488
  },
1269
1489
  primitive: { topology: "triangle-list" }
1270
1490
  });
1491
+ this.#rectangleOverwritePipeline = device.createRenderPipeline({
1492
+ label: "terminal row-reset overwrite pipeline",
1493
+ layout: "auto",
1494
+ vertex: {
1495
+ module: rectangleModule,
1496
+ entryPoint: "vertex_main",
1497
+ buffers: [
1498
+ {
1499
+ arrayStride: 40,
1500
+ stepMode: "instance",
1501
+ attributes: [
1502
+ { shaderLocation: 0, offset: 0, format: "float32x2" },
1503
+ { shaderLocation: 1, offset: 8, format: "float32x2" },
1504
+ { shaderLocation: 2, offset: 16, format: "float32x2" },
1505
+ { shaderLocation: 3, offset: 24, format: "float32x4" }
1506
+ ]
1507
+ }
1508
+ ]
1509
+ },
1510
+ fragment: { module: rectangleModule, entryPoint: "fragment_main", targets: [{ format }] },
1511
+ primitive: { topology: "triangle-list" }
1512
+ });
1271
1513
  this.#glyphPipeline = device.createRenderPipeline({
1272
1514
  label: "terminal glyph pipeline",
1273
1515
  layout: "auto",
@@ -1339,15 +1581,15 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1339
1581
  },
1340
1582
  primitive: { topology: "triangle-list" }
1341
1583
  });
1342
- this.#postProcessPipeline = device.createRenderPipeline({
1343
- label: "Ghostty custom shader pipeline",
1584
+ this.#effectPipeline = device.createRenderPipeline({
1585
+ label: "Ghosttea ordered shader effect pipeline",
1344
1586
  layout: "auto",
1345
- vertex: { module: postProcessModule, entryPoint: "vertex_main" },
1346
- fragment: { module: postProcessModule, entryPoint: "fragment_main", targets: [{ format }] },
1587
+ vertex: { module: effectModule, entryPoint: "vertex_main" },
1588
+ fragment: { module: effectModule, entryPoint: "fragment_main", targets: [{ format }] },
1347
1589
  primitive: { topology: "triangle-list" }
1348
1590
  });
1349
- this.#postProcessSampler = device.createSampler({
1350
- label: "Ghostty custom shader sampler",
1591
+ this.#effectSampler = device.createSampler({
1592
+ label: "Ghosttea shader effect sampler",
1351
1593
  addressModeU: "clamp-to-edge",
1352
1594
  addressModeV: "clamp-to-edge",
1353
1595
  minFilter: "linear",
@@ -1370,11 +1612,12 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1370
1612
  kind = "webgpu";
1371
1613
  #surfaces = /* @__PURE__ */ new Map();
1372
1614
  #rectanglePipeline;
1615
+ #rectangleOverwritePipeline;
1373
1616
  #glyphPipeline;
1374
1617
  #fallbackGlyphPipeline;
1375
1618
  #colorGlyphPipeline;
1376
- #postProcessPipeline;
1377
- #postProcessSampler;
1619
+ #effectPipeline;
1620
+ #effectSampler;
1378
1621
  #monoAtlas;
1379
1622
  #colorAtlas;
1380
1623
  #fallbackAtlas;
@@ -1399,18 +1642,15 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1399
1642
  const context = canvas.getContext("webgpu");
1400
1643
  if (!context) throw new Error("WebGPU canvas context unavailable");
1401
1644
  const sceneTexture = this.#createSceneTexture(canvas.width, canvas.height);
1402
- const postProcessConfigBuffer = this.device.createBuffer({
1403
- label: `Ghostty custom shader configuration ${id}`,
1404
- size: 16,
1405
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1406
- });
1407
1645
  const surface2 = {
1408
1646
  canvas,
1409
1647
  context,
1410
1648
  sceneTexture,
1411
- postProcessConfigBuffer,
1412
- postProcessBindGroup: this.#createPostProcessBindGroup(sceneTexture, postProcessConfigBuffer),
1413
- postProcessEnabled: void 0,
1649
+ effectTextures: [],
1650
+ effectPasses: [],
1651
+ effectSignature: "",
1652
+ effectFrame: 0,
1653
+ lastEffectTime: 0,
1414
1654
  width: 1,
1415
1655
  height: 1,
1416
1656
  dpr: 1,
@@ -1431,7 +1671,8 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1431
1671
  if (!surface2) return;
1432
1672
  surface2.context.unconfigure();
1433
1673
  surface2.sceneTexture.destroy();
1434
- surface2.postProcessConfigBuffer.destroy();
1674
+ for (const texture of surface2.effectTextures) texture.destroy();
1675
+ for (const pass of surface2.effectPasses) pass.configBuffer.destroy();
1435
1676
  surface2.rectangleBuffer.destroy();
1436
1677
  surface2.glyphBuffer.destroy();
1437
1678
  surface2.colorGlyphBuffer.destroy();
@@ -1453,11 +1694,12 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1453
1694
  #configure(surface2) {
1454
1695
  surface2.context.configure({ device: this.device, format: this.format, alphaMode: "premultiplied" });
1455
1696
  surface2.sceneTexture.destroy();
1697
+ for (const texture of surface2.effectTextures) texture.destroy();
1698
+ for (const pass of surface2.effectPasses) pass.configBuffer.destroy();
1456
1699
  surface2.sceneTexture = this.#createSceneTexture(surface2.canvas.width, surface2.canvas.height);
1457
- surface2.postProcessBindGroup = this.#createPostProcessBindGroup(
1458
- surface2.sceneTexture,
1459
- surface2.postProcessConfigBuffer
1460
- );
1700
+ surface2.effectTextures = [];
1701
+ surface2.effectPasses = [];
1702
+ surface2.effectSignature = "";
1461
1703
  clearGeometryCache(surface2);
1462
1704
  surface2.sceneValid = false;
1463
1705
  }
@@ -1469,29 +1711,116 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1469
1711
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
1470
1712
  });
1471
1713
  }
1472
- #createPostProcessBindGroup(texture, configBuffer) {
1714
+ #createEffectTexture(width, height, suffix) {
1715
+ return this.device.createTexture({
1716
+ label: `terminal shader ping-pong ${suffix}`,
1717
+ size: [Math.max(1, width), Math.max(1, height)],
1718
+ format: this.format,
1719
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
1720
+ });
1721
+ }
1722
+ #createEffectBindGroup(texture, configBuffer) {
1473
1723
  return this.device.createBindGroup({
1474
- label: "Ghostty custom shader bindings",
1475
- layout: this.#postProcessPipeline.getBindGroupLayout(0),
1724
+ label: "Ghosttea shader effect bindings",
1725
+ layout: this.#effectPipeline.getBindGroupLayout(0),
1476
1726
  entries: [
1477
1727
  { binding: 0, resource: texture.createView() },
1478
- { binding: 1, resource: this.#postProcessSampler },
1728
+ { binding: 1, resource: this.#effectSampler },
1479
1729
  { binding: 2, resource: { buffer: configBuffer } }
1480
1730
  ]
1481
1731
  });
1482
1732
  }
1733
+ #shaderStack(view) {
1734
+ const configured = view.effects?.shaderEffects?.filter((id) => EFFECT_MODES[id] !== void 0) ?? [];
1735
+ if (configured.length > 0) return [...configured];
1736
+ return view.effects?.postProcess === "better-crt" ? ["ghosttea:better-crt"] : [];
1737
+ }
1738
+ #ensureEffectPasses(surface2, stack) {
1739
+ const signature = stack.join("\0") || "ghosttea:blit";
1740
+ if (surface2.effectSignature === signature) return;
1741
+ for (const pass of surface2.effectPasses) pass.configBuffer.destroy();
1742
+ const modes = stack.length > 0 ? stack.map((id) => EFFECT_MODES[id]) : [0];
1743
+ const intermediateCount = effectIntermediateTextureCount(modes.length);
1744
+ while (surface2.effectTextures.length > intermediateCount) surface2.effectTextures.pop().destroy();
1745
+ while (surface2.effectTextures.length < intermediateCount) {
1746
+ const suffix = surface2.effectTextures.length === 0 ? "A" : "B";
1747
+ surface2.effectTextures.push(this.#createEffectTexture(surface2.canvas.width, surface2.canvas.height, suffix));
1748
+ }
1749
+ surface2.effectPasses = modes.map((mode, index) => {
1750
+ const configBuffer = this.device.createBuffer({
1751
+ label: `Ghosttea shader effect configuration ${index}`,
1752
+ size: 48,
1753
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1754
+ });
1755
+ const inputTexture = index === 0 ? surface2.sceneTexture : surface2.effectTextures[(index - 1) % 2];
1756
+ return {
1757
+ mode,
1758
+ configBuffer,
1759
+ bindGroup: this.#createEffectBindGroup(inputTexture, configBuffer)
1760
+ };
1761
+ });
1762
+ surface2.effectSignature = signature;
1763
+ surface2.effectFrame = 0;
1764
+ surface2.lastEffectTime = 0;
1765
+ }
1766
+ #encodeEffectStack(surface2, view, encoder) {
1767
+ const stack = this.#shaderStack(view);
1768
+ this.#ensureEffectPasses(surface2, stack);
1769
+ const now = view.effects?.animate ? performance.now() / 1e3 : 0;
1770
+ const delta = surface2.lastEffectTime > 0 ? Math.min(0.1, Math.max(0, now - surface2.lastEffectTime)) : 0;
1771
+ surface2.lastEffectTime = now;
1772
+ const passCount = surface2.effectPasses.length;
1773
+ for (let index = 0; index < passCount; index += 1) {
1774
+ const effectPass = surface2.effectPasses[index];
1775
+ const storage = new ArrayBuffer(48);
1776
+ const values = new DataView(storage);
1777
+ values.setUint32(0, effectPass.mode, true);
1778
+ values.setUint32(4, surface2.effectFrame, true);
1779
+ values.setUint32(8, index, true);
1780
+ values.setUint32(12, passCount, true);
1781
+ values.setFloat32(16, surface2.canvas.width, true);
1782
+ values.setFloat32(20, surface2.canvas.height, true);
1783
+ values.setFloat32(24, now, true);
1784
+ values.setFloat32(28, delta, true);
1785
+ values.setFloat32(32, (ORIGIN_X + view.cursor.x * CELL_WIDTH) * surface2.dpr, true);
1786
+ values.setFloat32(36, (ORIGIN_Y + view.cursor.y * LINE_HEIGHT) * surface2.dpr, true);
1787
+ values.setFloat32(40, view.cursor.visible ? 1 : 0, true);
1788
+ values.setFloat32(44, view.cursor.style, true);
1789
+ this.device.queue.writeBuffer(effectPass.configBuffer, 0, storage);
1790
+ const last = index === passCount - 1;
1791
+ const output = last ? surface2.context.getCurrentTexture().createView() : surface2.effectTextures[index % 2].createView();
1792
+ const pass = encoder.beginRenderPass({
1793
+ label: `Ghosttea shader effect pass ${index + 1}/${passCount}`,
1794
+ colorAttachments: [
1795
+ {
1796
+ view: output,
1797
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1798
+ loadOp: "clear",
1799
+ storeOp: "store"
1800
+ }
1801
+ ]
1802
+ });
1803
+ pass.setPipeline(this.#effectPipeline);
1804
+ pass.setBindGroup(0, effectPass.bindGroup);
1805
+ pass.draw(3);
1806
+ pass.end();
1807
+ }
1808
+ surface2.effectFrame += 1;
1809
+ return passCount;
1810
+ }
1811
+ #encodeEffectsOnly(surface2, view, encoder) {
1812
+ const effectPassCount = this.#encodeEffectStack(surface2, view, encoder);
1813
+ if (!this.#performanceMeasurementEnabled) return void 0;
1814
+ return {
1815
+ ...emptyRenderMetrics(),
1816
+ canvasPixels: surface2.canvas.width * surface2.canvas.height,
1817
+ renderPasses: effectPassCount,
1818
+ drawCalls: effectPassCount
1819
+ };
1820
+ }
1483
1821
  render(id, view) {
1484
1822
  const surface2 = this.#surfaces.get(id);
1485
1823
  if (!surface2) return;
1486
- const postProcessEnabled = view.effects?.postProcess === "better-crt";
1487
- if (surface2.postProcessEnabled !== postProcessEnabled) {
1488
- surface2.postProcessEnabled = postProcessEnabled;
1489
- this.device.queue.writeBuffer(
1490
- surface2.postProcessConfigBuffer,
1491
- 0,
1492
- new Uint32Array([Number(postProcessEnabled), 0, 0, 0])
1493
- );
1494
- }
1495
1824
  const encoder = this.device.createCommandEncoder({ label: `terminal frame ${id}` });
1496
1825
  const metrics = view.damage?.geometryChanged === false ? this.#encodeCachedRender(id, view, encoder) : this.#encodeRender(id, view, encoder);
1497
1826
  this.device.queue.submit([encoder.finish()]);
@@ -1504,16 +1833,6 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1504
1833
  const encoder = this.device.createCommandEncoder({ label: `terminal frame batch (${active.length} panes)` });
1505
1834
  const byId = /* @__PURE__ */ new Map();
1506
1835
  for (const { id, view } of active) {
1507
- const surface2 = this.#surfaces.get(id);
1508
- const postProcessEnabled = view.effects?.postProcess === "better-crt";
1509
- if (surface2.postProcessEnabled !== postProcessEnabled) {
1510
- surface2.postProcessEnabled = postProcessEnabled;
1511
- this.device.queue.writeBuffer(
1512
- surface2.postProcessConfigBuffer,
1513
- 0,
1514
- new Uint32Array([Number(postProcessEnabled), 0, 0, 0])
1515
- );
1516
- }
1517
1836
  byId.set(
1518
1837
  id,
1519
1838
  view.damage?.geometryChanged === false ? this.#encodeCachedRender(id, view, encoder) : this.#encodeRender(id, view, encoder)
@@ -1561,14 +1880,13 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1561
1880
  top,
1562
1881
  viewportWidth,
1563
1882
  Math.max(0, bottom - top),
1564
- // The rectangle pipeline blends. Force the row reset opaque so it
1565
- // replaces stale scene pixels even when a theme carries alpha.
1566
- [view.theme.background[0], view.theme.background[1], view.theme.background[2], 1],
1883
+ view.theme.background,
1567
1884
  viewportWidth,
1568
1885
  viewportHeight
1569
1886
  );
1570
1887
  }
1571
1888
  }
1889
+ const rowResetInstanceCount = rectangleVertices.length / 10;
1572
1890
  for (const row of renderRows) {
1573
1891
  for (const run of view.nativeStyleRows[row] ?? []) {
1574
1892
  const style = styleFor(run.styleId);
@@ -1605,6 +1923,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1605
1923
  );
1606
1924
  }
1607
1925
  }
1926
+ pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
1608
1927
  const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
1609
1928
  if (!hasNativeRows) {
1610
1929
  this.#fallbackAtlas.prepare(
@@ -1621,7 +1940,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1621
1940
  const style = styleFor(instance.styleId);
1622
1941
  if (style.invisible) continue;
1623
1942
  const isSelected = selectionContains(selection, row, instance.cellStart);
1624
- const foreground = isSelected ? view.theme.selectionForeground : style.foreground;
1943
+ const foreground = blockCursorContains(view, row, instance.cellStart, instance.cellSpan) ? view.theme.cursorText ?? view.theme.background : isSelected ? view.theme.selectionForeground : style.foreground;
1625
1944
  const boxDrawing = boxCells.get(instance.cellStart);
1626
1945
  if (boxDrawing) {
1627
1946
  let backdrop = style.background ?? view.theme.background;
@@ -1676,7 +1995,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1676
1995
  const glyph = this.#fallbackAtlas.glyph(grapheme, scale);
1677
1996
  const cells = glyph?.cells ?? graphemeCellWidth(grapheme);
1678
1997
  if (glyph) {
1679
- const foreground = selectionContains(selection, row, column) ? view.theme.selectionForeground : view.theme.foreground;
1998
+ const foreground = blockCursorContains(view, row, column, cells) ? view.theme.cursorText ?? view.theme.background : selectionContains(selection, row, column) ? view.theme.selectionForeground : view.theme.foreground;
1680
1999
  pushGlyph(
1681
2000
  fallbackGlyphVertices,
1682
2001
  (ORIGIN_X + column * CELL_WIDTH) * scale,
@@ -1734,6 +2053,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1734
2053
  glyphData: new Float32Array(glyphVertices),
1735
2054
  colorGlyphData: new Float32Array(colorGlyphVertices),
1736
2055
  fallbackGlyphData: new Float32Array(fallbackGlyphVertices),
2056
+ rowResetInstanceCount,
1737
2057
  backgroundInstanceCount,
1738
2058
  selectionInstanceCount,
1739
2059
  decorationInstanceStart,
@@ -1792,14 +2112,13 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1792
2112
  top,
1793
2113
  viewportWidth,
1794
2114
  Math.max(0, bottom - top),
1795
- // The rectangle pipeline blends. Force the row reset opaque so it
1796
- // replaces stale scene pixels even when a theme carries alpha.
1797
- [view.theme.background[0], view.theme.background[1], view.theme.background[2], 1],
2115
+ view.theme.background,
1798
2116
  viewportWidth,
1799
2117
  viewportHeight
1800
2118
  );
1801
2119
  }
1802
2120
  }
2121
+ const rowResetInstanceCount = rectangleVertices.length / 10;
1803
2122
  for (const row of renderRows) {
1804
2123
  for (const run of view.nativeStyleRows[row] ?? []) {
1805
2124
  const style = styleFor(run.styleId);
@@ -1836,6 +2155,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1836
2155
  );
1837
2156
  }
1838
2157
  }
2158
+ pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
1839
2159
  const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
1840
2160
  const hasNativeRows = view.nativeRows.some((row) => row.length > 0);
1841
2161
  if (!hasNativeRows) {
@@ -1853,7 +2173,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1853
2173
  const style = styleFor(instance.styleId);
1854
2174
  if (style.invisible) continue;
1855
2175
  const isSelected = selectionContains(selection, row, instance.cellStart);
1856
- const foreground = isSelected ? view.theme.selectionForeground : style.foreground;
2176
+ const foreground = blockCursorContains(view, row, instance.cellStart, instance.cellSpan) ? view.theme.cursorText ?? view.theme.background : isSelected ? view.theme.selectionForeground : style.foreground;
1857
2177
  const boxDrawing = boxCells.get(instance.cellStart);
1858
2178
  if (boxDrawing) {
1859
2179
  let backdrop = style.background ?? view.theme.background;
@@ -1910,7 +2230,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
1910
2230
  const glyph = this.#fallbackAtlas.glyph(grapheme, scale);
1911
2231
  const cells = glyph?.cells ?? graphemeCellWidth(grapheme);
1912
2232
  if (glyph) {
1913
- const foreground = selectionContains(selection, row, column) ? view.theme.selectionForeground : view.theme.foreground;
2233
+ const foreground = blockCursorContains(view, row, column, cells) ? view.theme.cursorText ?? view.theme.background : selectionContains(selection, row, column) ? view.theme.selectionForeground : view.theme.foreground;
1914
2234
  pushGlyph(
1915
2235
  fallbackGlyphVertices,
1916
2236
  (ORIGIN_X + column * CELL_WIDTH) * scale,
@@ -2026,7 +2346,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2026
2346
  viewportWidth,
2027
2347
  viewportHeight
2028
2348
  );
2029
- } else {
2349
+ } else if (cursorStyle !== CursorStyle.Block) {
2030
2350
  pushRectangle(rectangleVertices, x, y, width, height, cursorColor, viewportWidth, viewportHeight);
2031
2351
  }
2032
2352
  }
@@ -2045,21 +2365,21 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2045
2365
  colorAttachments: [
2046
2366
  {
2047
2367
  view: surface2.sceneTexture.createView(),
2048
- clearValue: {
2049
- r: view.theme.background[0],
2050
- g: view.theme.background[1],
2051
- b: view.theme.background[2],
2052
- a: view.theme.background[3]
2053
- },
2368
+ clearValue: premultipliedClear(view.theme.background),
2054
2369
  loadOp: fullRedraw ? "clear" : "load",
2055
2370
  storeOp: "store"
2056
2371
  }
2057
2372
  ]
2058
2373
  });
2059
- if (rectangleBuffer && backgroundInstanceCount > 0) {
2374
+ if (rectangleBuffer && rowResetInstanceCount > 0) {
2375
+ pass.setPipeline(this.#rectangleOverwritePipeline);
2376
+ pass.setVertexBuffer(0, rectangleBuffer);
2377
+ pass.draw(6, rowResetInstanceCount);
2378
+ }
2379
+ if (rectangleBuffer && backgroundInstanceCount > rowResetInstanceCount) {
2060
2380
  pass.setPipeline(this.#rectanglePipeline);
2061
2381
  pass.setVertexBuffer(0, rectangleBuffer);
2062
- pass.draw(6, backgroundInstanceCount);
2382
+ pass.draw(6, backgroundInstanceCount - rowResetInstanceCount, 0, rowResetInstanceCount);
2063
2383
  }
2064
2384
  if (rectangleBuffer && selectionInstanceCount > 0) {
2065
2385
  pass.setPipeline(this.#rectanglePipeline);
@@ -2095,26 +2415,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2095
2415
  pass.draw(6, cursorInstanceCount, 0, cursorInstanceStart);
2096
2416
  }
2097
2417
  pass.end();
2098
- const postProcessPass = encoder.beginRenderPass({
2099
- label: `Ghostty custom shader pass ${id}`,
2100
- colorAttachments: [
2101
- {
2102
- view: surface2.context.getCurrentTexture().createView(),
2103
- clearValue: {
2104
- r: view.theme.background[0],
2105
- g: view.theme.background[1],
2106
- b: view.theme.background[2],
2107
- a: 1
2108
- },
2109
- loadOp: "clear",
2110
- storeOp: "store"
2111
- }
2112
- ]
2113
- });
2114
- postProcessPass.setPipeline(this.#postProcessPipeline);
2115
- postProcessPass.setBindGroup(0, surface2.postProcessBindGroup);
2116
- postProcessPass.draw(3);
2117
- postProcessPass.end();
2418
+ const effectPassCount = this.#encodeEffectStack(surface2, view, encoder);
2118
2419
  surface2.sceneValid = true;
2119
2420
  if (!this.#performanceMeasurementEnabled) return void 0;
2120
2421
  const monoUploadsAfter = this.#monoAtlas.uploadMetrics();
@@ -2128,8 +2429,8 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2128
2429
  geometryCacheHits: 0,
2129
2430
  geometryCacheMisses: 0,
2130
2431
  canvasPixels: viewportWidth * viewportHeight,
2131
- renderPasses: 2,
2132
- drawCalls: Number(backgroundInstanceCount > 0) + Number(selectionInstanceCount > 0) + Number(glyphData.length > 0) + Number(colorGlyphData.length > 0) + Number(fallbackGlyphData.length > 0) + Number(decorationInstanceCount > 0) + Number(cursorInstanceCount > 0) + 1,
2432
+ renderPasses: 1 + effectPassCount,
2433
+ drawCalls: Number(rowResetInstanceCount > 0) + Number(backgroundInstanceCount > rowResetInstanceCount) + Number(selectionInstanceCount > 0) + Number(glyphData.length > 0) + Number(colorGlyphData.length > 0) + Number(fallbackGlyphData.length > 0) + Number(decorationInstanceCount > 0) + Number(cursorInstanceCount > 0) + effectPassCount,
2133
2434
  rectangleVertices: rectangleVertices.length / 10 * 6,
2134
2435
  monoGlyphVertices: glyphVertices.length / 12 * 6,
2135
2436
  colorGlyphVertices: colorGlyphVertices.length / 12 * 6,
@@ -2142,9 +2443,12 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2142
2443
  #encodeCachedRender(id, view, encoder) {
2143
2444
  const surface2 = this.#surfaces.get(id);
2144
2445
  if (!surface2) return;
2145
- if (!surface2.sceneValid || !view.damage || view.damage.full) return this.#encodeRender(id, view, encoder);
2446
+ if (!surface2.sceneValid || !view.damage || view.damage.full) {
2447
+ return this.#encodeRender(id, view, encoder);
2448
+ }
2146
2449
  const rowCount = Math.max(view.rows.length, view.nativeRows.length, view.nativeStyleRows.length);
2147
2450
  const damage = rowsForDamage(rowCount, view.damage, surface2.sceneValid);
2451
+ if (damage.rows.length === 0) return this.#encodeEffectsOnly(surface2, view, encoder);
2148
2452
  const monoUploadsBefore = this.#performanceMeasurementEnabled ? this.#monoAtlas.uploadMetrics() : void 0;
2149
2453
  const colorUploadsBefore = this.#performanceMeasurementEnabled ? this.#colorAtlas.uploadMetrics() : void 0;
2150
2454
  const fallbackUploadsBefore = this.#performanceMeasurementEnabled ? this.#fallbackAtlas.uploadMetrics() : void 0;
@@ -2187,6 +2491,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2187
2491
  glyphBuffer: promote ? createCachedVertexBuffer(this.device, `cached glyphs ${id}`, cpu.glyphData) : surface2.glyphBuffer.write(cpu.glyphData),
2188
2492
  colorGlyphBuffer: promote ? createCachedVertexBuffer(this.device, `cached color glyphs ${id}`, cpu.colorGlyphData) : surface2.colorGlyphBuffer.write(cpu.colorGlyphData),
2189
2493
  fallbackGlyphBuffer: promote ? createCachedVertexBuffer(this.device, `cached fallback glyphs ${id}`, cpu.fallbackGlyphData) : surface2.fallbackGlyphBuffer.write(cpu.fallbackGlyphData),
2494
+ rowResetInstanceCount: cpu.rowResetInstanceCount,
2190
2495
  backgroundInstanceCount: cpu.backgroundInstanceCount,
2191
2496
  selectionInstanceCount: cpu.selectionInstanceCount,
2192
2497
  decorationInstanceStart: cpu.decorationInstanceStart,
@@ -2228,21 +2533,26 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2228
2533
  colorAttachments: [
2229
2534
  {
2230
2535
  view: surface2.sceneTexture.createView(),
2231
- clearValue: {
2232
- r: view.theme.background[0],
2233
- g: view.theme.background[1],
2234
- b: view.theme.background[2],
2235
- a: view.theme.background[3]
2236
- },
2536
+ clearValue: premultipliedClear(view.theme.background),
2237
2537
  loadOp: "load",
2238
2538
  storeOp: "store"
2239
2539
  }
2240
2540
  ]
2241
2541
  });
2242
- if (geometry.rectangleBuffer && geometry.backgroundInstanceCount > 0) {
2542
+ if (geometry.rectangleBuffer && geometry.rowResetInstanceCount > 0) {
2543
+ pass.setPipeline(this.#rectangleOverwritePipeline);
2544
+ pass.setVertexBuffer(0, geometry.rectangleBuffer);
2545
+ pass.draw(6, geometry.rowResetInstanceCount);
2546
+ }
2547
+ if (geometry.rectangleBuffer && geometry.backgroundInstanceCount > geometry.rowResetInstanceCount) {
2243
2548
  pass.setPipeline(this.#rectanglePipeline);
2244
2549
  pass.setVertexBuffer(0, geometry.rectangleBuffer);
2245
- pass.draw(6, geometry.backgroundInstanceCount);
2550
+ pass.draw(
2551
+ 6,
2552
+ geometry.backgroundInstanceCount - geometry.rowResetInstanceCount,
2553
+ 0,
2554
+ geometry.rowResetInstanceCount
2555
+ );
2246
2556
  }
2247
2557
  if (geometry.rectangleBuffer && geometry.selectionInstanceCount > 0) {
2248
2558
  pass.setPipeline(this.#rectanglePipeline);
@@ -2278,26 +2588,7 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2278
2588
  pass.draw(6, cursorInstanceCount, 0, cursorInstanceStart);
2279
2589
  }
2280
2590
  pass.end();
2281
- const postProcessPass = encoder.beginRenderPass({
2282
- label: `Ghostty custom shader pass ${id}`,
2283
- colorAttachments: [
2284
- {
2285
- view: surface2.context.getCurrentTexture().createView(),
2286
- clearValue: {
2287
- r: view.theme.background[0],
2288
- g: view.theme.background[1],
2289
- b: view.theme.background[2],
2290
- a: 1
2291
- },
2292
- loadOp: "clear",
2293
- storeOp: "store"
2294
- }
2295
- ]
2296
- });
2297
- postProcessPass.setPipeline(this.#postProcessPipeline);
2298
- postProcessPass.setBindGroup(0, surface2.postProcessBindGroup);
2299
- postProcessPass.draw(3);
2300
- postProcessPass.end();
2591
+ const effectPassCount = this.#encodeEffectStack(surface2, view, encoder);
2301
2592
  surface2.sceneValid = true;
2302
2593
  if (!this.#performanceMeasurementEnabled) return void 0;
2303
2594
  const monoUploadsAfter = this.#monoAtlas.uploadMetrics();
@@ -2311,8 +2602,8 @@ var WebGpuTerminalRenderer = class _WebGpuTerminalRenderer {
2311
2602
  geometryCacheHits: Number(cacheHit),
2312
2603
  geometryCacheMisses: Number(!cacheHit),
2313
2604
  canvasPixels: viewportWidth * viewportHeight,
2314
- renderPasses: 2,
2315
- drawCalls: Number(geometry.backgroundInstanceCount > 0) + Number(geometry.selectionInstanceCount > 0) + Number(geometry.glyphInstanceCount > 0) + Number(geometry.colorGlyphInstanceCount > 0) + Number(geometry.fallbackGlyphInstanceCount > 0) + Number(geometry.decorationInstanceCount > 0) + Number(cursorInstanceCount > 0) + 1,
2605
+ renderPasses: 1 + effectPassCount,
2606
+ drawCalls: Number(geometry.rowResetInstanceCount > 0) + Number(geometry.backgroundInstanceCount > geometry.rowResetInstanceCount) + Number(geometry.selectionInstanceCount > 0) + Number(geometry.glyphInstanceCount > 0) + Number(geometry.colorGlyphInstanceCount > 0) + Number(geometry.fallbackGlyphInstanceCount > 0) + Number(geometry.decorationInstanceCount > 0) + Number(cursorInstanceCount > 0) + effectPassCount,
2316
2607
  rectangleVertices: (geometry.rectangleInstanceCount + cursorInstanceCount) * 6,
2317
2608
  monoGlyphVertices: geometry.glyphInstanceCount * 6,
2318
2609
  colorGlyphVertices: geometry.colorGlyphInstanceCount * 6,
@@ -2438,6 +2729,7 @@ var forceCanvasFallback = false;
2438
2729
  var partialRenderingEnabled = true;
2439
2730
  var pendingFrameCreditBytes = 0;
2440
2731
  var frameCreditTimer;
2732
+ var shaderAnimationScheduled = false;
2441
2733
  var performanceMeasurement;
2442
2734
  function appendPerformanceSample(samples, value) {
2443
2735
  if (samples.length < MAX_PERFORMANCE_SAMPLES) samples.push(value);
@@ -2762,6 +3054,7 @@ async function recoverDevice(info) {
2762
3054
  next.resize(id, mount2.size);
2763
3055
  invalidateFull(id);
2764
3056
  }
3057
+ scheduleShaderAnimation();
2765
3058
  postToRenderer({ type: "renderer-status", backend: next.kind, recovered: true });
2766
3059
  } catch (error) {
2767
3060
  console.error("[terminal-renderer] WebGPU recovery failed", error);
@@ -2785,6 +3078,23 @@ function scheduleFlush() {
2785
3078
  if (typeof self.requestAnimationFrame === "function") self.requestAnimationFrame(run);
2786
3079
  else setTimeout(run, 8);
2787
3080
  }
3081
+ function shaderAnimationActive(surfaceId, presentation) {
3082
+ return renderer?.kind === "webgpu" && mounts.has(surfaceId) && !occluded.has(surfaceId) && presentation.focused && terminalEffectsNeedAnimation(presentation.effects);
3083
+ }
3084
+ function scheduleShaderAnimation() {
3085
+ if (shaderAnimationScheduled || ![...surfaces].some(([surfaceId, presentation]) => shaderAnimationActive(surfaceId, presentation)))
3086
+ return;
3087
+ shaderAnimationScheduled = true;
3088
+ const run = () => {
3089
+ shaderAnimationScheduled = false;
3090
+ for (const [surfaceId, presentation] of surfaces) {
3091
+ if (shaderAnimationActive(surfaceId, presentation)) markDirty(surfaceId);
3092
+ }
3093
+ scheduleShaderAnimation();
3094
+ };
3095
+ if (typeof self.requestAnimationFrame === "function") self.requestAnimationFrame(run);
3096
+ else setTimeout(run, 16);
3097
+ }
2788
3098
  async function flush() {
2789
3099
  const backend = await ensureRenderer();
2790
3100
  const ids = [...dirty].filter((id) => !occluded.has(id));
@@ -2888,6 +3198,7 @@ async function mount(surfaceId, sessionHandle, canvas) {
2888
3198
  backend.mount(surfaceId, canvas);
2889
3199
  backend.resize(surfaceId, entry.size);
2890
3200
  invalidateFull(surfaceId);
3201
+ scheduleShaderAnimation();
2891
3202
  }
2892
3203
  function applyFrame(packet) {
2893
3204
  const active = performanceMeasurement;
@@ -3174,6 +3485,7 @@ self.onmessage = (event) => {
3174
3485
  invalidateFull(surfaceId);
3175
3486
  }
3176
3487
  }
3488
+ scheduleShaderAnimation();
3177
3489
  } else if (message.type === "selection") {
3178
3490
  if (message.surfaceId) {
3179
3491
  surface(message.surfaceId, message.sessionHandle).selection = message.selection;
@@ -3201,11 +3513,13 @@ self.onmessage = (event) => {
3201
3513
  cursorBlinkTimers.delete(surfaceId);
3202
3514
  }
3203
3515
  }
3516
+ scheduleShaderAnimation();
3204
3517
  } else if (message.type === "focus") {
3205
3518
  const value = surface(message.surfaceId, message.sessionHandle);
3206
3519
  value.focused = Boolean(message.focused);
3207
3520
  scheduleCursorBlink(message.surfaceId, value.focused);
3208
3521
  invalidateRows(message.surfaceId, [snapshot(message.sessionHandle).cursor.y]);
3522
+ scheduleShaderAnimation();
3209
3523
  } else if (message.type === "cursor-frozen") {
3210
3524
  const value = surfaces.get(message.surfaceId);
3211
3525
  if (!value) return;