package miaou-core

  1. Overview
  2. Docs
Miaou core/widgets (no drivers, no SDL)

Install

dune-project
 Dependency

Authors

Maintainers

Sources

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

doc/src/miaou-core.driver-common/terminal_raw.ml.html

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

(** Raw terminal operations shared between drivers.
    Based on term_terminal_setup.ml (lambda-term driver). *)

type t = {
  fd : Unix.file_descr;
  tty_out_fd : Unix.file_descr;
  orig_termios : Unix.terminal_io option;
  mutable cached_size : (int * int) option;
  mutable cleanup_done : bool;
  resize_pending : bool Atomic.t;
  (* Screen content to dump on exit for debugging *)
  mutable exit_screen_dump : string option;
  (* When false, enter_raw / cleanup skip the alternate-screen
     sequences. The TUI then renders inline at the cursor position
     and its final frame stays in the terminal scrollback. *)
  mutable use_alt_screen : bool;
}

let setup () =
  let fd = Unix.descr_of_in_channel stdin in
  if not (try Unix.isatty fd with _ -> false) then
    failwith "interactive TUI requires a terminal" ;
  let orig_termios = try Some (Unix.tcgetattr fd) with _ -> None in
  (* Open /dev/tty directly for writing escape sequences - this is more reliable
     than stdout during cleanup/signal handling because stdout might be redirected *)
  let tty_out_fd =
    try Unix.openfile "/dev/tty" [Unix.O_WRONLY] 0
    with _ -> Unix.descr_of_out_channel stdout
  in
  {
    fd;
    tty_out_fd;
    orig_termios;
    cached_size = None;
    cleanup_done = false;
    resize_pending = Atomic.make false;
    exit_screen_dump = None;
    use_alt_screen = true;
  }

let set_exit_screen_dump t dump = t.exit_screen_dump <- Some dump

let set_alt_screen t enabled = t.use_alt_screen <- enabled

let alt_screen_enabled t = t.use_alt_screen

let fd t = t.fd

let enter_raw t =
  match t.orig_termios with
  | None -> ()
  | Some orig -> (
      let raw =
        {
          orig with
          Unix.c_icanon = false;
          Unix.c_echo = false;
          Unix.c_isig = false;
          (* Disable SIGINT/SIGQUIT/SIGSUSP generation *)
          Unix.c_vmin = 1;
          Unix.c_vtime = 0;
        }
      in
      Unix.tcsetattr t.fd Unix.TCSANOW raw ;
      (* Enter alternate screen mode unless inline mode is on *)
      if t.use_alt_screen then
        try
          let alt_seq = "\027[?1049h" in
          ignore
            (Unix.write
               t.tty_out_fd
               (Bytes.of_string alt_seq)
               0
               (String.length alt_seq))
        with _ -> ())

let leave_raw t =
  match t.orig_termios with
  | None -> ()
  | Some orig -> ( try Unix.tcsetattr t.fd Unix.TCSANOW orig with _ -> ())

let disable_mouse t =
  (* ALWAYS run mouse cleanup - no flag check!
     Mouse tracking must be disabled reliably on exit.
     The escape sequences are idempotent (safe to send multiple times). *)
  let disable_seq =
    "\027[?1006l\027[?1015l\027[?1005l\027[?1003l\027[?1002l\027[?1000l"
  in
  (* Method 1: Write to /dev/tty *)
  (try
     for _ = 1 to 2 do
       let _ =
         Unix.write
           t.tty_out_fd
           (Bytes.of_string disable_seq)
           0
           (String.length disable_seq)
       in
       ()
     done ;
     Unix.tcdrain t.tty_out_fd
   with _ -> ()) ;
  (* Method 2: Write to stdout *)
  (try
     (print_string [@allow_forbidden "terminal driver writes escape sequences"])
       disable_seq ;
     Stdlib.flush stdout
   with _ -> ()) ;
  (* Method 3: Write to stderr as last resort *)
  (try Printf.eprintf "%s%!" disable_seq with _ -> ()) ;
  (* Give terminal time to process escape sequences *)
  (Unix.sleepf [@allow_forbidden "terminal cleanup needs blocking wait"]) 0.05

