let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty module Make (S : Types.State) (E : Types.Exec) (H : Types.History) = struct module Eval = Eval.Make (S) (E) let pp_colored c pp fmt v = Fmt.pf fmt "%a" (Fmt.styled (`Fg c) pp) v let subst_tilde path = match Eunix.find_env "HOME" with | None -> path | Some home -> ( match Fpath.rem_prefix (Fpath.v home) path with | Some rel -> Fpath.(v "~" // rel) | None -> path) let default_prompt (ctx : Eval.ctx Exit.t) = let state = match ctx with | Exit.Zero ctx | Exit.Nonzero { value = ctx; _ } -> Eval.state ctx in let pp_status ppf = function | Exit.Zero _ -> () | Exit.Nonzero { exit_code; _ } -> Fmt.pf ppf "[%a] " (pp_colored `Red Fmt.int) exit_code in let fs = Exit.value ctx |> Eval.fs in Fmt.pf Format.str_formatter "%a%a:%s >\n%!" pp_status ctx Fmt.(pp_colored `Yellow string) (Eunix.get_user_and_host fs) (Fpath.normalize @@ S.cwd state |> subst_tilde |> Fpath.to_string); Format.flush_str_formatter () let resolve_programs name = if not (String.contains name '/') then begin Eunix.find_env "PATH" |> Option.value ~default:"/bin:/usr/bin" |> String.split_on_char ':' |> List.concat_map (fun dir -> let files = try Sys.readdir dir |> Array.to_list with _ -> [] in List.filter (fun f -> String.starts_with ~prefix:name f) files) end else if Sys.file_exists name then [ name ] else [] let complete path = let rest, last_arg = String.split_on_char ' ' path |> List.rev |> function | [] -> ([], `None) | [ prog ] -> ([], `Prog prog) | x :: rest -> (List.rev rest, `Path x) in let completions from_path basename = match (Unix.stat from_path).st_kind with | S_DIR -> let entries = Sys.readdir from_path |> Array.to_list in List.filter_map (fun e -> match basename with | None -> Some e | Some prefix -> if String.starts_with ~prefix e then Some e else None) entries |> List.map (fun e -> Filename.concat from_path e) |> List.map (fun p -> String.concat " " (rest @ [ p ])) | S_REG -> [ String.concat " " (rest @ [ path ]) ] | _ -> [] | exception Unix.Unix_error (Unix.ENOTDIR, _, _) -> [] | exception Unix.Unix_error (Unix.ENOENT, _, _) -> [] in match last_arg with | `None -> [] | `Prog prog -> resolve_programs prog | `Path path -> ( match Unix.(stat path).st_kind with | exception Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) -> let dirname = Filename.dirname path in let basename = match Filename.basename path with "." | "" -> None | p -> Some p in completions dirname basename | S_DIR -> completions path None | _ -> []) let run ?(prompt = default_prompt) initial_ctx = Sys.set_signal Sys.sigttou Sys.Signal_ignore; Sys.set_signal Sys.sigttin Sys.Signal_ignore; Sys.set_signal Sys.sigtstp Sys.Signal_ignore; Sys.set_signal Sys.sigint Sys.Signal_ignore; let xdg = Xdge.create (Eval.fs (Exit.value initial_ctx)) "merry" in let history = Eio.Path.(Xdge.data_dir xdg / ".merry_history") in let initial_history = try H.load history with _ -> H.empty in let h = ref initial_history in let add_history c = let c = String.trim c in h := H.add (H.make_entry c) !h in let rec loop (ctx : Eval.ctx Exit.t) = Option.iter (Fmt.epr "%s%!") (S.lookup (Exit.value ctx |> Eval.state) ~param:"PS1"); let p = prompt ctx in Fmt.pr "%s\r%!" p; let hint command = if String.length command < 2 then None else match H.history ~command !h |> H.commands with | [] -> None | x :: _ -> ( match Astring.String.cut ~sep:command x with | Some ("", rest) -> Some (rest, `Fg (`Hi `Magenta)) | _ -> None) in match Bruit.bruit ~hint ~history:(fun command -> H.history ~command !h |> H.commands) ~complete "% " with | String None -> Fmt.pr "exit\n%!"; exit 0 | String (Some c) -> let ast = Ast.of_string (String.trim c) in Fmt.pr "\n%!"; let ctx', _ast = Eval.run ctx ast in add_history c; H.save !h history; loop ctx' | Ctrl_c -> let c = Exit.value ctx in Eunix.Signals.(raise Interrupt); if Eval.sigint_set c then loop (Exit.zero c) else begin Fmt.pr "\n%!"; loop (Exit.nonzero c 130) end in loop initial_ctx end