1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
open StdLabels
module Hit_grid = Hit_grid
module Glyph_pool = Glyph.Pool
type stats_state = {
mutable frame_count : int;
mutable total_cells : int;
mutable total_bytes : int;
}
type stats = { frame_count : int; total_cells : int; total_bytes : int }
type frame_metrics = {
frame_count : int;
cells : int;
bytes : int;
frame_time_ms : float;
interval_ms : float;
reset_ms : float;
overall_frame_ms : float;
frame_callback_ms : float;
stdout_ms : float;
mouse_enabled : bool;
cursor_visible : bool;
timestamp_s : float;
}
type cursor_info = Cursor_state.snapshot = {
row : int;
col : int;
has_position : bool;
style : [ `Block | `Line | `Underline ];
blinking : bool;
color : (int * int * int) option;
visible : bool;
}
type input_state = { mutable mouse_enabled : bool }
type t = {
glyph_pool : Glyph_pool.t;
stats : stats_state;
mutable last_metrics : frame_metrics;
mutable current : Grid.t;
mutable next : Grid.t;
mutable hit_current : Hit_grid.t;
mutable hit_next : Hit_grid.t;
input : input_state;
cursor : Cursor_state.t;
sgr_state : Ansi.Sgr_state.t;
mutable row_offset : int;
mutable scratch_bytes : bytes;
mutable last_render_time : float option;
mutable prefer_explicit_width : bool;
mutable explicit_width_capable : bool;
mutable use_explicit_width : bool;
mutable use_explicit_cursor_positioning : bool;
mutable hyperlinks_capable : bool;
mutable post_process_fns : (int * (Grid.t -> delta:float -> unit)) list;
mutable post_process_cache : (Grid.t -> delta:float -> unit) list;
mutable post_process_dirty : bool;
mutable next_effect_id : int;
}
let[@inline] width_step w = if w <= 0 then 1 else w
let[@inline] add_code_to_writer ~explicit_width ~explicit_cursor_positioning
~row_offset ~y ~x ~grid_width ~cell_width pool (scratch : bytes ref)
(w : Ansi.writer) grid idx =
let glyph = Grid.get_glyph grid idx in
if Glyph.is_empty glyph || Grid.is_continuation grid idx then
Ansi.emit (Ansi.char ' ') w
else
let len = Glyph_pool.length pool glyph in
if len <= 0 then Ansi.emit (Ansi.char ' ') w
else if len = 1 && (glyph :> int) < 128 then
Ansi.emit (Ansi.char (Char.chr (glyph :> int))) w
else (
if len > Bytes.length !scratch then
scratch := Bytes.create (max (Bytes.length !scratch * 2) len);
let written = Glyph_pool.blit pool glyph !scratch ~pos:0 in
if written <= 0 then Ansi.emit (Ansi.char ' ') w
else if explicit_width && cell_width >= 2 then
Ansi.emit
(Ansi.explicit_width_bytes ~width:cell_width ~bytes:!scratch ~off:0
~len:written)
w
else (
Ansi.emit (Ansi.bytes !scratch ~off:0 ~len:written) w;
if explicit_cursor_positioning && cell_width >= 2 then
let next_x = x + cell_width in
if next_x < grid_width then
Ansi.cursor_position ~row:(row_offset + y + 1) ~col:(next_x + 1) w))
type render_mode = [ `Diff | `Full ]
let render_generic ~pool ~row_offset ~use_explicit_width
~use_explicit_cursor_positioning ~use_hyperlinks ~mode ~height_limit ~writer
~scratch ~sgr_state ~prev ~curr =
let width = Grid.width curr in
let curr_height = Grid.height curr in
let height =
match height_limit with
| None -> curr_height
| Some limit -> max 0 (min curr_height limit)
in
let row_offset = max 0 row_offset in
let prev_width = match prev with None -> 0 | Some p -> Grid.width p in
let prev_height_raw = match prev with None -> 0 | Some p -> Grid.height p in
let prev_height =
match height_limit with
| None -> prev_height_raw
| Some _ -> min prev_height_raw height
in
let[@inline] is_cell_changed y x idx curr_width =
if curr_width <= 0 then false
else
match mode with
| `Full -> true
| `Diff -> (
if y >= prev_height || x >= prev_width then true
else
match prev with
| None -> true
| Some p ->
let prev_idx = (y * prev_width) + x in
not (Grid.cells_equal p prev_idx curr idx))
in
Ansi.Sgr_state.reset sgr_state;
let rec write_run y x =
if x >= width then x
else
let idx = (y * width) + x in
let curr_width = Grid.cell_width curr idx in
let step = width_step curr_width in
if curr_width <= 0 then x
else if not (is_cell_changed y x idx curr_width) then x
else
let attrs = Grid.get_attrs curr idx in
let link =
if use_hyperlinks then
Grid.hyperlink_url_direct curr (Grid.get_link curr idx)
else ""
in
let fg_r = Grid.get_fg_r curr idx in
let fg_g = Grid.get_fg_g curr idx in
let fg_b = Grid.get_fg_b curr idx in
let fg_a = Grid.get_fg_a curr idx in
let bg_r = Grid.get_bg_r curr idx in
let bg_g = Grid.get_bg_g curr idx in
let bg_b = Grid.get_bg_b curr idx in
let bg_a = Grid.get_bg_a curr idx in
Ansi.Sgr_state.update sgr_state writer ~fg_r ~fg_g ~fg_b ~fg_a ~bg_r
~bg_g ~bg_b ~bg_a ~attrs ~link;
add_code_to_writer ~explicit_width:use_explicit_width
~explicit_cursor_positioning:use_explicit_cursor_positioning
~row_offset ~y ~x ~grid_width:width ~cell_width:curr_width pool
scratch writer curr idx;
write_run y (x + step)
in
let rec process_cols y x row_cells =
if x >= width then row_cells
else
let idx = (y * width) + x in
let curr_width = Grid.cell_width curr idx in
if curr_width <= 0 then (
(if mode = `Diff && y < prev_height && x < prev_width then
match prev with
| None -> ()
| Some p ->
let prev_idx = (y * prev_width) + x in
if not (Grid.cells_equal p prev_idx curr idx) then (
Ansi.cursor_position
~row:(row_offset + y + 1)
~col:(x + 1) writer;
Ansi.emit (Ansi.char ' ') writer));
process_cols y (x + 1) row_cells)
else if is_cell_changed y x idx curr_width then (
let target_row = row_offset + y + 1 in
Ansi.cursor_position ~row:target_row ~col:(x + 1) writer;
let start_x = x in
let new_x = write_run y x in
let cells_in_run = new_x - start_x in
Ansi.Sgr_state.close_link sgr_state writer;
Ansi.Sgr_state.reset sgr_state;
process_cols y new_x (row_cells + cells_in_run))
else process_cols y (x + width_step curr_width) row_cells
in
let rec process_rows y total_cells =
if y >= height then total_cells
else
let row_cells = process_cols y 0 0 in
process_rows (y + 1) (total_cells + row_cells)
in
let total = process_rows 0 0 in
(if prev_width > width then
let start_col = width + 1 in
let rows = min height prev_height in
for y = 0 to rows - 1 do
Ansi.cursor_position ~row:(row_offset + y + 1) ~col:start_col writer;
Ansi.erase_line ~mode:`Right writer
done);
if prev_height > height then
for y = height to prev_height - 1 do
Ansi.cursor_position ~row:(row_offset + y + 1) ~col:1 writer;
Ansi.erase_line ~mode:`All writer
done;
Ansi.Sgr_state.close_link sgr_state writer;
Ansi.Sgr_state.reset sgr_state;
total
let[@inline] swap_buffers r =
let old_current = r.current in
r.current <- r.next;
r.next <- old_current;
let old_hit_current = r.hit_current in
r.hit_current <- r.hit_next;
r.hit_next <- old_hit_current;
Hit_grid.clear r.hit_next;
Grid.clear r.next
let post_processes r =
if r.post_process_dirty then (
r.post_process_cache <- List.rev_map ~f:snd r.post_process_fns;
r.post_process_dirty <- false);
r.post_process_cache
let prepare_frame r =
let now = Unix.gettimeofday () in
let delta_seconds =
match r.last_render_time with
| None ->
r.last_render_time <- Some now;
0.
| Some prev ->
let delta = now -. prev in
r.last_render_time <- Some now;
if delta <= 0. then 0. else delta
in
let delta_ms = delta_seconds *. 1000. in
List.iter ~f:(fun fn -> fn r.next ~delta:delta_ms) (post_processes r);
(now, delta_seconds)
let finalize_frame r ~now ~delta_seconds ~elapsed_ms ~cells ~output_len =
let t_reset_start = Unix.gettimeofday () in
swap_buffers r;
let t_reset_end = Unix.gettimeofday () in
let reset_ms = (t_reset_end -. t_reset_start) *. 1000. in
r.stats.frame_count <- r.stats.frame_count + 1;
r.stats.total_cells <- r.stats.total_cells + cells;
r.stats.total_bytes <- r.stats.total_bytes + output_len;
let next_m =
{
frame_count = r.stats.frame_count;
cells;
bytes = output_len;
frame_time_ms = elapsed_ms;
interval_ms = delta_seconds *. 1000.;
reset_ms;
overall_frame_ms = 0.;
frame_callback_ms = 0.;
stdout_ms = 0.;
mouse_enabled = r.input.mouse_enabled;
cursor_visible = Cursor_state.is_visible r.cursor;
timestamp_s = now;
}
in
r.last_metrics <- next_m
let submit ~(mode : render_mode) ?height_limit ~(writer : Ansi.writer) r =
let now, delta_seconds = prepare_frame r in
let cells = ref 0 in
let elapsed_ms = ref 0. in
Fun.protect
~finally:(fun () ->
Ansi.Sgr_state.close_link r.sgr_state writer;
Ansi.Sgr_state.reset r.sgr_state)
(fun () ->
let scratch = ref r.scratch_bytes in
let render_start = Unix.gettimeofday () in
let prev = match mode with `Diff -> Some r.current | `Full -> None in
cells :=
render_generic ~pool:r.glyph_pool ~row_offset:r.row_offset
~use_explicit_width:r.use_explicit_width
~use_explicit_cursor_positioning:r.use_explicit_cursor_positioning
~use_hyperlinks:r.hyperlinks_capable ~mode ~height_limit ~writer
~scratch ~sgr_state:r.sgr_state ~prev ~curr:r.next;
elapsed_ms := (Unix.gettimeofday () -. render_start) *. 1000.;
r.scratch_bytes <- !scratch);
let output_len = Ansi.Writer.len writer in
finalize_frame r ~now ~delta_seconds ~elapsed_ms:!elapsed_ms ~cells:!cells
~output_len
let render_to_bytes ?(full = false) ?height_limit frame bytes =
let writer = Ansi.Writer.make bytes in
let mode = if full then `Full else `Diff in
submit frame ~mode ?height_limit ~writer;
Ansi.Writer.len writer
let render ?(full = false) ?height_limit frame =
let bytes = Bytes.create 65536 in
let len = render_to_bytes ~full ?height_limit frame bytes in
Bytes.sub_string bytes ~pos:0 ~len
let glyph_pool t = t.glyph_pool
let create ?glyph_pool ?width_method ?respect_alpha ?(mouse_enabled = true)
?(cursor_visible = true) ?(explicit_width = false) () =
let glyph_pool =
match glyph_pool with Some p -> p | None -> Glyph_pool.create ()
in
let w_method = match width_method with Some m -> m | None -> `Unicode in
let r_alpha = match respect_alpha with Some r -> r | None -> false in
let t =
{
glyph_pool;
stats = { frame_count = 0; total_cells = 0; total_bytes = 0 };
last_metrics =
{
frame_count = 0;
cells = 0;
bytes = 0;
frame_time_ms = 0.;
interval_ms = 0.;
reset_ms = 0.;
overall_frame_ms = 0.;
frame_callback_ms = 0.;
stdout_ms = 0.;
mouse_enabled;
cursor_visible;
timestamp_s = 0.;
};
current =
Grid.create ~width:1 ~height:1 ~glyph_pool ~width_method:w_method
~respect_alpha:r_alpha ();
next =
Grid.create ~width:1 ~height:1 ~glyph_pool ~width_method:w_method
~respect_alpha:r_alpha ();
hit_current = Hit_grid.create ~width:0 ~height:0;
hit_next = Hit_grid.create ~width:0 ~height:0;
input = { mouse_enabled };
cursor = Cursor_state.create ();
sgr_state = Ansi.Sgr_state.create ();
row_offset = 0;
post_process_fns = [];
post_process_cache = [];
post_process_dirty = false;
next_effect_id = 0;
prefer_explicit_width = explicit_width;
explicit_width_capable = true;
use_explicit_width = explicit_width;
use_explicit_cursor_positioning = false;
hyperlinks_capable = true;
scratch_bytes = Bytes.create 1024;
last_render_time = None;
}
in
Cursor_state.set_visible t.cursor cursor_visible;
t
let reset t =
Grid.clear t.next;
Hit_grid.clear t.hit_current;
Hit_grid.clear t.hit_next;
t.last_render_time <- None;
t.stats.frame_count <- 0;
t.stats.total_cells <- 0;
t.stats.total_bytes <- 0;
Cursor_state.reset t.cursor;
Ansi.Sgr_state.reset t.sgr_state
let resize t ~width ~height =
Grid.resize t.current ~width ~height;
Grid.resize t.next ~width ~height;
Hit_grid.resize t.hit_current ~width ~height;
Hit_grid.resize t.hit_next ~width ~height;
if width > 0 && height > 0 then
Cursor_state.clamp_to_bounds t.cursor ~max_row:height ~max_col:width
let internal_build t ~width ~height f =
if width <= 0 || height <= 0 then (
Hit_grid.clear t.hit_next;
t)
else (
if width <> Grid.width t.next || height <> Grid.height t.next then
resize t ~width ~height;
Hit_grid.clear t.hit_next;
f t.next t.hit_next;
t)
let build t ~width ~height f =
internal_build t ~width ~height (fun grid hits -> f grid hits)
let grid frame = frame.next
let hit_grid frame = frame.hit_next
let query_hit frame ~x ~y = Hit_grid.get frame.hit_current ~x ~y
let row_offset t = t.row_offset
let set_row_offset t offset = t.row_offset <- max 0 offset
let invalidate_presented t =
Grid.clear t.current
let active_height (t : t) = Grid.active_height t.next
let stats t =
{
frame_count = t.stats.frame_count;
total_cells = t.stats.total_cells;
total_bytes = t.stats.total_bytes;
}
let last_metrics t = t.last_metrics
let record_runtime_metrics t ~frame_callback_ms ~overall_frame_ms ~stdout_ms =
let m = t.last_metrics in
let new_m = { m with frame_callback_ms; overall_frame_ms; stdout_ms } in
t.last_metrics <- new_m
let set_mouse_enabled t enabled = t.input.mouse_enabled <- enabled
let set_cursor_visible t visible = Cursor_state.set_visible t.cursor visible
let set_cursor_position t ~row ~col =
Cursor_state.set_position t.cursor ~row ~col
let clear_cursor_position t = Cursor_state.clear_position t.cursor
let set_cursor_style t ~style ~blinking =
Cursor_state.set_style t.cursor ~style ~blinking
let clamp_byte v = max 0 (min 255 v)
let set_cursor_color t ~r ~g ~b =
Cursor_state.set_color t.cursor
(Some (clamp_byte r, clamp_byte g, clamp_byte b))
let reset_cursor_color t = Cursor_state.set_color t.cursor None
let cursor_info t = Cursor_state.snapshot t.cursor
let apply_capabilities r ~explicit_width ~explicit_cursor_positioning
~hyperlinks =
r.explicit_width_capable <- explicit_width;
r.use_explicit_width <- r.prefer_explicit_width && explicit_width;
r.use_explicit_cursor_positioning <-
(not r.use_explicit_width) && explicit_cursor_positioning;
r.hyperlinks_capable <- hyperlinks
let set_explicit_width t flag =
t.prefer_explicit_width <- flag;
t.use_explicit_width <- flag && t.explicit_width_capable
let set_width_method (t : t) (method_ : Glyph.width_method) =
Grid.set_width_method t.current method_;
Grid.set_width_method t.next method_
type effect_id = int
let post_process f frame =
let id = frame.next_effect_id in
frame.next_effect_id <- id + 1;
frame.post_process_fns <- (id, f) :: frame.post_process_fns;
frame.post_process_dirty <- true;
id
let remove_post_process id frame =
frame.post_process_fns <-
List.filter ~f:(fun (eid, _) -> eid <> id) frame.post_process_fns;
frame.post_process_dirty <- true;
frame
let clear_post_processes frame =
frame.post_process_fns <- [];
frame.post_process_cache <- [];
frame.post_process_dirty <- false;
frame
let add_hit_region frame ~x ~y ~width ~height ~id =
Hit_grid.add frame.hit_next ~x ~y ~width ~height ~id;
frame