let enable_mouse t =
  (* 1002: Button event tracking (reports motion while button pressed)
     1006: SGR extended mode (allows coordinates > 223)
     Write to tty_out_fd for consistency with other terminal operations. *)
  let enable_seq = "\027[?1002h\027[?1006h" in
  (try
     ignore
       (Unix.write
          t.tty_out_fd
          (Bytes.of_string enable_seq)
          0
          (String.length enable_seq)) ;
     Unix.tcdrain t.tty_out_fd
   with _ -> ()) ;
  (* Also write to stdout as fallback *)
  try
    (print_string [@allow_forbidden "terminal driver writes escape sequences"])
      enable_seq ;
    Stdlib.flush stdout
  with _ -> ()

let cleanup t =
  if not t.cleanup_done then begin
    t.cleanup_done <- true ;
    (* Step 1: Exit alternate screen mode (restores main buffer).
       In inline mode we skip this — the rendered frame stays in
       scrollback. We do, however, append a newline so the next shell
       prompt does not land on top of the last rendered row. *)
    (if t.use_alt_screen then
       let exit_alt_seq = "\027[?1049l" in
       try
         ignore
           (Unix.write
              t.tty_out_fd
              (Bytes.of_string exit_alt_seq)
              0
              (String.length exit_alt_seq)) ;
         Unix.tcdrain t.tty_out_fd
       with _ -> ()
     else
       try
         ignore (Unix.write t.tty_out_fd (Bytes.of_string "\n") 0 1) ;
         Unix.tcdrain t.tty_out_fd
       with _ -> ()) ;
    (* Step 2: Restore terminal settings *)
    leave_raw t ;
    (* Step 3: Print saved screen content (if any) for debugging.
       Write directly to /dev/tty to avoid shell interference (ble.sh, etc.).
       Skipped in inline mode — the live frame is already in scrollback. *)
    (if t.use_alt_screen then
       match t.exit_screen_dump with
       | Some dump -> (
           try
             let clear_seq = "\027[2J\027[H" in
             ignore
               (Unix.write
                  t.tty_out_fd
                  (Bytes.of_string clear_seq)
                  0
                  (String.length clear_seq)) ;
             ignore
               (Unix.write
                  t.tty_out_fd
                  (Bytes.of_string dump)
                  0
                  (String.length dump)) ;
             ignore (Unix.write t.tty_out_fd (Bytes.of_string "\n") 0 1) ;
             Unix.tcdrain t.tty_out_fd
           with _ -> ())
       | None -> ()) ;
    (* Step 4: Show cursor, reset style *)
    let final_seq = "\027[?25h\027[0m" in
    try
      (print_string
      [@allow_forbidden "terminal driver writes escape sequences"])
        final_seq ;
      Stdlib.flush stdout
    with _ -> ()
  end ;
  (* THEN disable mouse tracking - terminal is now in normal mode *)
  disable_mouse t

let write t s =
  try
    let _ = Unix.write t.tty_out_fd (Bytes.of_string s) 0 (String.length s) in
    ()
  with _ -> (
    try
      (print_string
      [@allow_forbidden "terminal driver writes escape sequences"])
        s ;
      Stdlib.flush stdout
    with _ -> ())

