stitch

Note taking for messy minimalists

git clone git://mccd.space/stitch
commit acb42d7250f4832e2c93e5ecfaab4dd69b4c4a11
parent f805a04bd0600653f71e5e4e006104892cf7b6b9
Author: Marc Coquand <marc@mccd.space>
Date:   Sat, 18 May 2024 13:36:55 -0500

Arbitrary command for article

Diffstat:
Mbin/main.ml | 2+-
Mlib/arbitrary_command.ml | 2+-
Mlib/grep.ml | 5+++++
Mlib/headlines.ml | 2++
Mlib/stitched_article.ml | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
5 files changed, 85 insertions(+), 8 deletions(-)
diff --git a/bin/main.ml b/bin/main.ml
@@ -147,7 +147,7 @@ let headlines_cmd =
         "You can run arbitrary commands in Stitch. These commands can make use of \
          variable substitutions. Available substitutions are"
     ; `I ("%(file)", "Currently selected file")
-    ; `I ("%(content)", "Currently selected content")
+    ; `I ("%(line)", "Currently selected content")
     ]
   in
   let credit =
diff --git a/lib/arbitrary_command.ml b/lib/arbitrary_command.ml
@@ -7,7 +7,7 @@ let run ~(on_return : string -> unit) t ~selected_file ~content ~command =
     command
     |> List.map (fun s ->
       let s = Str.global_replace (Str.regexp "%(file)") selected_file s in
-      Str.global_replace (Str.regexp "%(content)") content s)
+      Str.global_replace (Str.regexp "%(line)") content s)
   in
   Common.Term.cursor t (Some (0, 0));
   try
diff --git a/lib/grep.ml b/lib/grep.ml
@@ -292,6 +292,11 @@ type display_type =
   | Bold of string
   | Normal of string
 
+let display_type_line = function
+  | Bold s -> s
+  | Normal s -> s
+
+
 let pretty_print_parsed_content parsed_files =
   let padding = String.make headline_pattern_length ' ' in
   List.concat_map
diff --git a/lib/headlines.ml b/lib/headlines.ml
@@ -114,6 +114,8 @@ let rec render
       ; content = full_content |> Array.of_list
       ; content_pretty = full_content_pretty
       ; scroll = 0
+      ; tag
+      ; output = None
       ; go_back = (fun () -> render t state)
       ; goto_todos_view
       ; goto_done_view
diff --git a/lib/stitched_article.ml b/lib/stitched_article.ml
@@ -12,6 +12,8 @@ type state =
   ; content_pretty : (int * Grep.display_type) array
   ; goto_todos_view : (unit -> unit) -> unit
   ; goto_done_view : (unit -> unit) -> unit
+  ; output : string option
+  ; tag : string
   }
 
 let title = I.strf ~attr:A.(st bold) "%s" "Note View" |> I.pad ~l:0 ~t:0
@@ -20,30 +22,45 @@ let content_start = 1
 (* TODO: Use grep -l to filter notes by regexp and rerender those files*)
 let rec render
   t
-  ({ pos; scroll; content_pretty; go_back; content; goto_todos_view; goto_done_view } as
-   state)
+  ({ output
+   ; pos
+   ; scroll
+   ; content_pretty
+   ; go_back
+   ; content
+   ; goto_todos_view
+   ; goto_done_view
+   ; tag
+   } as state)
   =
   let size_x, size_y = Common.Term.size t in
   let x, y = pos in
   let img =
-    let elements =
+    let output_info =
+      match output with
+      | Some line ->
+        I.strf "%s%s" (String.escaped line) (String.make size_x ' ')
+        |> I.pad ~t:(size_y - 1)
+      | None -> I.empty
+    and elements =
       Array.mapi
         (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) title elements
+    output_info </> 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 - content_start - scroll = 0 then max (scroll - 1) 0 else scroll in
-    render t @@ { state with pos = x, max (y - 1) content_start; scroll }
+    render t @@ { state with pos = x, max (y - 1) content_start; scroll; output = None }
   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_length - 1); scroll; output = None }
   in
   match Common.Term.event t with
   | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> ()
@@ -57,6 +74,58 @@ let rec render
     render t { state with pos = 0, min y content_length }
   | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down ()
   | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up ()
+  | `Key (`ASCII '!', []) ->
+    let file_number_offset, content_line = Array.get content_pretty (y - content_start) in
+    let content_line = Grep.display_type_line content_line in
+    let selected_file, _, _, _ =
+      Array.get content (y - content_start - file_number_offset)
+    in
+    let selected_file = Grep.execution_directory ^ "/" ^ selected_file in
+    let (input_state : Input_prompt.state) =
+      { screen = img
+      ; user_input = ""
+      ; prompt = "COMMAND"
+      ; on_enter =
+          (fun command ->
+            if String.equal (String.trim command) String.empty
+            then (
+              Common.Term.cursor t None;
+              render t state)
+            else (
+              let content = Grep.get_file_names tag |> Array.of_list in
+              let content =
+                Array.map
+                  (fun file_name -> Grep.get_full_file_content_content file_name)
+                  content
+                |> Array.to_list
+              in
+              let content = Grep.parse_full_content content in
+              let content_pretty =
+                Grep.pretty_print_parsed_content content |> Array.of_list
+              in
+              Arbitrary_command.run
+                t
+                ~command:(Scanf.unescaped command)
+                ~content:content_line
+                ~selected_file
+                ~on_return:(fun result ->
+                  Common.Term.cursor t None;
+                  render
+                    t
+                    { state with
+                      content = Array.of_list content
+                    ; content_pretty
+                    ; pos = 0, content_start
+                    ; scroll = 0
+                    ; output = Some result
+                    })))
+      ; on_cancel =
+          (fun _ ->
+            Common.Term.cursor t None;
+            render t state)
+      }
+    in
+    Input_prompt.render t input_state
   | `Key (`ASCII 's', []) ->
     let (input_state : Input_prompt.state) =
       { screen = img
@@ -87,6 +156,7 @@ let rec render
                 ; content_pretty
                 ; pos = 0, content_start
                 ; scroll = 0
+                ; tag
                 }
             with
             | _ ->