package miaou-driver-matrix

  1. Overview
  2. Docs
Miaou high-performance terminal driver with diff rendering

Install

dune-project
 Dependency

Authors

Maintainers

Sources

v0.5.2.tar.gz
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6

doc/src/miaou-driver-matrix.driver/matrix_ansi_parser.ml.html

Source file matrix_ansi_parser.ml

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
(******************************************************************************)
(*                                                                            *)
(* SPDX-License-Identifier: MIT                                               *)
(* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com>                 *)
(*                                                                            *)
(******************************************************************************)

type parse_state =
  | Normal
  | EscapeStart
  | CSI of int list * int (* accumulated params, current param *)
  | OSC (* inside an OSC sequence — skip until ST *)
  | DCS (* inside a DCS sequence (e.g. Sixel) — accumulate until ST *)

type t = {
  mutable style : Matrix_cell.style;
  mutable state : parse_state;
  osc_buf : Buffer.t;
  dcs_buf : Buffer.t;
}

let create () =
  {
    style = Matrix_cell.default_style;
    state = Normal;
    osc_buf = Buffer.create 64;
    dcs_buf = Buffer.create 1024;
  }

let reset t =
  t.style <- Matrix_cell.default_style ;
  t.state <- Normal ;
  Buffer.clear t.osc_buf ;
  Buffer.clear t.dcs_buf

let current_style t = t.style

(* Apply SGR (Select Graphic Rendition) parameters to style *)
let apply_sgr params style =
  let rec process params style =
    match params with
    | [] -> style
    | 0 :: rest ->
        (* Reset all SGR attributes — preserves URL (OSC 8 is not SGR) *)
        process
          rest
          {
            Matrix_cell.default_style with
            Matrix_cell.url = style.Matrix_cell.url;
          }
    | 1 :: rest ->
        (* Bold *)
        process rest {style with bold = true}
    | 2 :: rest ->
        (* Dim *)
        process rest {style with dim = true}
    | 4 :: rest ->
        (* Underline *)
        process rest {style with underline = true}
    | 7 :: rest ->
        (* Reverse *)
        process rest {style with reverse = true}
    | 22 :: rest ->
        (* Normal intensity (reset bold/dim) *)
        process rest {style with bold = false; dim = false}
    | 24 :: rest ->
        (* Underline off *)
        process rest {style with underline = false}
    | 27 :: rest ->
        (* Reverse off *)
        process rest {style with reverse = false}
    (* Basic foreground colors 30-37 *)
    | n :: rest when n >= 30 && n <= 37 -> process rest {style with fg = n - 30}
    | 39 :: rest ->
        (* Default foreground *)
        process rest {style with fg = -1}
    (* Basic background colors 40-47 *)
    | n :: rest when n >= 40 && n <= 47 -> process rest {style with bg = n - 40}
    | 49 :: rest ->
        (* Default background *)
        process rest {style with bg = -1}
    (* Bright foreground colors 90-97 *)
    | n :: rest when n >= 90 && n <= 97 ->
        process rest {style with fg = n - 90 + 8}
    (* Bright background colors 100-107 *)
    | n :: rest when n >= 100 && n <= 107 ->
        process rest {style with bg = n - 100 + 8}
    (* 256-color foreground: 38;5;N *)
    | 38 :: 5 :: n :: rest ->
        let fg = if n >= 0 && n <= 255 then n else -1 in
        process rest {style with fg}
    (* 256-color background: 48;5;N *)
    | 48 :: 5 :: n :: rest ->
        let bg = if n >= 0 && n <= 255 then n else -1 in
        process rest {style with bg}
    (* Unknown - skip *)
    | _ :: rest -> process rest style
  in
  process params style

(* Get UTF-8 character length in bytes *)
let utf8_char_length c =
  let code = Char.code c in
  if code land 0x80 = 0 then 1
  else if code land 0xE0 = 0xC0 then 2
  else if code land 0xF0 = 0xE0 then 3
  else if code land 0xF8 = 0xF0 then 4
  else 1 (* Invalid, treat as single byte *)

