From 482e8b80fa66e328e252567c915f5e96e727f7cf Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Tue, 23 Jan 2024 18:08:56 +0100 Subject: feat: add simple status bar with timed status message support --- bin/main.ml | 2 +- lib/editor.ml | 85 +++++++++++++++++++++++++++++++++++++++++++++++++---- lib/editorBuffer.ml | 14 ++++++++- lib/terminal.ml | 9 ++++++ lib/terminal.mli | 21 +++++++++++++ lib/util.ml | 7 +++++ 6 files changed, 131 insertions(+), 7 deletions(-) diff --git a/bin/main.ml b/bin/main.ml index 932c452..e763f70 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -4,5 +4,5 @@ let () = let open Editor in let cli = Config.parse Sys.argv in let editor = Editor.init cli in - let rec loop () = Action.(render *> handle_next_command >>= loop) in + let rec loop () = Action.(render *> handle_next_command *> tick >>= loop) in Action.eval ~editor loop diff --git a/lib/editor.ml b/lib/editor.ml index 69dc666..c34d558 100644 --- a/lib/editor.ml +++ b/lib/editor.ml @@ -2,7 +2,7 @@ open Base module Buffer = EditorBuffer open Util -type mode = Normal | Insert +type mode = Normal | Insert | Control type cursor = int * int type editor = { @@ -15,6 +15,10 @@ type editor = { pending : Key.t Sequence.t; i_pending : Command.t Sequence.t; n_pending : Command.t Sequence.t; + status_size : int; + message : string option; + message_timestamp : float; + message_duration : float; } type t = editor @@ -30,8 +34,57 @@ let init (c : Config.t) : editor = pending = Key.stream; i_pending = Command.i_stream; n_pending = Command.n_stream; + status_size = 2; + message = Some "Hello, control line!"; + message_timestamp = Unix.time (); + message_duration = 5.; } +let string_of_mode = function + | Insert -> " I " + | Normal -> " N " + | Control -> " C " + +let statusbar e = + let open Text in + (* let open Sequence.Infix in *) + let w = e.term.size |> snd in + let status = + let mode = e.mode |> string_of_mode |> sequence_of_string in + let msize = Sequence.length mode + and path = + Buffer.( + e.buffer |> Option.map ~f:kind + |> Option.value ~default:No_name + |> string_of_kind |> sequence_of_string) + and br, bc = + Option.(e.buffer |> map ~f:Buffer.size |> value ~default:(0, 0)) + and cr, cc = + Option.( + e.buffer + |> map ~f:(Buffer.cursor ~rendered:false) + |> value ~default:(0, 0)) + in + let perc = + match cr with + | 0 -> "Top" + | n when n = br -> "Bot" + | n -> Printf.sprintf "%2d%%" (100 * n / br) + in + let nav = + Printf.sprintf "%d/%d %2d/%2d [%s] " cr br cc bc perc + |> sequence_of_string + in + let nsize = Sequence.length nav in + spread ~l:(bold mode) ~lsize:msize ~c:path ~r:(bold nav) ~rsize:nsize + ~fill:' ' w + |> invert + and control = + let msg = Option.value ~default:"" e.message |> sequence_of_string in + spread ~l:msg ~fill:' ' w + in + Sequence.(take (of_list [ status; control ]) e.status_size) + type 'a action = t -> 'a * t module Action = struct @@ -71,6 +124,8 @@ module Action = struct let update_cursor = let aux e = let dx, dy = e.offset and rs, cs = e.term.size in + (* Limit cursor to buffer view *) + let rs = rs - e.status_size in match Option.map ~f:Buffer.cursor e.buffer with | None -> { e with cursor = (1, 1); offset = (0, 0) } | Some (cx, cy) -> @@ -107,22 +162,41 @@ module Action = struct let x, y = e.offset and ((r, c) as size) = e.term.size and fill = Sequence.singleton '~' + and status = statusbar e and limit = Buffer.(if e.rendered then rendered_view else unrendered_view) in - let view = + let ssize = e.status_size in + let bufview = Option.( - e.buffer >>| limit x y r c + e.buffer + >>| limit x y (r - ssize) c |> value ~default:(welcome size) - |> Text.extend ~fill r) + |> Text.extend ~fill r + |> Fn.flip Sequence.take (r - ssize)) in - Terminal.redraw view e.cursor + let screen = Sequence.append bufview status in + Terminal.redraw screen e.cursor in get >>| aux (* TODO: save logic *) let quit n = Stdlib.exit n + (* Statusbar *) + let set_message m e = + ((), { e with message = m; message_timestamp = Unix.time () }) + + + let tick = + let check_message_timestamp e = + let now = Unix.time () in + let expired = Float.(e.message_timestamp < now - e.message_duration) in + if Option.is_some e.message && expired then { e with message = None } + else e + in + get >>| check_message_timestamp >>= put + (* Debug *) let get_rendered e = (e.rendered, e) let set_rendered r e = ((), { e with rendered = r }) @@ -376,6 +450,7 @@ let handle_next_command m e = match Sequence.next e.n_pending with | None -> ((), e) | Some (h, t) -> handle_normal_command h { e with n_pending = t }) + | Control -> failwith "unimplemented" let handle_next_command = let open Action in diff --git a/lib/editorBuffer.ml b/lib/editorBuffer.ml index e1706dd..c492b8b 100644 --- a/lib/editorBuffer.ml +++ b/lib/editorBuffer.ml @@ -21,6 +21,18 @@ let empty = rendered = push Sequence.empty empty; } +let kind b = b.kind + +let string_of_kind = function + | No_name -> "[No Name]" + | Scratch -> "[Scratch]" + | File name -> name + +let size e = + match e.content with + | Error _ -> (0, 0) + | Ok z -> (length z, apply_focus_or ~default:0 length z) + let render = let open Sequence in let tabsize = 8 in @@ -235,7 +247,7 @@ let unrendered_view x y h w b = let rendered_view x y h w b = let window from len seq = Sequence.(take (drop_eagerly seq from) len) in - let cx, _ = cursor b in + let cx, _ = cursor ~rendered:false b in context ~l:(cx - x) ~r:(x + h - cx) b.rendered |> to_seq |> Sequence.map ~f:(window y w) diff --git a/lib/terminal.ml b/lib/terminal.ml index c8312b6..96103f5 100644 --- a/lib/terminal.ml +++ b/lib/terminal.ml @@ -31,6 +31,15 @@ let show_cursor show = let cmd = if show then 'h' else 'l' in escape cmd ~prefix:"?" ~args:[ 25 ] +(* Incomplete. See https://stackoverflow.com/a/33206814 for a full list + of escape codes related to terminal formatting capabilities *) +let fmt_reset = escape 'm' +let fmt_bold_on = escape ~args:[ 1 ] 'm' +let fmt_bold_off = escape ~args:[ 22 ] 'm' +let fmt_underline = escape ~args:[ 4 ] 'm' +let fmt_blink = escape ~args:[ 5 ] 'm' +let fmt_inverted_on = escape ~args:[ 7 ] 'm' +let fmt_inverted_off = escape ~args:[ 27 ] 'm' let input_bytes = Bytes.create 1 let get_char () = diff --git a/lib/terminal.mli b/lib/terminal.mli index 0fd11ed..cf8f61d 100644 --- a/lib/terminal.mli +++ b/lib/terminal.mli @@ -13,6 +13,27 @@ type state = { } (** Global state of the terminal window. *) +val fmt_reset : char Sequence.t +(** Escape sequence: reset text formatting *) + +val fmt_bold_on : char Sequence.t +(** Escape sequence: turn on bold text*) + +val fmt_bold_off : char Sequence.t +(** Escape sequence: turn off bold text*) + +val fmt_underline : char Sequence.t +(** Escape sequence: underlined text*) + +val fmt_blink : char Sequence.t +(** Escape sequence: blinking text*) + +val fmt_inverted_on : char Sequence.t +(** Escape sequence: turn on inverted text*) + +val fmt_inverted_off : char Sequence.t +(** Escape sequence: turn off inverted text*) + val get_char : unit -> char option (** Non-blocking request for a keypress. Use {!val:Terminal.char_stream} for an infinite sequence of input diff --git a/lib/util.ml b/lib/util.ml index 9ad3b59..b86119b 100644 --- a/lib/util.ml +++ b/lib/util.ml @@ -24,3 +24,10 @@ let sequence_of_bytes (b : Bytes.t) : char Sequence.t = loop 0 () in traverse b |> run + +(** Turn a string into a sequence. + + @param s the input string. + @return a sequence of bytes. *) +let sequence_of_string (s : string) : char Sequence.t = + s |> String.to_list |> Sequence.of_list -- cgit v1.2.3