(* Size detection using stty - direct method without System capability *)
let detect_size_direct () =
  let run_stty ?stdin_fd argv =
    try
      let pipe_read, pipe_write = Unix.pipe () in
      let stdin_fd =
        match stdin_fd with
        | Some fd -> fd
        | None -> Unix.descr_of_in_channel Stdlib.stdin
      in
      (* Redirect stderr to /dev/null to suppress stty error messages *)
      let dev_null = Unix.openfile "/dev/null" [Unix.O_WRONLY] 0 in
      let pid =
        Fun.protect
          ~finally:(fun () -> Unix.close dev_null)
          (fun () ->
            Unix.create_process "stty" argv stdin_fd pipe_write dev_null)
      in
      Unix.close pipe_write ;
      let buf = Buffer.create 32 in
      let tmp = Bytes.create 64 in
      let rec read_all () =
        match Unix.read pipe_read tmp 0 64 with
        | 0 -> ()
        | n ->
            Buffer.add_subbytes buf tmp 0 n ;
            read_all ()
      in
      read_all () ;
      Unix.close pipe_read ;
      let _ = Unix.waitpid [] pid in
      let output = String.trim (Buffer.contents buf) in
      match String.split_on_char ' ' output with
      | [r; c] ->
          let rows = int_of_string r in
          let cols = int_of_string c in
          Some (rows, cols)
      | _ -> None
    with _ -> None
  in
  let try_stty argv =
    let tty_fd =
      try Some (Unix.openfile "/dev/tty" [Unix.O_RDONLY] 0) with _ -> None
    in
    let res =
      match tty_fd with
      | Some fd ->
          Fun.protect
            ~finally:(fun () -> Unix.close fd)
            (fun () -> run_stty ~stdin_fd:fd argv)
      | None -> run_stty argv
    in
    res
  in
  match try_stty [|"stty"; "size"; "-F"; "/proc/self/fd/1"|] with
  | Some s -> Some s
  | None -> (
      match try_stty [|"stty"; "size"; "-F"; "/dev/tty"|] with
      | Some s -> Some s
      | None -> try_stty [|"stty"; "size"|])

let detect_size_env () =
  match (Sys.getenv_opt "MIAOU_TUI_ROWS", Sys.getenv_opt "MIAOU_TUI_COLS") with
  | Some r, Some c -> (
      try
        let rows = int_of_string (String.trim r) in
        let cols = int_of_string (String.trim c) in
        Some (rows, cols)
      with _ -> None)
  | _ -> None

let detect_size_uncached () =
  match detect_size_env () with
  | Some s -> s
  | None -> ( match detect_size_direct () with Some s -> s | None -> (24, 80))

let size t =
  match t.cached_size with
  | Some s -> s
  | None ->
      let s = detect_size_uncached () in
      t.cached_size <- Some s ;
      s

let invalidate_size_cache t = t.cached_size <- None

let install_signals' t ~on_resize ~on_exit ?(handle_sigint = true) () =
  let exit_flag = Atomic.make false in
  let sigwinch = 28 in
  (* Install resize handler *)
  (try
     Sys.set_signal
       sigwinch
       (Sys.Signal_handle
          (fun _ ->
            invalidate_size_cache t ;
            Atomic.set t.resize_pending true ;
            on_resize ()))
   with _ -> ()) ;
  (* Install exit handlers *)
  let set_exit_handler sigv =
    try
      Sys.set_signal
        sigv
        (Sys.Signal_handle
           (fun _ ->
             (* Run cleanup synchronously in signal handler *)
             (try on_exit () with _ -> ()) ;
             (* Set flag so main loop can exit gracefully *)
             Atomic.set exit_flag true ;
             (* Exit immediately *)
             exit 130))
    with _ -> ()
  in
  if handle_sigint then set_exit_handler Sys.sigint
  else Sys.set_signal Sys.sigint Sys.Signal_ignore ;
  set_exit_handler Sys.sigterm ;
  (try set_exit_handler Sys.sighup with _ -> ()) ;
  (try set_exit_handler Sys.sigquit with _ -> ()) ;
  exit_flag

let install_signals t ~on_resize ~on_exit =
  install_signals' t ~on_resize ~on_exit ~handle_sigint:true ()

let resize_pending t = Atomic.get t.resize_pending

let clear_resize_pending t = Atomic.set t.resize_pending false