(* Extract UTF-8 character from string at position *)
let extract_utf8_char s pos =
  if pos >= String.length s then ("", 0)
  else
    let len = utf8_char_length s.[pos] in
    let len = min len (String.length s - pos) in
    (String.sub s pos len, len)

(* Process a completed OSC sequence payload.
   For OSC 8 (hyperlinks): "8;params;uri" — extract the URI and update style.
   Empty URI closes the current hyperlink. *)
let process_osc t =
  let payload = Buffer.contents t.osc_buf in
  let plen = String.length payload in
  (* Check for OSC 8: starts with "8;" *)
  if plen >= 2 && payload.[0] = '8' && payload.[1] = ';' then
    (* Find the second semicolon that separates params from URI *)
    match String.index_from_opt payload 2 ';' with
    | Some idx ->
        let url = String.sub payload (idx + 1) (plen - idx - 1) in
        t.style <- {t.style with url}
    | None ->
        (* Malformed OSC 8 — no second semicolon, ignore *)
        ()

(* Core parsing function - shared state machine logic.
   Takes a callback ~emit_char that receives (char, style) for each visible character.
   Returns the number of visible characters parsed. *)
let parse_core t ~emit_char input =
  let len = String.length input in
  let count = ref 0 in
  let pos = ref 0 in

  while !pos < len do
    match t.state with
    | Normal -> (
        let c = input.[!pos] in
        match c with
        | '\027' ->
            t.state <- EscapeStart ;
            incr pos
        | '\n' | '\r' ->
            (* Skip newlines in single-line parsing *)
            incr pos
        | _ ->
            (* Regular character - extract full UTF-8 *)
            let char, char_len = extract_utf8_char input !pos in
            if char_len > 0 then begin
              emit_char char t.style ;
              incr count ;
              pos := !pos + char_len
            end
            else incr pos)
    | EscapeStart ->
        if !pos < len then (
          match input.[!pos] with
          | '[' ->
              t.state <- CSI ([], 0) ;
              incr pos
          | ']' ->
              (* OSC sequence (e.g. OSC 8 hyperlinks) *)
              Buffer.clear t.osc_buf ;
              t.state <- OSC ;
              incr pos
          | 'P' ->
              (* DCS sequence (e.g. Sixel graphics) — accumulate and emit as cell *)
              Buffer.clear t.dcs_buf ;
              Buffer.add_string t.dcs_buf "\027P" ;
              t.state <- DCS ;
              incr pos
          | _ ->
              (* Not a CSI/OSC/DCS sequence, back to normal *)
              t.state <- Normal ;
              incr pos)
        else t.state <- Normal
    | OSC ->
        (* Collect bytes until String Terminator: ESC \ or BEL (\007) *)
        if !pos < len then
          let c = input.[!pos] in
          if c = '\007' then (
            (* BEL terminates OSC — process payload *)
            process_osc t ;
            t.state <- Normal ;
            incr pos)
          else if c = '\027' && !pos + 1 < len && input.[!pos + 1] = '\\' then (
            (* ESC \ terminates OSC — process payload *)
            process_osc t ;
            t.state <- Normal ;
            pos := !pos + 2)
          else (
            Buffer.add_char t.osc_buf c ;
            incr pos)
        else t.state <- Normal
    | DCS ->
        (* Accumulate DCS payload until ESC \ (String Terminator) *)
        if !pos < len then
          let c = input.[!pos] in
          if c = '\027' && !pos + 1 < len && input.[!pos + 1] = '\\' then begin
            Buffer.add_string t.dcs_buf "\027\\" ;
            (* Emit the entire DCS sequence as a single passthrough cell *)
            emit_char (Buffer.contents t.dcs_buf) t.style ;
            t.state <- Normal ;
            pos := !pos + 2
          end
          else begin
            Buffer.add_char t.dcs_buf c ;
            incr pos
          end
        else t.state <- Normal
    | CSI (params, current) ->
        if !pos < len then (
          let c = input.[!pos] in
          match c with
          | '0' .. '9' ->
              let digit = Char.code c - Char.code '0' in
              t.state <- CSI (params, (current * 10) + digit) ;
              incr pos
          | ';' ->
              t.state <- CSI (params @ [current], 0) ;
              incr pos
          | 'm' ->
              (* SGR complete *)
              let all_params = params @ [current] in
              t.style <- apply_sgr all_params t.style ;
              t.state <- Normal ;
              incr pos
          | 'A' .. 'Z' | 'a' .. 'l' | 'n' .. 'z' ->
              (* Other CSI sequence terminator - ignore *)
              t.state <- Normal ;
              incr pos
          | _ ->
              (* Unknown, abort CSI parsing *)
              t.state <- Normal ;
              incr pos)
        else t.state <- Normal
  done ;
  !count

