OCaml port of Linenoise
at main 662 lines 22 kB view raw
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. *)