diff --git a/example/main.ml b/example/main.ml index 8bfbea8..63fec56 100644 --- a/example/main.ml +++ b/example/main.ml @@ -1,3 +1,9 @@ +let h = ref [] + +let history prefix = + if prefix <> "" then List.filter (fun s -> String.starts_with ~prefix s) !h + else !h + let complete s = match String.get s 0 with | 'h' -> [ "hello"; "hello there" ] @@ -10,9 +16,10 @@ let () = if sys_break then "[\x1b[31m130\x1b[0m] \x1b[33m>>\x1b[0m " else "\x1b[33m>>\x1b[0m " in - match Bruit.bruit ~complete prompt with + match Bruit.bruit ~history ~complete prompt with | String (Some s) -> Fmt.pr "%s\n%!" s; + h := s :: !h; loop false | String None -> () | Ctrl_c -> loop true diff --git a/src/bruit.ml b/src/bruit.ml index b280d3c..a175ef7 100644 --- a/src/bruit.ml +++ b/src/bruit.ml @@ -47,6 +47,8 @@ module State = struct old_rows : int; old_row_pos : int; history_index : int; + history : string list; + saved_buf : string; read_buf : Bytes.t; in_completion : bool; completion_idx : int; @@ -54,7 +56,7 @@ module State = struct } let make ?(in_completion = false) ?(completion_idx = 0) ?complete - ?(old_pos = 0) ?(pos = 0) ?(len = 0) ?(ifd = Unix.stdin) + ?(old_pos = 0) ?(pos = 0) ?(len = 0) ?(history = []) ?(ifd = Unix.stdin) ?(ofd = Unix.stdout) ~prompt buf = { in_completion; @@ -69,8 +71,10 @@ module State = struct len; cols = 0; old_row_pos = 1; + history; old_rows = 0; - history_index = 0; + history_index = -1; + saved_buf = ""; complete; completion_idx; read_buf = Bytes.make 1 '\000' (* For reading a character *); @@ -78,12 +82,17 @@ module State = struct let override ?in_completion ?completion_idx ?complete ?ifd ?ofd ?buf ?buf_len ?prompt ?plen ?old_pos ?pos ?len ?cols ?old_rows ?old_row_pos - ?history_index (t : t) = + ?history_index ?history ?saved_buf (t : t) = + let () = + match buf with + | None -> () + | Some buf -> Bytes.blit buf 0 t.buf 0 (Bytes.length buf) + in { in_completion = Option.value ~default:t.in_completion in_completion; ifd = Option.value ~default:t.ifd ifd; ofd = Option.value ~default:t.ofd ofd; - buf = Option.value ~default:t.buf buf; + buf = t.buf; buf_len = Option.value ~default:t.buf_len buf_len; prompt = Option.value ~default:t.prompt prompt; plen = Option.value ~default:t.plen plen; @@ -97,6 +106,8 @@ module State = struct complete = (match complete with Some f -> Some f | None -> t.complete); read_buf = t.read_buf; completion_idx = Option.value ~default:t.completion_idx completion_idx; + history = Option.value ~default:t.history history; + saved_buf = Option.value ~default:t.saved_buf saved_buf; } end @@ -335,7 +346,38 @@ let move_right (state : State.t) = in refresh_line s -let edit_feed state = +let edit_history dir fn (state : State.t) = + let saved_state = state in + let current_buf = Bytes.sub_string state.buf 0 state.len in + let state = + match (dir, state.history_index) with + | `Prev, -1 -> + State.override ~history:(fn current_buf) ~history_index:0 + ~saved_buf:current_buf state + | `Prev, m -> + let max_history = List.length state.history in + if m < max_history - 1 then + State.override ~history_index:(state.history_index + 1) state + else state + | `Next, m when m >= 0 -> + State.override ~history_index:(state.history_index - 1) state + | _ -> state + in + match (state.history, state.history_index) with + | [], _ -> saved_state + | _, -1 -> + let len = String.length state.saved_buf in + State.override ~buf:(Bytes.of_string state.saved_buf) ~pos:len ~len state + |> refresh_line + | _ -> + let max_history = List.length state.history in + let idx = min max_history state.history_index in + let s = List.nth state.history idx in + let s_len = String.length s in + State.override ~buf:(Bytes.of_string s) ~pos:s_len ~len:s_len state + |> refresh_line + +let edit_feed ~history state = match read_char state with | `Editing -> Editing state | `None -> Finished None @@ -368,36 +410,40 @@ let edit_feed state = read_char state |> function `Some c -> c | _ -> assert false in match (c1, c2) with + | '[', 'A' -> Editing (edit_history `Prev history state) + | '[', 'B' -> Editing (edit_history `Next history state) | '[', 'C' -> Editing (move_right state) | '[', 'D' -> Editing (move_left state) - | '[', _ -> - Fmt.epr "TODO %c\n%!" c; - Editing state) + | _ -> Editing state) | Unknown _ | _ -> let state = edit_insert state uc in Editing state)) type result = String of string option | Ctrl_c -let blocking_edit ?complete ~stdin ~stdout buf ~prompt = +let blocking_edit ?complete ~history ~stdin ~stdout buf ~prompt = let state = State.make ?complete ~prompt buf in let res = edit_start ~stdin ~stdout state @@ fun state -> let rec loop = function - | Editing state -> loop (edit_feed state) + | Editing state -> loop (edit_feed ~history state) | Finished s -> String (Option.map Bytes.to_string s) | Ctrl_c -> Ctrl_c in - loop (edit_feed state) + loop (edit_feed ~history state) in Format.printf "\n%!"; res -let bruit ?complete prompt = +type history = string -> string list + +let bruit ?complete ?(history = fun _ -> []) prompt = let prompt = Bytes.of_string prompt in let buf = Bytes.make max_line '\000' in if not (Unix.isatty Unix.stdin) then failwith "Stdin is not a tty" - else blocking_edit ?complete ~stdin:Unix.stdin ~stdout:Unix.stdout buf ~prompt + else + blocking_edit ?complete ~history ~stdin:Unix.stdin ~stdout:Unix.stdout buf + ~prompt (* * Copyright (c) 2010-2023, Salvatore Sanfilippo diff --git a/src/bruit.mli b/src/bruit.mli index 9380c6c..b178b75 100644 --- a/src/bruit.mli +++ b/src/bruit.mli @@ -4,9 +4,14 @@ The main entry point to the library is {! bruit}. *) +type history = string -> string list +(** The history callback that provides the user with the current line and + expects a list of history items to scroll through using the arrow keys. *) + type result = String of string option | Ctrl_c -val bruit : ?complete:(string -> string list) -> string -> result +val bruit : + ?complete:(string -> string list) -> ?history:history -> string -> result (** [bruit ?complete prompt] reads from [stdin] and returns the read string if any, and on [ctrl+c] returns {! Ctrl_c}.