Shells in OCaml

History abstraction

A simple history abstraction that now uses XDG for persisting the
history to a file and loading it during interactive mode.

+91 -20
+1
dune-project
··· 25 25 visitors 26 26 re 27 27 terminal 28 + xdge 28 29 (menhir 29 30 (= 20250912)) 30 31 (yojson
+4 -2
merry.opam
··· 13 13 "visitors" 14 14 "re" 15 15 "terminal" 16 + "xdge" 16 17 "menhir" {= "20250912"} 17 18 "yojson" {= "2.2.2"} 18 19 "ppxlib" {>= "0.37.0"} ··· 42 43 dev-repo: "git+https://tangled.org/patrick.sirref.org/merry" 43 44 x-maintenance-intent: ["(latest)"] 44 45 pin-depends:[ 45 - [ "eio.dev" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 46 - [ "eio_posix.dev" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 46 + [ "eio.1.3" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 47 + [ "eio_posix.1.3" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 48 + [ "xdge.1.0.1" "https://tangled.org/anil.recoil.org/xdge/archive/main" ] 47 49 ]
+3 -2
merry.opam.template
··· 1 1 pin-depends:[ 2 - [ "eio.dev" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 3 - [ "eio_posix.dev" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 2 + [ "eio.1.3" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 3 + [ "eio_posix.1.3" "git+https://github.com/ocaml-multicore/eio#c44ee5ce96c120b7ccc23a12d241dc8672e2888f" ] 4 + [ "xdge.1.0.1" "https://tangled.org/anil.recoil.org/xdge/archive/main" ] 4 5 ]
+4 -1
src/bin/main.ml
··· 1 1 (* A shell... one day *) 2 2 module C = Merry.Eval.Make (Merry_posix.State) (Merry_posix.Exec) 3 - module I = Merry.Interactive.Make (Merry_posix.State) (Merry_posix.Exec) 3 + 4 + module I = 5 + Merry.Interactive.Make (Merry_posix.State) (Merry_posix.Exec) 6 + (Merry.History.Prefix_search) 4 7 5 8 let sh ~command ~dump ~file ~rest env = 6 9 let executor = Merry_posix.Exec.{ mgr = env#process_mgr } in
+1
src/lib/dune
··· 21 21 bruit 22 22 fpath 23 23 cmdliner 24 + xdge 24 25 merry.glob))
+1
src/lib/eval.ml
··· 73 73 74 74 let state ctx = ctx.state 75 75 let sigint_set ctx = ctx.signal_handler.sigint_set 76 + let fs ctx = ctx.fs 76 77 let clear_local_state ctx = { ctx with local_state = [] } 77 78 78 79 let rec tilde_expansion ctx = function
+3
src/lib/eval.mli
··· 32 32 E.t -> 33 33 ctx 34 34 35 + val fs : ctx -> Eio.Fs.dir_ty Eio.Path.t 36 + (** The file system capability *) 37 + 35 38 val state : ctx -> S.t 36 39 (** Return the current state of the context. *) 37 40
+24
src/lib/history.ml
··· 1 + (* Some history implementation that can be used out of the box. *) 2 + module Prefix_search : Types.History = struct 3 + type t = string list 4 + 5 + let empty = [] 6 + 7 + type entry = string 8 + 9 + let make_entry s = s 10 + let add e t = e :: t 11 + 12 + let history ~command h = 13 + if command <> "" then 14 + List.filter (fun s -> String.starts_with ~prefix:command s) h 15 + else h 16 + 17 + let commands t = t 18 + 19 + let save t path = 20 + Eio.Path.save ~create:(`If_missing 0o644) path (String.concat "\n" t) 21 + 22 + let load path = Eio.Path.load path |> String.split_on_char '\n' 23 + let pp ppf = Fmt.(list ~sep:(Fmt.any "\n") string) ppf 24 + end
+15 -15
src/lib/interactive.ml
··· 1 1 let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty 2 2 3 - module Make (S : Types.State) (E : Types.Exec) = struct 3 + module Make (S : Types.State) (E : Types.Exec) (H : Types.History) = struct 4 4 module Eval = Eval.Make (S) (E) 5 5 6 6 let pp_colored c pp fmt v = Fmt.pf fmt "%a" (Fmt.styled (`Fg c) pp) v ··· 65 65 | S_DIR -> completions path None 66 66 | _ -> []) 67 67 68 - (* For now a very simple, prefixed based history *) 69 - 70 - let h = ref [] 71 - 72 - let add_history c = 73 - let c = String.trim c in 74 - let new_h = List.filter (fun s -> not (String.equal c s)) !h in 75 - h := c :: new_h 76 - 77 - let history prefix = 78 - if prefix <> "" then List.filter (fun s -> String.starts_with ~prefix s) !h 79 - else !h 80 - 81 68 let run ?(prompt = default_prompt) initial_ctx = 82 69 Sys.set_signal Sys.sigttou Sys.Signal_ignore; 83 70 Sys.set_signal Sys.sigttin Sys.Signal_ignore; 84 71 Sys.set_signal Sys.sigtstp Sys.Signal_ignore; 85 72 Sys.set_signal Sys.sigint Sys.Signal_ignore; 73 + let xdg = Xdge.create (Eval.fs (Exit.value initial_ctx)) "merry" in 74 + let history = Eio.Path.(Xdge.data_dir xdg / ".merry_history") in 75 + let initial_history = try H.load history with _ -> H.empty in 76 + let h = ref initial_history in 77 + let add_history c = 78 + let c = String.trim c in 79 + h := H.add (H.make_entry c) !h 80 + in 86 81 let rec loop (ctx : Eval.ctx Exit.t) = 87 82 Option.iter (Fmt.epr "%s%!") 88 83 (S.lookup (Exit.value ctx |> Eval.state) ~param:"PS1" 89 84 |> Option.map Ast.word_components_to_string); 90 85 let p = prompt ctx in 91 86 Fmt.pr "%s\r%!" p; 92 - match Bruit.bruit ~history ~complete "" with 87 + match 88 + Bruit.bruit 89 + ~history:(fun command -> H.history ~command !h |> H.commands) 90 + ~complete "" 91 + with 93 92 | String None -> 94 93 Fmt.pr "exit\n%!"; 95 94 exit 0 ··· 98 97 Fmt.pr "\n%!"; 99 98 let ctx', _ast = Eval.run ctx ast in 100 99 add_history c; 100 + H.save !h history; 101 101 loop ctx' 102 102 | Ctrl_c -> 103 103 let c = Exit.value ctx in
+1
src/lib/merry.ml
··· 7 7 module Eval = Eval 8 8 module Interactive = Interactive 9 9 module Built_ins = Built_ins 10 + module History = History 10 11 11 12 module Variable = struct 12 13 type t
+1
src/lib/merry.mli
··· 6 6 module Eval = Eval 7 7 module Interactive = Interactive 8 8 module Built_ins = Built_ins 9 + module History = History 9 10 10 11 module Variable : sig 11 12 type t
+33
src/lib/types.ml
··· 128 128 (** Given a job, [await_exit] will wait for the job to finish and return the 129 129 exit based on the various options passed in. *) 130 130 end 131 + 132 + module type History = sig 133 + type t 134 + (** A history of commands *) 135 + 136 + val empty : t 137 + (** The empty history *) 138 + 139 + type entry 140 + (** A history entry, usually a command. *) 141 + 142 + val make_entry : string -> entry 143 + (** Construct an entry given a command. *) 144 + 145 + val add : entry -> t -> t 146 + (** Add an entry to the history. *) 147 + 148 + val save : t -> _ Eio.Path.t -> unit 149 + (** [save t path] should save history [t] to [path]. *) 150 + 151 + val load : _ Eio.Path.t -> t 152 + (** [load path] should try to read a history from [path]. *) 153 + 154 + val history : command:string -> t -> t 155 + (** [history ~command t] should return some subset of [t] (perhaps all of [t]) 156 + based on the current [command] to be used for history searching. *) 157 + 158 + val commands : t -> string list 159 + (** Converts your (perhaps richer) history to just a series of commands. *) 160 + 161 + val pp : t Fmt.t 162 + (** A pretty printer for the commands *) 163 + end