(* Parse a single line into a buffer at given row/col *)
let parse_line t buf ~row ~col input =
  let col = ref col in
  let emit_char char style =
    Matrix_buffer.set_char buf ~row ~col:!col ~char ~style ;
    incr col
  in
  let _ = parse_core t ~emit_char input in
  !col

(* Parse into buffer, handling newlines *)
let parse_into t buf ~row ~col input =
  let lines = String.split_on_char '\n' input in
  let row = ref row in
  let col = ref col in
  List.iteri
    (fun i line ->
      if i > 0 then begin
        incr row ;
        col := 0
      end ;
      col := parse_line t buf ~row:!row ~col:!col line)
    lines ;
  (!row, !col)

(* Batch version using batch_ops for thread-safe access *)
let parse_line_batch t (ops : Matrix_buffer.batch_ops) ~row ~col input =
  let col = ref col in
  let emit_char char style =
    ops.set_char ~row ~col:!col ~char ~style ;
    incr col
  in
  let _ = parse_core t ~emit_char input in
  !col

let parse_into_batch t ops ~row ~col input =
  let lines = String.split_on_char '\n' input in
  let row = ref row in
  let col = ref col in
  List.iteri
    (fun i line ->
      if i > 0 then begin
        incr row ;
        col := 0
      end ;
      col := parse_line_batch t ops ~row:!row ~col:!col line)
    lines ;
  (!row, !col)

(* Parse to a list of (char, style) pairs *)
let parse_to_cells t input =
  let results = ref [] in
  let emit_char char style = results := (char, style) :: !results in
  let _ = parse_core t ~emit_char input in
  List.rev !results

(* Calculate visible length (chars excluding ANSI codes and OSC sequences) *)
let visible_length input =
  let len = String.length input in
  let count = ref 0 in
  let pos = ref 0 in
  let in_escape = ref false in
  let in_osc = ref false in
  let in_dcs = ref false in

  while !pos < len do
    let c = input.[!pos] in
    if !in_dcs then begin
      (* Skip DCS payload until ESC \ *)
      if c = '\027' && !pos + 1 < len && input.[!pos + 1] = '\\' then (
        in_dcs := false ;
        pos := !pos + 2)
      else incr pos
    end
    else if !in_osc then begin
      if c = '\007' then (
        in_osc := false ;
        incr pos)
      else if c = '\027' && !pos + 1 < len && input.[!pos + 1] = '\\' then (
        in_osc := false ;
        pos := !pos + 2)
      else incr pos
    end
    else if !in_escape then begin
      if c = ']' then (
        in_escape := false ;
        in_osc := true ;
        incr pos)
      else if c = 'P' then (
        (* DCS sequence *)
        in_escape := false ;
        in_dcs := true ;
        incr pos)
      else (
        if c = 'm' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') then
          in_escape := false ;
        incr pos)
    end
    else if c = '\027' then begin
      in_escape := true ;
      incr pos
    end
    else if c = '\n' || c = '\r' then incr pos
    else begin
      let char_len = utf8_char_length c in
      incr count ;
      pos := !pos + char_len
    end
  done ;
  !count