diff --git a/example/dune b/example/dune --- a/example/dune +++ b/example/dune @@ -1,3 +1,3 @@ (executable (name main) - (libraries bruit terminal fmt.tty)) + (libraries bruit bruit.unix terminal fmt.tty)) diff --git a/example/main.ml b/example/main.ml --- a/example/main.ml +++ b/example/main.ml @@ -16,10 +16,7 @@ let prompt = if sys_break then "[\x1b[31m130\x1b[0m] \x1b[33m>>\x1b[0m " else "\x1b[33m>>\x1b[0m " in - let ctx = - Bruit.make_ctx ~get_columns:Terminal.Size.get_columns - ~guess_printed_width:Terminal.guess_printed_width - in + let ctx = Bruit_unix.stdctx () in match Bruit.bruit ~history ~complete ctx prompt with | String (Some s) -> Fmt.pr "%s\n%!" s; diff --git a/src/bruit.ml b/src/bruit.ml --- a/src/bruit.ml +++ b/src/bruit.ml @@ -37,22 +37,32 @@ | 27 -> Escape_sequence | 127 -> Backspace | _ -> Unknown (Uchar.of_char c) -type ctx = { +type 'tio ctx = { get_columns : unit -> int option; guess_printed_width : string -> int; + read : bytes -> off:int -> len:int -> int; + write : bytes -> off:int -> len:int -> int; + enter_raw_mode : unit -> 'tio; + exit_raw_mode : 'tio -> unit; } -let make_ctx = - fun ~get_columns -> - fun ~guess_printed_width -> { get_columns; guess_printed_width } +let make_ctx ~get_columns ~guess_printed_width ~read ~write ~enter_raw_mode + ~exit_raw_mode = + { + get_columns; + guess_printed_width; + read; + write; + enter_raw_mode; + exit_raw_mode; + } module State = struct type completion = string -> string list + type ctxt = Ctxt : _ ctx -> ctxt type t = { - ifd : Unix.file_descr; - ofd : Unix.file_descr; - ctx : ctx; + ctxt : ctxt; buf : bytes; buf_len : int; prompt : bytes; @@ -74,17 +84,30 @@ complete : completion option; hint : hint; } + let read t = + let (Ctxt c) = t.ctxt in + c.read + + let write t = + let (Ctxt c) = t.ctxt in + c.write + + let get_columns t = + let (Ctxt c) = t.ctxt in + c.get_columns () + + let guess_printed_width t = + let (Ctxt c) = t.ctxt in + c.guess_printed_width + let buf t = Bytes.sub t.buf 0 t.len let make ?(in_completion = false) ?(completion_idx = 0) ?complete ?(old_pos = 0) ?(pos = 0) ?(len = 0) ?(history = []) - ?(hint = fun _ -> None) ?(ifd = Unix.stdin) ?(ofd = Unix.stdout) ~prompt - ctx buf = + ?(hint = fun _ -> None) ~prompt ctx buf = { in_completion; - ctx; - ifd; - ofd; + ctxt = Ctxt ctx; buf; buf_len = Bytes.length buf; prompt; @@ -104,9 +127,9 @@ read_buf = Bytes.make 1 '\000' (* For reading a character *); hint; } - 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 ?history ?saved_buf (t : t) = + let override ?in_completion ?completion_idx ?complete ?buf ?buf_len ?prompt + ?plen ?old_pos ?pos ?len ?cols ?old_rows ?old_row_pos ?history_index + ?history ?saved_buf (t : t) = let () = match buf with | None -> () @@ -114,8 +137,6 @@ | 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 = t.buf; buf_len = Option.value ~default:t.buf_len buf_len; prompt = Option.value ~default:t.prompt prompt; @@ -133,65 +154,46 @@ 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; hint = t.hint; - ctx = t.ctx; + ctxt = t.ctxt; } end let with_raw_mode (state : State.t) fn = - let saved_tio = Unix.tcgetattr state.ifd in - let tio : Unix.terminal_io = - { - saved_tio with - c_brkint = false; - c_icrnl = false; - c_inpck = false; - c_istrip = false; - c_ixon = false; - c_opost = false; - c_csize = 8; - c_echo = false; - c_icanon = false; - c_isig = false; - c_vtime = 0; - c_vmin = 1; - } - in - Unix.tcsetattr state.ifd TCSADRAIN tio; - Fun.protect - ~finally:(fun () -> Unix.tcsetattr state.ifd TCSADRAIN saved_tio) - fn + let (State.Ctxt c) = state.ctxt in + let tio = c.enter_raw_mode () in + Fun.protect ~finally:(fun () -> c.exit_raw_mode tio) fn -let write_bytes fd s = +let write_bytes (state : State.t) s = let len = Bytes.length s in - let wrote = Unix.write fd s 0 len in + let wrote = State.write state s ~off:0 ~len in assert (Int.equal len wrote) -let write_uchar fd u = +let write_uchar (state : State.t) u = let b_len = Uchar.utf_8_byte_length u in let bs = Bytes.create b_len in let wrote = Bytes.set_utf_8_uchar bs 0 u in assert (Int.equal b_len wrote); - write_bytes fd bs + write_bytes state bs type edit = Editing of State.t | Finished of bytes option | Ctrl_c -let read_char state = - try - let read = Unix.read state.State.ifd state.read_buf 0 1 in - if read = 0 then `None else `Some (Bytes.unsafe_get state.read_buf 0) - with Unix.Unix_error ((Unix.EWOULDBLOCK | Unix.EAGAIN), _, _) -> `Editing +let read_char (state : State.t) = + (* try *) + let read = State.read state state.read_buf ~off:0 ~len:1 in + if read = 0 then `None else `Some (Bytes.unsafe_get state.read_buf 0) +(* with Unix.Unix_error ((Unix.EWOULDBLOCK | Unix.EAGAIN), _, _) -> `Editing *) let edit_start ~stdin:_ ~stdout:_ state fn = with_raw_mode state @@ fun () -> - let cols = state.ctx.get_columns () |> Option.value ~default:80 in + let cols = State.get_columns state |> Option.value ~default:80 in (* Bytes.set state.buf 0 '\000'; *) let state = State.override ~cols ~buf_len:(state.buf_len - 1) state in - write_bytes state.ofd state.prompt; + write_bytes state state.prompt; fn state let utf8_display_width state b len = let s = Bytes.to_string b in - state.State.ctx.guess_printed_width (String.sub s 0 len) + State.guess_printed_width state (String.sub s 0 len) let utf8_next_char_len s off = Bytes.get_utf_8_uchar s off @@ -266,7 +268,7 @@ (* Cursor to the original position *) if List.mem Rewrite flags then begin Buffer.add_string ab (Format.sprintf "\r\x1b[%dC" (!poscol + pwidth)) end; - write_bytes state.ofd (Buffer.to_bytes ab); + write_bytes state (Buffer.to_bytes ab); state let refresh_line state = refresh_single_line ~flags:[ Rewrite ] state @@ -300,7 +302,7 @@ utf8_display_width state state.prompt state.plen + utf8_display_width state state.buf state.len < state.cols then begin - write_uchar state.ofd c; + write_uchar state c; refresh_line state end else refresh_line state @@ -642,7 +644,7 @@ Editing state)) type result = String of string option | Ctrl_c -let blocking_edit ?complete ~history ~hint ~stdin ~stdout ctx buf ~prompt = +let blocking_edit ?complete ~history ~hint ctx buf ~prompt = let state = State.make ?complete ~hint ~prompt ctx buf in let res = edit_start ~stdin ~stdout state @@ fun state -> @@ -662,8 +664,7 @@ = 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" *) - blocking_edit ?complete ~history ~hint ~stdin:Unix.stdin ~stdout:Unix.stdout - ctx buf ~prompt + blocking_edit ?complete ~history ~hint ctx buf ~prompt (* * Copyright (c) 2010-2023, Salvatore Sanfilippo diff --git a/src/bruit.mli b/src/bruit.mli --- a/src/bruit.mli +++ b/src/bruit.mli @@ -14,17 +14,23 @@ extra information to fill in on the current line. *) type result = String of string option | Ctrl_c -type ctx +type 'terminal_io ctx (** The context is an abstraction that provides OS-specific backend details. *) val make_ctx : - get_columns:(unit -> int option) -> guess_printed_width:(string -> int) -> ctx + get_columns:(unit -> int option) -> + guess_printed_width:(string -> int) -> + read:(bytes -> off:int -> len:int -> int) -> + write:(bytes -> off:int -> len:int -> int) -> + enter_raw_mode:(unit -> 'tio) -> + exit_raw_mode:('tio -> unit) -> + 'tio ctx val bruit : ?complete:(string -> string list) -> ?history:history -> ?hint:hint -> - ctx -> + 'tio ctx -> string -> result (** [bruit ?complete prompt] reads from [stdin] and returns the read string if diff --git a/src/dune b/src/dune --- a/src/dune +++ b/src/dune @@ -1,4 +1,4 @@ (library (public_name bruit) - (libraries fmt unix astring) + (libraries fmt astring) (name bruit)) diff --git a/src/unix/bruit_unix.ml b/src/unix/bruit_unix.ml new file mode 100644 --- /dev/null +++ b/src/unix/bruit_unix.ml @@ -0,0 +1,34 @@ +let enter_raw_mode stdin () = + let saved_tio = Unix.tcgetattr stdin in + let tio : Unix.terminal_io = + { + saved_tio with + c_brkint = false; + c_icrnl = false; + c_inpck = false; + c_istrip = false; + c_ixon = false; + c_opost = false; + c_csize = 8; + c_echo = false; + c_icanon = false; + c_isig = false; + c_vtime = 0; + c_vmin = 1; + } + in + Unix.tcsetattr stdin TCSADRAIN tio; + saved_tio + +let exit_raw_mode stdin saved_tio = Unix.tcsetattr stdin TCSADRAIN saved_tio +let read stdin b ~off ~len = Unix.read stdin b off len +let write stdout b ~off ~len = Unix.write stdout b off len + +let stdctx ?(stdin = Unix.stdin) ?(stdout = Unix.stdout) () = + let read = read stdin in + let write = write stdout in + let enter_raw_mode = enter_raw_mode stdin in + let exit_raw_mode = exit_raw_mode stdin in + Bruit.make_ctx ~read ~write ~enter_raw_mode ~exit_raw_mode + ~get_columns:Terminal.Size.get_columns + ~guess_printed_width:Terminal.guess_printed_width diff --git a/src/unix/bruit_unix.mli b/src/unix/bruit_unix.mli new file mode 100644 --- /dev/null +++ b/src/unix/bruit_unix.mli @@ -0,0 +1,12 @@ +(** {1 Bruit for Unix} + + This package provides a standard {! Bruit.ctx} for Unix platforms. *) + +val stdctx : + ?stdin:Unix.file_descr -> + ?stdout:Unix.file_descr -> + unit -> + Unix.terminal_io Bruit.ctx +(** [stdctx ()] will by default use {! Unix.stdin} and {! Unix.stdout}. It is up + to the callee to pass sensible arguments if they choose to not go with the + defaults. *) diff --git a/src/unix/dune b/src/unix/dune new file mode 100644 --- /dev/null +++ b/src/unix/dune @@ -0,0 +1,5 @@ +(library + (name bruit_unix) + (public_name bruit.unix) + (optional) + (libraries terminal bruit unix))