From 3f696169ab1a560d94d169c1a5b744346da4c081 Mon Sep 17 00:00:00 2001 From: Marc Coquand Date: Wed, 15 May 2024 14:05:03 -0500 Subject: Add done view + visual --- lib/done.ml | 141 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/grep.ml | 12 ++++- lib/headlines.ml | 23 +++++--- lib/help_screen.ml | 11 ++-- lib/stitch.ml | 35 +++++++++++- lib/stitched_article.ml | 15 ++++-- lib/todos.ml | 36 ++++++++----- 7 files changed, 240 insertions(+), 33 deletions(-) create mode 100644 lib/done.ml diff --git a/lib/done.ml b/lib/done.ml new file mode 100644 index 0000000..5daad60 --- /dev/null +++ b/lib/done.ml @@ -0,0 +1,141 @@ +module Grep = Grep +module Common = Common +open Notty +module Help_screen = Help_screen + +type state = + { pos : int * int + ; scroll : int + ; content : (string * string) array + ; content_pretty : string array + ; goto_headlines : (unit -> unit) -> unit + ; goto_todo : (unit -> unit) -> unit + } + +let title = I.strf ~attr:A.(st bold) "%s" "Done View" |> I.pad ~l:0 ~t:0 +let content_start = 2 + +let init ~goto_todo ~goto_headlines = + let content = Grep.get_done () |> Grep.parse_todo_string in + let content_pretty = Grep.pretty_format_todo content in + { pos = 0, content_start + ; scroll = 0 + ; content = content |> Array.of_list + ; content_pretty = content_pretty |> Array.of_list + ; goto_headlines + ; goto_todo + } + + +let load_done () = + let done_content = Grep.get_done () |> Grep.parse_todo_string in + let done_pretty = Grep.pretty_format_todo done_content in + done_content, done_pretty + + +let rec render + t + ({ pos; scroll; content; content_pretty; goto_headlines; goto_todo } as state) + = + let x, y = pos in + let content_position = y - content_start in + let img = + let dot = I.string A.(st bold) ">" |> I.pad ~l:0 ~t:(y - scroll) + and elements = + Array.mapi + (fun i el -> + if i == y - scroll + then I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start) + else I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start)) + (Array.to_seq content_pretty |> Seq.drop scroll |> Array.of_seq) + in + let open I in + Array.fold_left (fun sum el -> el sum) (title dot) elements + in + let _, size_y = Common.Term.size t in + Common.Term.image t img; + let content_end = Array.length content_pretty + (content_start - 1) in + let scroll_up () = + let scroll = if y - content_start - scroll = 0 then max (scroll - 1) 0 else scroll in + render t { state with pos = x, max (y - 1) content_start; scroll } + in + let scroll_down () = + let scroll = if y - scroll >= size_y - 1 then scroll + 1 else scroll in + render t { state with pos = x, min (y + 1) content_end; scroll } + in + match Common.Term.event t with + | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> () + | `Mouse (`Press (`Scroll s), _, _) -> + (match s with + | `Down -> scroll_down () + | `Up -> scroll_up ()) + | `Resize _ -> render t state + | `Mouse ((`Press _ | `Drag), (_, y), _) -> + render t { state with pos = 0, min y content_end } + | `Key (`ASCII '?', []) -> Help_screen.render t { go_back = (fun () -> render t state) } + | `Key (`ASCII 's', []) -> + let (input_state : Input_screen.state) = + { screen = img + ; user_input = "" + ; prompt = "GREP: " + ; on_enter = + (fun tag -> + let content = Grep.get_tagged_headlines tag () |> Grep.parse_headlines in + let content_pretty = Grep.pretty_format content in + Common.Term.cursor t None; + render t { state with content; content_pretty }) + ; on_cancel = + (fun _ -> + Common.Term.cursor t None; + render t state) + } + in + Input_screen.render t input_state + | `Key (`ASCII '1', []) -> goto_headlines (fun () -> render t state) + | `Key (`ASCII '2', []) -> goto_todo (fun () -> render t state) + | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down () + | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up () + | `Key (`ASCII 't', []) -> + let selected_file, _ = Array.get content (y - content_start) in + let _ = Grep.toggle_todo selected_file in + let content, content_pretty = load_done () in + render + t + { state with + content = content |> Array.of_list + ; content_pretty = Array.of_list content_pretty + } + | `Key (`Arrow d, _) -> + (match d with + | `Up -> scroll_up () + | `Down -> scroll_down () + | _ -> render t state) + | `Key (`ASCII 'e', []) | `Key (`Enter, []) -> + (* Editor might be set with extra args, in that case we need to separate these *) + let[@warning "-8"] (editor :: args) = + String.split_on_char ' ' (Sys.getenv "EDITOR") + in + let selected_file, _ = Array.get content content_position in + let full_path_file = Grep.execution_directory ^ "/" ^ selected_file in + let full_args = Array.append (Array.of_list args) [| full_path_file |] in + Common.Term.cursor t (Some (0, 0)); + let _ = + Unix.create_process_env + editor + full_args + (Unix.environment ()) + Unix.stdin + Unix.stdout + Unix.stderr + in + let rec run_editor () = + match Unix.wait () with + | _, _ -> + Common.Term.cursor t None; + render t state + (* Capture resizing events *) + | exception Unix.Unix_error (Unix.EINTR, _, _) -> run_editor () + | exception Unix.Unix_error (_, _, _) -> failwith "ERROR" + in + run_editor () + | _ -> render t state diff --git a/lib/grep.ml b/lib/grep.ml index 6ed2a98..0e8294a 100644 --- a/lib/grep.ml +++ b/lib/grep.ml @@ -52,6 +52,14 @@ let pad str n = exception Not_A_Tuple of string * string (* todo parsing *) +let done_get_args = [ grep_cmd; done_pattern_regexp; "-H"; "-r"; "-n"; "--no-messages" ] + +let get_done () = + let open Shexp_process in + let open Shexp_process.Infix in + run_calls (call done_get_args |- call [ "sort"; "-n"; "-r" ]) + + let todo_get_args = [ grep_cmd; todo_pattern_regexp; "-H"; "-r"; "-n"; "--no-messages" ] let get_todos () = @@ -158,6 +166,8 @@ let get_tagged_headlines tag () = (find_sort_name () |- call headline_args |- call [ grep_cmd; "--no-messages"; "-E"; tag ] + |- call filter_todos_args + |- call filter_done_args |- call [ "sort"; "-n"; "-r" ] |- read_all)) @@ -231,6 +241,6 @@ let pretty_print_parsed_content parsed_files = List.concat_map (fun (file_name, line_number, line_content, _) -> if line_number == 0 - then [ Bold ("------- " ^ file_name); Normal line_content ] + then [ Bold ("--------- " ^ file_name); Normal line_content ] else [ Normal (padding ^ line_content) ]) parsed_files diff --git a/lib/headlines.ml b/lib/headlines.ml index f61d8b9..51bcc6b 100644 --- a/lib/headlines.ml +++ b/lib/headlines.ml @@ -11,9 +11,10 @@ type state = ; content : (string * string) array ; content_pretty : string array ; goto_todos_view : (unit -> unit) -> unit + ; goto_done_view : (unit -> unit) -> unit } -let init ~goto_todos_view ~regexp = +let init ~goto_done_view ~goto_todos_view ~regexp = let tag = if String.equal regexp "" then None else Some regexp in let content = match tag with @@ -26,15 +27,19 @@ let init ~goto_todos_view ~regexp = exit 0) else ( let content_pretty = content |> Grep.pretty_format in - { pos = 0, 2; scroll = 0; content; content_pretty; goto_todos_view }) + { pos = 0, 2; scroll = 0; content; content_pretty; goto_done_view; goto_todos_view }) +let title = I.strf ~attr:A.(st bold) "%s" "Note View" |> I.pad ~l:0 ~t:0 + (* TODO: Add page title *) -let rec render t ({ pos; scroll; content; content_pretty; goto_todos_view } as state) = - let title = I.strf ~attr:A.(st underline) "%s" "Note View" |> I.pad ~l:0 ~t:0 in +let rec render + t + ({ pos; scroll; content; content_pretty; goto_todos_view; goto_done_view } as state) + = let content_start = 2 in let x, y = pos in - let content_position = y - content_start + 1 in + let content_position = y - content_start in let img = let dot = I.string A.(st bold) ">" |> I.pad ~l:0 ~t:(y - scroll) and elements = @@ -56,7 +61,6 @@ let rec render t ({ pos; scroll; content; content_pretty; goto_todos_view } as s render t { state with pos = x, max (y - 1) content_start; scroll } in let scroll_down () = - print_endline (Int.to_string y); let scroll = if y - scroll >= size_y - 1 then scroll + 1 else scroll in render t { state with pos = x, min (y + 1) content_end; scroll } in @@ -71,6 +75,7 @@ let rec render t ({ pos; scroll; content; content_pretty; goto_todos_view } as s render t { state with pos = 0, min y content_end } | `Key (`ASCII '?', []) -> Help_screen.render t { go_back = (fun () -> render t state) } | `Key (`ASCII '2', []) -> goto_todos_view (fun () -> render t state) + | `Key (`ASCII '3', []) -> goto_done_view (fun () -> render t state) | `Key (`ASCII 's', []) -> let content = Array.map @@ -84,7 +89,7 @@ let rec render t ({ pos; scroll; content; content_pretty; goto_todos_view } as s in Stitched_article.render t - { pos = 0, 0 + { pos = 0, Stitched_article.content_start ; content = full_content |> Array.of_list ; content_pretty = full_content_pretty ; scroll = 0 @@ -101,7 +106,9 @@ let rec render t ({ pos; scroll; content; content_pretty; goto_todos_view } as s let content = Grep.get_tagged_headlines tag () |> Grep.parse_headlines in let content_pretty = Grep.pretty_format content in Common.Term.cursor t None; - render t { state with content; content_pretty; pos = 0, 0; scroll = 0 }) + render + t + { state with content; content_pretty; pos = 0, content_start; scroll = 0 }) ; on_cancel = (fun _ -> Common.Term.cursor t None; diff --git a/lib/help_screen.ml b/lib/help_screen.ml index b07cad2..b74e91c 100644 --- a/lib/help_screen.ml +++ b/lib/help_screen.ml @@ -4,15 +4,18 @@ open Notty type state = { go_back : unit -> unit } let render_info = - let title = I.strf ~attr:A.(st bold ++ st underline) "%s" "Stitch" |> I.pad ~l:2 ~t:0 in + let title = I.strf ~attr:A.(st bold) "%s" "Stitch" |> I.pad ~l:0 ~t:0 in let description = I.strf ~attr:A.(st bold) "%s" - "Minimal Note Composer. Run with stitch --help for more info." - |> I.pad ~l:2 ~t:1 + "Minimal Note Composer. Run stitch --help for more info." + |> I.pad ~l:2 ~t:2 + in + let license = + I.strf "%s" "Made by Marc Coquand (https://mccd.space). Licensed under BSD-3." + |> I.pad ~l:2 ~t:4 in - let license = I.strf "%s" "Licensed under BSD-3" |> I.pad ~l:2 ~t:3 in let open I in 5, title description license diff --git a/lib/stitch.ml b/lib/stitch.ml index e49ed9c..2fc0a9c 100644 --- a/lib/stitch.ml +++ b/lib/stitch.ml @@ -5,7 +5,8 @@ module Headlines = Headlines let start (tag : string) () = (* This is a rather funky state management that isn't maybe entirely functional. - What we do is store a function for each view that restores it's state. + What we do is store a function for each view that restores it's state. Since the render function is + void -> void This allows us to remember the state of the view and restore it as we travel between different views. @@ -13,17 +14,47 @@ let start (tag : string) () = *) let term = Common.Term.create () in let restore_headline_state = ref (fun () -> ()) in + let restore_done_state = ref (fun () -> ()) in let restore_todo_state = ref (fun () -> ()) in + (* DONE *) + let goto_todo_from_done new_done_state = + restore_done_state := new_done_state; + !restore_todo_state () + in + let goto_headlines_from_done new_done_state = + restore_done_state := new_done_state; + !restore_headline_state () + in + (restore_done_state + := fun () -> + let done_state = + Done.init + ~goto_headlines:goto_headlines_from_done + ~goto_todo:goto_todo_from_done + in + Done.render term done_state); + (* TODO *) + let goto_done_from_todo new_todo_state = + restore_todo_state := new_todo_state; + !restore_done_state () + in let goto_headline_from_todo new_todo_state = restore_todo_state := new_todo_state; !restore_headline_state () in (restore_todo_state := fun () -> - let todo = Todos.init ~goto_headlines:goto_headline_from_todo in + let todo = + Todos.init + ~goto_headlines:goto_headline_from_todo + ~goto_done:goto_done_from_todo + in Todos.render term todo); let headline = Headlines.init + ~goto_done_view:(fun new_state -> + restore_headline_state := new_state; + !restore_done_state ()) ~goto_todos_view:(fun new_state -> restore_headline_state := new_state; !restore_todo_state ()) diff --git a/lib/stitched_article.ml b/lib/stitched_article.ml index 66a108e..56250c8 100644 --- a/lib/stitched_article.ml +++ b/lib/stitched_article.ml @@ -13,6 +13,9 @@ type state = ; goto_todos_view : (unit -> unit) -> unit } +let title = I.strf ~attr:A.(st bold) "%s" "Note View" |> I.pad ~l:0 ~t:0 +let content_start = 1 + (* TODO: Use grep -l to filter notes by regexp and rerender those files*) let rec render t @@ -23,18 +26,18 @@ let rec render let img = let elements = Array.mapi - (fun i el -> Compontent.current_line size_x y scroll el i) + (fun i el -> Compontent.current_line size_x y scroll el (i + content_start)) (* TODO: Fix this ugly slow conversion *) (Array.to_seq content_pretty |> Seq.drop scroll |> Array.of_seq) in let open I in - Array.fold_left (fun sum el -> el sum) I.empty elements + Array.fold_left (fun sum el -> el sum) title elements in Common.Term.image t img; let content_length = Array.length content_pretty in let scroll_up () = - let scroll = if y - scroll = 0 then max (scroll - 1) 0 else scroll in - render t @@ { state with pos = x, max (y - 1) 0; scroll } + let scroll = if y - content_start - scroll = 0 then max (scroll - 1) 0 else scroll in + render t @@ { state with pos = x, max (y - 1) content_start; scroll } in let scroll_down () = let scroll = if y - scroll >= size_y - 1 then scroll + 1 else scroll in @@ -59,7 +62,9 @@ let rec render let[@warning "-8"] (editor :: args) = String.split_on_char ' ' (Sys.getenv "EDITOR") in - let selected_file, line_number, _, file_number_offset = Array.get content y in + let selected_file, line_number, _, file_number_offset = + Array.get content (y - content_start) + in let full_path_file = Grep.execution_directory ^ "/" ^ selected_file in (* Because each file title consists of two lines, we need to account for the offset it adds by removing the file_number *) diff --git a/lib/todos.ml b/lib/todos.ml index d2af47f..fd9eaf1 100644 --- a/lib/todos.ml +++ b/lib/todos.ml @@ -9,16 +9,21 @@ type state = ; content : (string * string) array ; content_pretty : string array ; goto_headlines : (unit -> unit) -> unit + ; goto_done : (unit -> unit) -> unit } -let init ~goto_headlines = +let title = I.strf ~attr:A.(st bold) "%s" "Todo View" |> I.pad ~l:0 ~t:0 +let content_start = 2 + +let init ~goto_done ~goto_headlines = let content = Grep.get_todos () |> Grep.parse_todo_string in let content_pretty = Grep.pretty_format_todo content in - { pos = 0, 0 + { pos = 0, content_start ; scroll = 0 ; content = content |> Array.of_list ; content_pretty = content_pretty |> Array.of_list ; goto_headlines + ; goto_done } @@ -28,31 +33,35 @@ let load_todos () = todo_content, todo_pretty -let rec render t ({ pos; scroll; content; content_pretty; goto_headlines } as state) = +let rec render + t + ({ pos; scroll; content; content_pretty; goto_headlines; goto_done } as state) + = let x, y = pos in + let content_position = y - content_start in let img = let dot = I.string A.(st bold) ">" |> I.pad ~l:0 ~t:(y - scroll) and elements = Array.mapi (fun i el -> if i == y - scroll - then I.strf ~attr:A.(st underline) "%s" el |> I.pad ~l:2 ~t:i - else I.strf "%s" el |> I.pad ~l:2 ~t:i) + then I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start) + else I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start)) (Array.to_seq content_pretty |> Seq.drop scroll |> Array.of_seq) in let open I in - Array.fold_left (fun sum el -> el sum) dot elements + Array.fold_left (fun sum el -> el sum) (title dot) elements in let _, size_y = Common.Term.size t in Common.Term.image t img; - let content_length = Array.length content_pretty in + let content_end = Array.length content_pretty + (content_start - 1) in let scroll_up () = - let scroll = if y - scroll = 0 then max (scroll - 1) 0 else scroll in - render t @@ { state with pos = x, max (y - 1) 0; scroll } + let scroll = if y - content_start - scroll = 0 then max (scroll - 1) 0 else scroll in + render t { state with pos = x, max (y - 1) content_start; scroll } in let scroll_down () = let scroll = if y - scroll >= size_y - 1 then scroll + 1 else scroll in - render t @@ { state with pos = x, min (y + 1) (content_length - 1); scroll } + render t { state with pos = x, min (y + 1) content_end; scroll } in match Common.Term.event t with | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> () @@ -62,7 +71,7 @@ let rec render t ({ pos; scroll; content; content_pretty; goto_headlines } as st | `Up -> scroll_up ()) | `Resize _ -> render t state | `Mouse ((`Press _ | `Drag), (_, y), _) -> - render t { state with pos = 0, min y content_length } + render t { state with pos = 0, min y content_end } | `Key (`ASCII '?', []) -> Help_screen.render t { go_back = (fun () -> render t state) } | `Key (`ASCII 's', []) -> let (input_state : Input_screen.state) = @@ -83,10 +92,11 @@ let rec render t ({ pos; scroll; content; content_pretty; goto_headlines } as st in Input_screen.render t input_state | `Key (`ASCII '1', []) -> goto_headlines (fun () -> render t state) + | `Key (`ASCII '3', []) -> goto_done (fun () -> render t state) | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down () | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up () | `Key (`ASCII 't', []) -> - let selected_file, _ = Array.get content y in + let selected_file, _ = Array.get content (y - content_start) in let _ = Grep.toggle_done selected_file in let content, content_pretty = load_todos () in render @@ -105,7 +115,7 @@ let rec render t ({ pos; scroll; content; content_pretty; goto_headlines } as st let[@warning "-8"] (editor :: args) = String.split_on_char ' ' (Sys.getenv "EDITOR") in - let selected_file, _ = Array.get content y in + let selected_file, _ = Array.get content content_position in let full_path_file = Grep.execution_directory ^ "/" ^ selected_file in let full_args = Array.append (Array.of_list args) [| full_path_file |] in Common.Term.cursor t (Some (0, 0)); -- cgit v1.2.3