OCaml port of Linenoise
1(* See the end of the file for the original license of Linenoise. *)
2let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
3let max_line = 2048
4
5type hint = string -> (string * Fmt.style) option
6
7type key =
8 | Enter
9 | Ctrl_a
10 | Ctrl_b
11 | Ctrl_c
12 | Ctrl_d
13 | Ctrl_e
14 | Ctrl_f
15 | Ctrl_r
16 | Ctrl_p
17 | Ctrl_g
18 | Backspace
19 | Escape_sequence
20 | Tab
21 | Unknown of Uchar.t
22
23let key_of_char c =
24 match Char.code c with
25 | 1 -> Ctrl_a
26 | 2 -> Ctrl_b
27 | 3 -> Ctrl_c
28 | 4 -> Ctrl_d
29 | 5 -> Ctrl_e
30 | 6 -> Ctrl_f
31 | 7 -> Ctrl_g
32 | 16 -> Ctrl_p
33 | 18 -> Ctrl_r
34 | 9 -> Tab
35 | 13 -> Enter
36 | 27 -> Escape_sequence
37 | 127 -> Backspace
38 | _ -> Unknown (Uchar.of_char c)
39
40module State = struct
41 type completion = string -> string list
42
43 type t = {
44 ifd : Unix.file_descr;
45 ofd : Unix.file_descr;
46 buf : bytes;
47 buf_len : int;
48 prompt : bytes;
49 plen : int;
50 old_pos : int;
51 pos : int;
52 len : int;
53 (* Update later *)
54 cols : int;
55 old_rows : int;
56 old_row_pos : int;
57 history_index : int;
58 history : string list;
59 saved_buf : string;
60 read_buf : Bytes.t;
61 in_completion : bool;
62 completion_idx : int;
63 complete : completion option;
64 hint : hint;
65 }
66
67 let buf t = Bytes.sub t.buf 0 t.len
68
69 let make ?(in_completion = false) ?(completion_idx = 0) ?complete
70 ?(old_pos = 0) ?(pos = 0) ?(len = 0) ?(history = [])
71 ?(hint = fun _ -> None) ?(ifd = Unix.stdin) ?(ofd = Unix.stdout) ~prompt
72 buf =
73 {
74 in_completion;
75 ifd;
76 ofd;
77 buf;
78 buf_len = Bytes.length buf;
79 prompt;
80 plen = Bytes.length prompt;
81 old_pos;
82 pos;
83 len;
84 cols = 0;
85 old_row_pos = 1;
86 history;
87 old_rows = 0;
88 history_index = -1;
89 saved_buf = "";
90 complete;
91 completion_idx;
92 read_buf = Bytes.make 1 '\000' (* For reading a character *);
93 hint;
94 }
95
96 let override ?in_completion ?completion_idx ?complete ?ifd ?ofd ?buf ?buf_len
97 ?prompt ?plen ?old_pos ?pos ?len ?cols ?old_rows ?old_row_pos
98 ?history_index ?history ?saved_buf (t : t) =
99 let () =
100 match buf with
101 | None -> ()
102 | Some buf -> Bytes.blit buf 0 t.buf 0 (Bytes.length buf)
103 in
104 {
105 in_completion = Option.value ~default:t.in_completion in_completion;
106 ifd = Option.value ~default:t.ifd ifd;
107 ofd = Option.value ~default:t.ofd ofd;
108 buf = t.buf;
109 buf_len = Option.value ~default:t.buf_len buf_len;
110 prompt = Option.value ~default:t.prompt prompt;
111 plen = Option.value ~default:t.plen plen;
112 old_pos = Option.value ~default:t.old_pos old_pos;
113 pos = Option.value ~default:t.pos pos;
114 len = Option.value ~default:t.len len;
115 cols = Option.value ~default:t.cols cols;
116 old_rows = Option.value ~default:t.old_rows old_rows;
117 old_row_pos = Option.value ~default:t.old_row_pos old_row_pos;
118 history_index = Option.value ~default:t.history_index history_index;
119 complete = (match complete with Some f -> Some f | None -> t.complete);
120 read_buf = t.read_buf;
121 completion_idx = Option.value ~default:t.completion_idx completion_idx;
122 history = Option.value ~default:t.history history;
123 saved_buf = Option.value ~default:t.saved_buf saved_buf;
124 hint = t.hint;
125 }
126end
127
128let get_columns () =
129 match Terminal.Size.get_columns () with Some n -> n | None -> 80
130
131let with_raw_mode (state : State.t) fn =
132 let saved_tio = Unix.tcgetattr state.ifd in
133 let tio : Unix.terminal_io =
134 {
135 saved_tio with
136 c_brkint = false;
137 c_icrnl = false;
138 c_inpck = false;
139 c_istrip = false;
140 c_ixon = false;
141 c_opost = false;
142 c_csize = 8;
143 c_echo = false;
144 c_icanon = false;
145 c_isig = false;
146 c_vtime = 0;
147 c_vmin = 1;
148 }
149 in
150 Unix.tcsetattr state.ifd TCSADRAIN tio;
151 Fun.protect
152 ~finally:(fun () -> Unix.tcsetattr state.ifd TCSADRAIN saved_tio)
153 fn
154
155let write_bytes fd s =
156 let len = Bytes.length s in
157 let wrote = Unix.write fd s 0 len in
158 assert (Int.equal len wrote)
159
160let write_uchar fd u =
161 let b_len = Uchar.utf_8_byte_length u in
162 let bs = Bytes.create b_len in
163 let wrote = Bytes.set_utf_8_uchar bs 0 u in
164 assert (Int.equal b_len wrote);
165 write_bytes fd bs
166
167type edit = Editing of State.t | Finished of bytes option | Ctrl_c
168
169let read_char state =
170 try
171 let read = Unix.read state.State.ifd state.read_buf 0 1 in
172 if read = 0 then `None else `Some (Bytes.unsafe_get state.read_buf 0)
173 with Unix.Unix_error ((Unix.EWOULDBLOCK | Unix.EAGAIN), _, _) -> `Editing
174
175let edit_start ~stdin:_ ~stdout:_ state fn =
176 with_raw_mode state @@ fun () ->
177 let cols = get_columns () in
178 (* Bytes.set state.buf 0 '\000'; *)
179 let state = State.override ~cols ~buf_len:(state.buf_len - 1) state in
180 write_bytes state.ofd state.prompt;
181 fn state
182
183let utf8_display_width b len =
184 let s = Bytes.to_string b in
185 Terminal.guess_printed_width (String.sub s 0 len)
186
187let utf8_next_char_len s off =
188 Bytes.get_utf_8_uchar s off
189 |> Uchar.utf_decode_uchar |> Uchar.utf_8_byte_length
190
191let utf8_prev_char_len s off =
192 let rec loop pos =
193 if pos < 0 || pos < off - 4 then
194 invalid_arg "UTF8 previous character length";
195 let decode = Bytes.get_utf_8_uchar s off in
196 if Uchar.utf_decode_is_valid decode then
197 Uchar.utf_decode_uchar decode |> Uchar.utf_8_byte_length
198 else loop (pos - 1)
199 in
200 loop (off - 1)
201
202type refresh_flag = Rewrite
203
204let refresh_with_hints ~pwidth ~ab (state : State.t) =
205 let buf_width = utf8_display_width state.buf state.len in
206 if pwidth + buf_width < state.cols then begin
207 match state.hint (State.buf state |> Bytes.to_string) with
208 | None -> ()
209 | Some (hint, style) ->
210 let () =
211 Format.fprintf Format.str_formatter "%a"
212 Fmt.(styled style string)
213 hint
214 in
215 Buffer.add_string ab (Format.flush_str_formatter ())
216 end
217
218let refresh_single_line ?(flags = []) ?prompt (state : State.t) =
219 let prompt = match prompt with None -> state.prompt | Some p -> p in
220 let pwidth = utf8_display_width prompt state.plen in
221 let poscol = ref @@ utf8_display_width state.buf state.pos in
222 let lencol = ref @@ utf8_display_width state.buf state.len in
223
224 let rec loop (state : State.t) =
225 if pwidth + !poscol >= state.cols then begin
226 let clen = utf8_next_char_len state.buf 0 in
227 let c_width =
228 Uchar.utf_8_byte_length
229 (Bytes.get_utf_8_uchar state.buf clen |> Uchar.utf_decode_uchar)
230 in
231 poscol := !poscol - c_width;
232 lencol := !lencol - c_width;
233 let state =
234 State.override ~len:(state.len - clen) ~pos:(state.pos - clen) state
235 in
236 loop state
237 end
238 else state
239 in
240 let state = loop state in
241 let ab = Buffer.create 0 in
242 (* Clear line *)
243 Buffer.add_char ab '\r';
244
245 (* Add prompt *)
246 if List.mem Rewrite flags then begin
247 Buffer.add_bytes ab prompt;
248 Buffer.add_bytes ab (Bytes.sub state.buf 0 state.len)
249 end;
250
251 refresh_with_hints ~pwidth:state.len ~ab state;
252
253 (* Erase to the right *)
254 Buffer.add_string ab "\x1b[0K";
255
256 (* Cursor to the original position *)
257 if List.mem Rewrite flags then begin
258 Buffer.add_string ab (Format.sprintf "\r\x1b[%dC" (!poscol + pwidth))
259 end;
260 write_bytes state.ofd (Buffer.to_bytes ab);
261 state
262
263let refresh_line state = refresh_single_line ~flags:[ Rewrite ] state
264
265let refresh_line_with_completions (state : State.t) lcs =
266 if state.completion_idx < List.length lcs then
267 let saved_state = state in
268 let saved_buf = Bytes.copy state.buf in
269 let state =
270 State.override
271 ~len:(Bytes.length (List.nth lcs state.completion_idx))
272 ~pos:(Bytes.length (List.nth lcs state.completion_idx))
273 ~buf:(List.nth lcs state.completion_idx)
274 state
275 in
276 let state : State.t = refresh_line state in
277 State.override state ~len:saved_state.len ~pos:saved_state.pos
278 ~buf:saved_buf
279 else state
280
281let edit_insert (state : State.t) c =
282 let clen = Uchar.utf_8_byte_length c in
283 (* At the end of the line *)
284 if Int.equal state.len state.pos then begin
285 let _ : int = Bytes.set_utf_8_uchar state.buf state.pos c in
286 let state =
287 State.override ~pos:(state.pos + clen) ~len:(state.len + clen) state
288 in
289 if
290 utf8_display_width state.prompt state.plen
291 + utf8_display_width state.buf state.len
292 < state.cols
293 then begin
294 write_uchar state.ofd c;
295 refresh_line state
296 end
297 else refresh_line state
298 end
299 else begin
300 Bytes.blit state.buf state.pos state.buf (state.pos + clen)
301 (state.len - state.pos);
302 let _ : int = Bytes.set_utf_8_uchar state.buf state.pos c in
303 let state =
304 State.override ~len:(state.len + clen) ~pos:(state.pos + clen) state
305 in
306 refresh_line state
307 end
308
309let edit_backspace (state : State.t) =
310 let state =
311 if state.pos > 0 && state.len > 0 then begin
312 let clen = utf8_prev_char_len state.buf state.pos in
313 let dst = state.pos - clen in
314 let src = state.pos in
315 let len = state.len - state.pos in
316 Bytes.blit state.buf src state.buf dst len;
317 State.override ~pos:(state.pos - clen) ~len:(state.len - clen) state
318 end
319 else state
320 in
321 refresh_line state
322
323let complete_line (state : State.t) c cn =
324 match cn (String.of_bytes (State.buf state)) with
325 | [] -> (State.override ~in_completion:false state, `Char c)
326 | xs ->
327 let state, c =
328 match key_of_char c with
329 | Tab ->
330 if not state.in_completion then
331 ( State.override ~in_completion:true ~completion_idx:0 state,
332 `Edit_more )
333 else
334 ( State.override
335 ~completion_idx:
336 ((state.completion_idx + 1) mod (List.length xs + 1))
337 state,
338 `Edit_more )
339 | _ ->
340 let state =
341 if state.completion_idx < List.length xs then begin
342 let to_write = List.nth xs state.completion_idx in
343 let to_write_len = String.length to_write in
344 Bytes.blit_string to_write 0 state.buf 0 to_write_len;
345 State.override ~len:to_write_len ~pos:to_write_len state
346 end
347 else state
348 in
349 (State.override ~in_completion:false state, `Char c)
350 in
351 if state.in_completion && state.completion_idx < List.length xs then begin
352 (refresh_line_with_completions state (List.map Bytes.of_string xs), c)
353 end
354 else begin
355 (refresh_line state, c)
356 end
357
358let move_left (state : State.t) =
359 let s =
360 if state.pos > 0 then
361 State.override
362 ~pos:(state.pos - utf8_prev_char_len state.buf state.pos)
363 state
364 else state
365 in
366 refresh_line s
367
368let complete_with_hint (state : State.t) =
369 let buf = State.buf state |> Bytes.to_string in
370 match state.hint buf with
371 | None -> state
372 | Some (h, _) ->
373 let new_buf = buf ^ h in
374 let end_buf = String.length new_buf in
375 Bytes.blit_string new_buf 0 state.buf 0 end_buf;
376 State.override ~pos:end_buf ~len:end_buf state
377
378let move_right (state : State.t) =
379 let s =
380 if state.pos < state.len then
381 State.override
382 ~pos:(state.pos + utf8_next_char_len state.buf state.pos)
383 state
384 else if state.pos = state.len then complete_with_hint state
385 else state
386 in
387 refresh_line s
388
389let move_right_next_word (state : State.t) =
390 let pos = ref state.pos in
391 while !pos < state.len && Bytes.get state.buf !pos = ' ' do
392 incr pos
393 done;
394 while !pos < state.len && Bytes.get state.buf !pos <> ' ' do
395 incr pos
396 done;
397 let s = State.override ~pos:!pos state in
398 refresh_line s
399
400let move_left_next_word (state : State.t) =
401 let pos = ref state.pos in
402 while !pos > 0 && Bytes.get state.buf !pos = ' ' do
403 decr pos
404 done;
405 while !pos > 0 && Bytes.get state.buf !pos <> ' ' do
406 decr pos
407 done;
408 let s = State.override ~pos:!pos state in
409 refresh_line s
410
411let reverse_incr_search ~history (state : State.t) =
412 let has_match = ref true in
413 let search_buf = Buffer.create 16 in
414 let search_pos = ref 0 in
415 let search_dir = ref (-1) in
416 let h = history "" in
417 let history_len = List.length h in
418 let saved_buf = Bytes.copy state.buf in
419 let exception Completed of State.t in
420 let rec loop state : State.t =
421 let prompt =
422 if !has_match then
423 Fmt.str "(reverse-i-search)`%s': " (Buffer.contents search_buf)
424 else
425 Fmt.str "(failed-reverse-i-search)`%s': " (Buffer.contents search_buf)
426 in
427 let new_char = ref false in
428 let state = State.override ~pos:0 state in
429 let state =
430 refresh_single_line ~flags:[ Rewrite ] ~prompt:(String.to_bytes prompt)
431 state
432 in
433 let state =
434 match read_char state with
435 | `Editing -> loop state
436 | `None -> loop state
437 | `Some c -> (
438 match key_of_char c with
439 | Backspace ->
440 if Buffer.length search_buf > 0 then begin
441 (* Pretty wasteful... *)
442 let s = Buffer.contents search_buf in
443 Buffer.clear search_buf;
444 Buffer.add_substring search_buf s 0 (String.length s - 1);
445 search_pos := 0
446 end;
447 state
448 | Ctrl_p ->
449 search_dir := -1;
450 if !search_pos >= history_len then search_pos := history_len - 1;
451 state
452 | Ctrl_r ->
453 search_dir := 1;
454 if !search_pos < 0 then search_pos := 0;
455 state
456 | Ctrl_g ->
457 let l = Bytes.length saved_buf in
458 Bytes.blit saved_buf 0 state.buf 0 l;
459 let state = refresh_line (State.override ~pos:l ~len:l state) in
460 raise (Completed state)
461 | Enter ->
462 let state = State.override ~pos:state.len state in
463 raise (Completed state)
464 | _ ->
465 if Char.compare c ' ' > 0 then begin
466 new_char := true;
467 Buffer.add_char search_buf c;
468 search_pos := 0;
469 state
470 end
471 else
472 State.override ~pos:state.len state |> refresh_line |> fun s ->
473 raise (Completed s))
474 in
475 has_match := false;
476 let state =
477 if Buffer.length search_buf > 0 then begin
478 let rec inner_loop () =
479 if !search_pos >= 0 && !search_pos < history_len then begin
480 let entry = List.nth h !search_pos in
481 match
482 ( Astring.String.cut ~sep:(Buffer.contents search_buf) entry,
483 !new_char
484 || not
485 @@ String.equal entry (Bytes.to_string (State.buf state)) )
486 with
487 | Some (_l, _r), true ->
488 has_match := true;
489 Bytes.blit_string entry 0 state.buf 0 (String.length entry);
490 let state = State.override ~len:(String.length entry) state in
491 state
492 | _ ->
493 search_pos := !search_pos + !search_dir;
494 inner_loop ()
495 end
496 else state
497 in
498 inner_loop ()
499 end
500 else state
501 in
502 loop state
503 in
504 try loop state with Completed state -> state
505
506let edit_history dir fn (state : State.t) =
507 let saved_state = state in
508 let current_buf = Bytes.sub_string state.buf 0 state.len in
509 let state =
510 match (dir, state.history_index) with
511 | `Prev, -1 ->
512 State.override ~history:(fn current_buf) ~history_index:0
513 ~saved_buf:current_buf state
514 | `Prev, m ->
515 let max_history = List.length state.history in
516 if m < max_history - 1 then
517 State.override ~history_index:(state.history_index + 1) state
518 else state
519 | `Next, m when m >= 0 ->
520 State.override ~history_index:(state.history_index - 1) state
521 | _ -> state
522 in
523 match (state.history, state.history_index) with
524 | [], _ -> saved_state
525 | _, -1 ->
526 let len = String.length state.saved_buf in
527 State.override ~buf:(Bytes.of_string state.saved_buf) ~pos:len ~len state
528 |> refresh_line
529 | _ ->
530 let max_history = List.length state.history in
531 let idx = min max_history state.history_index in
532 let s = List.nth state.history idx in
533 let s_len = String.length s in
534 State.override ~buf:(Bytes.of_string s) ~pos:s_len ~len:s_len state
535 |> refresh_line
536
537let edit_feed ~history state =
538 match read_char state with
539 | `Editing -> Editing state
540 | `None -> Finished None
541 | `Some c -> (
542 let uc = Uchar.of_char c in
543 let state, c =
544 if state.in_completion || key_of_char c = Tab then
545 match state.complete with
546 | None -> (state, `Char c)
547 | Some cn -> complete_line state c cn
548 else (state, `Char c)
549 in
550 match c with
551 | `Edit_more -> Editing state
552 | `Char c -> (
553 match key_of_char c with
554 | Enter -> Finished (Some (Bytes.sub state.buf 0 state.len))
555 | Ctrl_d ->
556 if Int.equal state.len 0 then Finished None else Editing state
557 | Ctrl_c -> Ctrl_c
558 | Ctrl_b -> Editing (move_left state)
559 | Ctrl_f -> Editing (move_right state)
560 | Ctrl_r -> Editing (reverse_incr_search ~history state)
561 | Backspace -> Editing (edit_backspace state)
562 | Tab -> Editing state
563 | Escape_sequence -> (
564 let c0 =
565 read_char state |> function `Some c -> c | _ -> assert false
566 in
567 match c0 with
568 | '[' ->
569 let c1 =
570 read_char state |> function
571 | `Some c -> c
572 | _ -> assert false
573 in
574 if Char.compare c1 '0' >= 0 && Char.compare c1 '9' <= 0 then
575 let c2 =
576 read_char state |> function
577 | `Some c -> c
578 | _ -> assert false
579 in
580 let c3 =
581 match read_char state with
582 | `Some c -> Some c
583 | (exception _) | _ -> None
584 in
585 let c4 =
586 match read_char state with
587 | `Some c -> Some c
588 | (exception _) | _ -> None
589 in
590 match (c2, c3) with
591 | ';', Some '5' -> (
592 match c4 with
593 | Some 'D' -> Editing (move_left_next_word state)
594 | Some 'C' -> Editing (move_right_next_word state)
595 | _ -> Editing state)
596 | _ -> Editing state
597 else begin
598 match c1 with
599 | 'A' -> Editing (edit_history `Prev history state)
600 | 'B' -> Editing (edit_history `Next history state)
601 | 'C' -> Editing (move_right state)
602 | 'D' -> Editing (move_left state)
603 | _ -> Editing state
604 end
605 | _ -> Editing state)
606 | _ ->
607 let state = edit_insert state uc in
608 Editing state))
609
610type result = String of string option | Ctrl_c
611
612let blocking_edit ?complete ~history ~hint ~stdin ~stdout buf ~prompt =
613 let state = State.make ?complete ~hint ~prompt buf in
614 let res =
615 edit_start ~stdin ~stdout state @@ fun state ->
616 let rec loop = function
617 | Editing state -> loop (edit_feed ~history state)
618 | Finished s -> String (Option.map Bytes.to_string s)
619 | Ctrl_c -> Ctrl_c
620 in
621 loop (edit_feed ~history state)
622 in
623 res
624
625type history = string -> string list
626
627let bruit ?complete ?(history = fun _ -> []) ?(hint = fun _ -> None) prompt =
628 let prompt = Bytes.of_string prompt in
629 let buf = Bytes.make max_line '\000' in
630 if not (Unix.isatty Unix.stdin) then failwith "Stdin is not a tty"
631 else
632 blocking_edit ?complete ~history ~hint ~stdin:Unix.stdin ~stdout:Unix.stdout
633 buf ~prompt
634
635(*
636 * Copyright (c) 2010-2023, Salvatore Sanfilippo <antirez at gmail dot com>
637 * Copyright (c) 2010-2013, Pieter Noordhuis <pcnoordhuis at gmail dot com>
638 *
639 * All rights reserved.
640 *
641 * Redistribution and use in source and binary forms, with or without
642 * modification, are permitted provided that the following conditions are
643 * met:
644 *
645 * * Redistributions of source code must retain the above copyright
646 * notice, this list of conditions and the following disclaimer.
647 *
648 * * Redistributions in binary form must reproduce the above copyright
649 * notice, this list of conditions and the following disclaimer in the
650 * documentation and/or other materials provided with the distribution.
651 *
652 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
653 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
654 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
655 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
656 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
657 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
658 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
659 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
660 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
661 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
662 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *)