···11+let h = ref []
22+33+let history prefix =
44+ if prefix <> "" then List.filter (fun s -> String.starts_with ~prefix s) !h
55+ else !h
66+17let complete s =
28 match String.get s 0 with
39 | 'h' -> [ "hello"; "hello there" ]
···1016 if sys_break then "[\x1b[31m130\x1b[0m] \x1b[33m>>\x1b[0m "
1117 else "\x1b[33m>>\x1b[0m "
1218 in
1313- match Bruit.bruit ~complete prompt with
1919+ match Bruit.bruit ~history ~complete prompt with
1420 | String (Some s) ->
1515- Fmt.pr "%s\n%!" s;
2121+ Fmt.pr "\n%s\n%!" s;
2222+ h := s :: !h;
1623 loop false
1724 | String None -> ()
1825 | Ctrl_c -> loop true
+305-27
src/bruit.ml
···11(* See the end of the file for the original license of Linenoise. *)
22-22+let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
33let max_line = 2048
44+55+type hint = string -> (string * Fmt.style) option
4657type key =
68 | Enter
···911 | Ctrl_c
1012 | Ctrl_d
1113 | Ctrl_e
1414+ | Ctrl_f
1515+ | Ctrl_r
1616+ | Ctrl_p
1717+ | Ctrl_g
1218 | Backspace
1319 | Escape_sequence
1420 | Tab
···2127 | 3 -> Ctrl_c
2228 | 4 -> Ctrl_d
2329 | 5 -> Ctrl_e
3030+ | 6 -> Ctrl_f
3131+ | 7 -> Ctrl_g
3232+ | 16 -> Ctrl_p
3333+ | 18 -> Ctrl_r
2434 | 9 -> Tab
2535 | 13 -> Enter
2636 | 27 -> Escape_sequence
···4555 old_rows : int;
4656 old_row_pos : int;
4757 history_index : int;
5858+ history : string list;
5959+ saved_buf : string;
4860 read_buf : Bytes.t;
4961 in_completion : bool;
5062 completion_idx : int;
5163 complete : completion option;
6464+ hint : hint;
5265 }
6666+6767+ let buf t = Bytes.sub t.buf 0 t.len
53685469 let make ?(in_completion = false) ?(completion_idx = 0) ?complete
5555- ?(old_pos = 0) ?(pos = 0) ?(len = 0) ?(ifd = Unix.stdin)
5656- ?(ofd = Unix.stdout) ~prompt buf =
7070+ ?(old_pos = 0) ?(pos = 0) ?(len = 0) ?(history = [])
7171+ ?(hint = fun _ -> None) ?(ifd = Unix.stdin) ?(ofd = Unix.stdout) ~prompt
7272+ buf =
5773 {
5874 in_completion;
5975 ifd;
···6783 len;
6884 cols = 0;
6985 old_row_pos = 1;
8686+ history;
7087 old_rows = 0;
7171- history_index = 0;
8888+ history_index = -1;
8989+ saved_buf = "";
7290 complete;
7391 completion_idx;
7474- read_buf = Bytes.create 1 (* For reading a character *);
9292+ read_buf = Bytes.make 1 '\000' (* For reading a character *);
9393+ hint;
7594 }
76957796 let override ?in_completion ?completion_idx ?complete ?ifd ?ofd ?buf ?buf_len
7897 ?prompt ?plen ?old_pos ?pos ?len ?cols ?old_rows ?old_row_pos
7979- ?history_index (t : t) =
9898+ ?history_index ?history ?saved_buf (t : t) =
9999+ let () =
100100+ match buf with
101101+ | None -> ()
102102+ | Some buf -> Bytes.blit buf 0 t.buf 0 (Bytes.length buf)
103103+ in
80104 {
81105 in_completion = Option.value ~default:t.in_completion in_completion;
82106 ifd = Option.value ~default:t.ifd ifd;
83107 ofd = Option.value ~default:t.ofd ofd;
8484- buf = Option.value ~default:t.buf buf;
108108+ buf = t.buf;
85109 buf_len = Option.value ~default:t.buf_len buf_len;
86110 prompt = Option.value ~default:t.prompt prompt;
87111 plen = Option.value ~default:t.plen plen;
···95119 complete = (match complete with Some f -> Some f | None -> t.complete);
96120 read_buf = t.read_buf;
97121 completion_idx = Option.value ~default:t.completion_idx completion_idx;
122122+ history = Option.value ~default:t.history history;
123123+ saved_buf = Option.value ~default:t.saved_buf saved_buf;
124124+ hint = t.hint;
98125 }
99126end
100127···120147 c_vmin = 1;
121148 }
122149 in
123123- Unix.tcsetattr state.ifd TCSAFLUSH tio;
150150+ Unix.tcsetattr state.ifd TCSADRAIN tio;
124151 Fun.protect
125152 ~finally:(fun () -> Unix.tcsetattr state.ifd TCSADRAIN saved_tio)
126153 fn
···148175let edit_start ~stdin:_ ~stdout:_ state fn =
149176 with_raw_mode state @@ fun () ->
150177 let cols = get_columns () in
151151- Bytes.set state.buf 0 '\000';
178178+ (* Bytes.set state.buf 0 '\000'; *)
152179 let state = State.override ~cols ~buf_len:(state.buf_len - 1) state in
153180 write_bytes state.ofd state.prompt;
154181 fn state
···174201175202type refresh_flag = Rewrite
176203177177-let refresh_single_line ?(flags = []) (state : State.t) =
178178- let pwidth = utf8_display_width state.prompt state.plen in
204204+let refresh_with_hints ~pwidth ~ab (state : State.t) =
205205+ let buf_width = utf8_display_width state.buf state.len in
206206+ if pwidth + buf_width < state.cols then begin
207207+ match state.hint (State.buf state |> Bytes.to_string) with
208208+ | None -> ()
209209+ | Some (hint, style) ->
210210+ let () =
211211+ Format.fprintf Format.str_formatter "%a"
212212+ Fmt.(styled style string)
213213+ hint
214214+ in
215215+ Buffer.add_string ab (Format.flush_str_formatter ())
216216+ end
217217+218218+let refresh_single_line ?(flags = []) ?prompt (state : State.t) =
219219+ let prompt = match prompt with None -> state.prompt | Some p -> p in
220220+ let pwidth = utf8_display_width prompt state.plen in
179221 let poscol = ref @@ utf8_display_width state.buf state.pos in
180222 let lencol = ref @@ utf8_display_width state.buf state.len in
181223···202244203245 (* Add prompt *)
204246 if List.mem Rewrite flags then begin
205205- Buffer.add_bytes ab state.prompt;
247247+ Buffer.add_bytes ab prompt;
206248 Buffer.add_bytes ab (Bytes.sub state.buf 0 state.len)
207249 end;
250250+251251+ refresh_with_hints ~pwidth:state.len ~ab state;
208252209253 (* Erase to the right *)
210254 Buffer.add_string ab "\x1b[0K";
···248292 < state.cols
249293 then begin
250294 write_uchar state.ofd c;
251251- state
295295+ refresh_line state
252296 end
253297 else refresh_line state
254298 end
255299 else begin
256256- assert false
300300+ Bytes.blit state.buf state.pos state.buf (state.pos + clen)
301301+ (state.len - state.pos);
302302+ let _ : int = Bytes.set_utf_8_uchar state.buf state.pos c in
303303+ let state =
304304+ State.override ~len:(state.len + clen) ~pos:(state.pos + clen) state
305305+ in
306306+ refresh_line state
257307 end
258308259309let edit_backspace (state : State.t) =
···271321 refresh_line state
272322273323let complete_line (state : State.t) c cn =
274274- match cn (String.of_bytes state.buf) with
324324+ match cn (String.of_bytes (State.buf state)) with
275325 | [] -> (State.override ~in_completion:false state, `Char c)
276326 | xs ->
277327 let state, c =
···305355 (refresh_line state, c)
306356 end
307357308308-let edit_feed state =
358358+let move_left (state : State.t) =
359359+ let s =
360360+ if state.pos > 0 then
361361+ State.override
362362+ ~pos:(state.pos - utf8_prev_char_len state.buf state.pos)
363363+ state
364364+ else state
365365+ in
366366+ refresh_line s
367367+368368+let complete_with_hint (state : State.t) =
369369+ let buf = State.buf state |> Bytes.to_string in
370370+ match state.hint buf with
371371+ | None -> state
372372+ | Some (h, _) ->
373373+ let new_buf = buf ^ h in
374374+ let end_buf = String.length new_buf in
375375+ Bytes.blit_string new_buf 0 state.buf 0 end_buf;
376376+ State.override ~pos:end_buf ~len:end_buf state
377377+378378+let move_right (state : State.t) =
379379+ let s =
380380+ if state.pos < state.len then
381381+ State.override
382382+ ~pos:(state.pos + utf8_next_char_len state.buf state.pos)
383383+ state
384384+ else if state.pos = state.len then complete_with_hint state
385385+ else state
386386+ in
387387+ refresh_line s
388388+389389+let move_right_next_word (state : State.t) =
390390+ let pos = ref state.pos in
391391+ while !pos < state.len && Bytes.get state.buf !pos = ' ' do
392392+ incr pos
393393+ done;
394394+ while !pos < state.len && Bytes.get state.buf !pos <> ' ' do
395395+ incr pos
396396+ done;
397397+ let s = State.override ~pos:!pos state in
398398+ refresh_line s
399399+400400+let move_left_next_word (state : State.t) =
401401+ let pos = ref state.pos in
402402+ while !pos > 0 && Bytes.get state.buf !pos = ' ' do
403403+ decr pos
404404+ done;
405405+ while !pos > 0 && Bytes.get state.buf !pos <> ' ' do
406406+ decr pos
407407+ done;
408408+ let s = State.override ~pos:!pos state in
409409+ refresh_line s
410410+411411+let reverse_incr_search ~history (state : State.t) =
412412+ let has_match = ref true in
413413+ let search_buf = Buffer.create 16 in
414414+ let search_pos = ref 0 in
415415+ let search_dir = ref (-1) in
416416+ let h = history "" in
417417+ let history_len = List.length h in
418418+ let saved_buf = Bytes.copy state.buf in
419419+ let exception Completed of State.t in
420420+ let rec loop state : State.t =
421421+ let prompt =
422422+ if !has_match then
423423+ Fmt.str "(reverse-i-search)`%s': " (Buffer.contents search_buf)
424424+ else
425425+ Fmt.str "(failed-reverse-i-search)`%s': " (Buffer.contents search_buf)
426426+ in
427427+ let new_char = ref false in
428428+ let state = State.override ~pos:0 state in
429429+ let state =
430430+ refresh_single_line ~flags:[ Rewrite ] ~prompt:(String.to_bytes prompt)
431431+ state
432432+ in
433433+ let state =
434434+ match read_char state with
435435+ | `Editing -> loop state
436436+ | `None -> loop state
437437+ | `Some c -> (
438438+ match key_of_char c with
439439+ | Backspace ->
440440+ if Buffer.length search_buf > 0 then begin
441441+ (* Pretty wasteful... *)
442442+ let s = Buffer.contents search_buf in
443443+ Buffer.clear search_buf;
444444+ Buffer.add_substring search_buf s 0 (String.length s - 1);
445445+ search_pos := 0
446446+ end;
447447+ state
448448+ | Ctrl_p ->
449449+ search_dir := -1;
450450+ if !search_pos >= history_len then search_pos := history_len - 1;
451451+ state
452452+ | Ctrl_r ->
453453+ search_dir := 1;
454454+ if !search_pos < 0 then search_pos := 0;
455455+ state
456456+ | Ctrl_g ->
457457+ let l = Bytes.length saved_buf in
458458+ Bytes.blit saved_buf 0 state.buf 0 l;
459459+ let state = refresh_line (State.override ~pos:l ~len:l state) in
460460+ raise (Completed state)
461461+ | Enter ->
462462+ let state = State.override ~pos:state.len state in
463463+ raise (Completed state)
464464+ | _ ->
465465+ if Char.compare c ' ' > 0 then begin
466466+ new_char := true;
467467+ Buffer.add_char search_buf c;
468468+ search_pos := 0;
469469+ state
470470+ end
471471+ else
472472+ State.override ~pos:state.len state |> refresh_line |> fun s ->
473473+ raise (Completed s))
474474+ in
475475+ has_match := false;
476476+ let state =
477477+ if Buffer.length search_buf > 0 then begin
478478+ let rec inner_loop () =
479479+ if !search_pos >= 0 && !search_pos < history_len then begin
480480+ let entry = List.nth h !search_pos in
481481+ match
482482+ ( Astring.String.cut ~sep:(Buffer.contents search_buf) entry,
483483+ !new_char
484484+ || not
485485+ @@ String.equal entry (Bytes.to_string (State.buf state)) )
486486+ with
487487+ | Some (_l, _r), true ->
488488+ has_match := true;
489489+ Bytes.blit_string entry 0 state.buf 0 (String.length entry);
490490+ let state = State.override ~len:(String.length entry) state in
491491+ state
492492+ | _ ->
493493+ search_pos := !search_pos + !search_dir;
494494+ inner_loop ()
495495+ end
496496+ else state
497497+ in
498498+ inner_loop ()
499499+ end
500500+ else state
501501+ in
502502+ loop state
503503+ in
504504+ try loop state with Completed state -> state
505505+506506+let edit_history dir fn (state : State.t) =
507507+ let saved_state = state in
508508+ let current_buf = Bytes.sub_string state.buf 0 state.len in
509509+ let state =
510510+ match (dir, state.history_index) with
511511+ | `Prev, -1 ->
512512+ State.override ~history:(fn current_buf) ~history_index:0
513513+ ~saved_buf:current_buf state
514514+ | `Prev, m ->
515515+ let max_history = List.length state.history in
516516+ if m < max_history - 1 then
517517+ State.override ~history_index:(state.history_index + 1) state
518518+ else state
519519+ | `Next, m when m >= 0 ->
520520+ State.override ~history_index:(state.history_index - 1) state
521521+ | _ -> state
522522+ in
523523+ match (state.history, state.history_index) with
524524+ | [], _ -> saved_state
525525+ | _, -1 ->
526526+ let len = String.length state.saved_buf in
527527+ State.override ~buf:(Bytes.of_string state.saved_buf) ~pos:len ~len state
528528+ |> refresh_line
529529+ | _ ->
530530+ let max_history = List.length state.history in
531531+ let idx = min max_history state.history_index in
532532+ let s = List.nth state.history idx in
533533+ let s_len = String.length s in
534534+ State.override ~buf:(Bytes.of_string s) ~pos:s_len ~len:s_len state
535535+ |> refresh_line
536536+537537+let edit_feed ~history state =
309538 match read_char state with
310539 | `Editing -> Editing state
311540 | `None -> Finished None
···322551 | `Edit_more -> Editing state
323552 | `Char c -> (
324553 match key_of_char c with
325325- | Enter -> Finished (Some state.buf)
554554+ | Enter -> Finished (Some (Bytes.sub state.buf 0 state.len))
326555 | Ctrl_d ->
327327- if Int.equal state.len 0 then Finished None else assert false
556556+ if Int.equal state.len 0 then Finished None else Editing state
328557 | Ctrl_c -> Ctrl_c
558558+ | Ctrl_b -> Editing (move_left state)
559559+ | Ctrl_f -> Editing (move_right state)
560560+ | Ctrl_r -> Editing (reverse_incr_search ~history state)
329561 | Backspace -> Editing (edit_backspace state)
330562 | Tab -> Editing state
331331- | Unknown _ | _ ->
563563+ | Escape_sequence -> (
564564+ let c0 =
565565+ read_char state |> function `Some c -> c | _ -> assert false
566566+ in
567567+ match c0 with
568568+ | '[' ->
569569+ let c1 =
570570+ read_char state |> function
571571+ | `Some c -> c
572572+ | _ -> assert false
573573+ in
574574+ if Char.compare c1 '0' >= 0 && Char.compare c1 '9' <= 0 then
575575+ let c2 =
576576+ read_char state |> function
577577+ | `Some c -> c
578578+ | _ -> assert false
579579+ in
580580+ let c3 =
581581+ match read_char state with
582582+ | `Some c -> Some c
583583+ | (exception _) | _ -> None
584584+ in
585585+ let c4 =
586586+ match read_char state with
587587+ | `Some c -> Some c
588588+ | (exception _) | _ -> None
589589+ in
590590+ match (c2, c3) with
591591+ | ';', Some '5' -> (
592592+ match c4 with
593593+ | Some 'D' -> Editing (move_left_next_word state)
594594+ | Some 'C' -> Editing (move_right_next_word state)
595595+ | _ -> Editing state)
596596+ | _ -> Editing state
597597+ else begin
598598+ match c1 with
599599+ | 'A' -> Editing (edit_history `Prev history state)
600600+ | 'B' -> Editing (edit_history `Next history state)
601601+ | 'C' -> Editing (move_right state)
602602+ | 'D' -> Editing (move_left state)
603603+ | _ -> Editing state
604604+ end
605605+ | _ -> Editing state)
606606+ | _ ->
332607 let state = edit_insert state uc in
333608 Editing state))
334609335610type result = String of string option | Ctrl_c
336611337337-let blocking_edit ?complete ~stdin ~stdout buf ~prompt =
338338- let state = State.make ?complete ~prompt buf in
612612+let blocking_edit ?complete ~history ~hint ~stdin ~stdout buf ~prompt =
613613+ let state = State.make ?complete ~hint ~prompt buf in
339614 let res =
340615 edit_start ~stdin ~stdout state @@ fun state ->
341616 let rec loop = function
342342- | Editing state -> loop (edit_feed state)
617617+ | Editing state -> loop (edit_feed ~history state)
343618 | Finished s -> String (Option.map Bytes.to_string s)
344619 | Ctrl_c -> Ctrl_c
345620 in
346346- loop (edit_feed state)
621621+ loop (edit_feed ~history state)
347622 in
348348- Format.printf "\n%!";
349623 res
350624351351-let bruit ?complete prompt =
625625+type history = string -> string list
626626+627627+let bruit ?complete ?(history = fun _ -> []) ?(hint = fun _ -> None) prompt =
352628 let prompt = Bytes.of_string prompt in
353353- let buf = Bytes.create max_line in
629629+ let buf = Bytes.make max_line '\000' in
354630 if not (Unix.isatty Unix.stdin) then failwith "Stdin is not a tty"
355355- else blocking_edit ?complete ~stdin:Unix.stdin ~stdout:Unix.stdout buf ~prompt
631631+ else
632632+ blocking_edit ?complete ~history ~hint ~stdin:Unix.stdin ~stdout:Unix.stdout
633633+ buf ~prompt
356634357635(*
358636 * Copyright (c) 2010-2023, Salvatore Sanfilippo <antirez at gmail dot com>
+14-1
src/bruit.mli
···4455 The main entry point to the library is {! bruit}. *)
6677+type history = string -> string list
88+(** The history callback that provides the user with the current line and
99+ expects a list of history items to scroll through using the arrow keys. *)
1010+1111+type hint = string -> (string * Fmt.style) option
1212+(** The hint callback takes the current input and a user can return, optionally,
1313+ extra information to fill in on the current line. *)
1414+715type result = String of string option | Ctrl_c
81699-val bruit : ?complete:(string -> string list) -> string -> result
1717+val bruit :
1818+ ?complete:(string -> string list) ->
1919+ ?history:history ->
2020+ ?hint:hint ->
2121+ string ->
2222+ result
1023(** [bruit ?complete prompt] reads from [stdin] and returns the read string if
1124 any, and on [ctrl+c] returns {! Ctrl_c}.
1225