stitch

Note taking for messy minimalists

git clone git://mccd.space/stitch
commit 0bc0958e789847d3065f4d084a96117d62d18691
parent 20aa8079b40ee794bc84ead7283622c78f49afb5
Author: Marc Coquand <marc@mccd.space>
Date:   Tue, 14 May 2024 16:20:57 -0500

Add nice headings

Diffstat:
MREADME.org | 22++++++++++++++++------
Mlib/grep.ml | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mlib/headlines.ml | 63+++++++++++++++++++++++++++------------------------------------
Alib/stitched_article.ml | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 164 insertions(+), 60 deletions(-)
diff --git a/README.org b/README.org
@@ -5,8 +5,11 @@ built around the idea of writing notes separately and then using tags
 to compose these notes together.
 
 Stitch does not have any opinion about which file format you use for
-file capturing, use org, markdown, whatever you want. You can
-customize the grep command.
+file capturing, use org, markdown, whatever you want. You can also
+customize the grep command to speed up performance.
+
+Stitch tries to have few dependencies, and stick only to coreutils by
+default
 
 Stitch limits itself only to note composing. For capturing notes, you
 will have to set up your own system.
@@ -30,7 +33,8 @@ Set the environment variables:
 
 STICH_DIRECTORY
 STICH_GREP_CMD (default "grep")
-STITCH_HEADLINE_PATTERN (default "^\\* ")
+STITCH_HEADLINE_PATTERN_REGEXP (default "^\\* ")
+STITCH_HEADLINE_PATTERN (default "* ")
 STITCH_TAG_PATTERN (default ":[a-z-]+:", matches :a-tag:)
 
 ** SPEED UP
@@ -55,8 +59,11 @@ dune exec -- stitch
 - Building a journaling system
 You can build a basic capture command using $EDITOR and date command:
 
-alias capture="JRNL=\"$STITCH_DIRECTORY/$(date +'%Y-%m-%d
-%H:%M').org\" echo '* :journal:' > $JRNL_FILE && $EDITOR $JRNL"
+#+begin_src
+export STITCH_DIRECTORY=/home/me/notes
+
+alias capture="JRNL=\"$STITCH_DIRECTORY/$(date +'%Y-%m-%d %H:%M').org\" echo '* :journal:' > "$STITCH_DIRECTORY && $EDITOR $JRNL"
+#+end_src
 
 and then you can find your journal entries, automatically sorted by
 creation date with stitch and the journal tag:
@@ -65,7 +72,10 @@ alias jrnl="stitch -t journal"
 
 * KNOWN ISSUES
 
-- Resizing the screen when editor is open causes panic
+- ugrep doesn't correctly handle ordering, meaning that if you sort by
+  modified date, it doesn't behave correctly. Therefore, that behavior
+  is disabled for now.
+  
 
 
 * Author
diff --git a/lib/grep.ml b/lib/grep.ml
@@ -3,25 +3,33 @@ let execution_directory =
 
 
 let grep_cmd = Sys.getenv_opt "STICH_GREP_CMD" |> Option.value ~default:"ugrep"
-let tag_pattern = Sys.getenv_opt "STITCH_TAG_PATTERN" |> Option.value ~default:":[a-z-]+:"
+
+let tag_pattern =
+  Sys.getenv_opt "STITCH_TAG_PATTERN" |> Option.value ~default:"\\:[a-z-]+\\:"
+
+
+let headline_pattern_regexp =
+  Sys.getenv_opt "STITCH_HEADLINE_PATTERN_REGEXP" |> Option.value ~default:"^\\* "
+
 
 let headline_pattern =
-  Sys.getenv_opt "STITCH_HEADLINE_PATTERN" |> Option.value ~default:"^\\* "
+  Sys.getenv_opt "STITCH_HEADLINE_PATTERN" |> Option.value ~default:"* "
 
 
+let headline_pattern_length = String.length headline_pattern
+
 let find_sort_modification () =
   let open Shexp_process in
   let open Shexp_process.Infix in
   call [ "find"; "."; "-printf"; "%Ts/%f\\n" ]
   |- call [ "sort"; "-n" ]
   |- call [ "cut"; "-c12-" ]
-  |- call [ "sed"; "/^\\./d" ]
 
 
 let find_sort_name () =
   let open Shexp_process in
   let open Shexp_process.Infix in
-  call [ "find"; "." ] |- call [ "sort"; "-n" ] |- call [ "cut"; "-c3-" ]
+  call [ "find"; "." ] |- call [ "cut"; "-c3-" ]
 
 
 let run_print ~dir args =
@@ -32,18 +40,13 @@ let run_print ~dir args =
 
 let headline_args = [ "xargs"; grep_cmd; "^\\*"; "-H"; "-r"; "-n"; "--no-messages" ]
 
-(* type sort = *)
-(*   | Name *)
-(*   | LastModified *)
-
-(* let sort_to_cmd = function *)
-(*   | Name -> find_sort_name () *)
-(*   | LastModified -> find_sort_modification () *)
-
 let get_headlines () =
   let open Shexp_process in
   let open Shexp_process.Infix in
-  eval (chdir execution_directory (find_sort_name () |- call headline_args |- read_all))
+  eval
+    (chdir
+       execution_directory
+       (find_sort_name () |- call headline_args |- call [ "sort"; "-n"; "-r" ] |- read_all))
 
 
 let get_tagged_headlines tag () =
@@ -54,7 +57,8 @@ let get_tagged_headlines tag () =
        execution_directory
        (find_sort_name ()
         |- call headline_args
-        |- call [ grep_cmd; "-E"; "--no-messages"; tag ]
+        |- call [ grep_cmd; "--no-messages"; "-E"; tag ]
+        |- call [ "sort"; "-n"; "-r" ]
         |- read_all))
 
 
@@ -105,10 +109,41 @@ let pretty_format parsed_headlines =
 
 (** Full body parsing *)
 
-let get_full_content () =
-  run_print
-    ~dir:execution_directory
-    [ grep_cmd; "^\\*"; "-h"; "-r"; "-n"; "-C"; "9999"; "--separator='|'" ]
+let get_full_content_command file = [ "cat"; file ]
+
+let get_full_file_content_content file =
+  let open Shexp_process in
+  let open Shexp_process.Infix in
+  ( file
+  , eval
+      (chdir
+         execution_directory
+         (find_sort_name ()
+          |- call headline_args
+          |- call (get_full_content_command file)
+          |- read_all)) )
+
+
+let parse_full_content files =
+  List.concat_map
+    (fun ((file_name : string), content) ->
+      let content = String.split_on_char '\n' content in
+      List.mapi (fun line_number line -> file_name, line_number, line) content)
+    files
+
+
+type display_type =
+  | Bold of string
+  | Normal of string
+
+let pretty_print_parsed_content parsed_files =
+  let padding = String.make headline_pattern_length ' ' in
+  List.concat_map
+    (fun (file_name, line_number, line_content) ->
+      if line_number == 0
+      then [ Bold ("------- " ^ file_name); Normal line_content ]
+      else [ Normal (padding ^ line_content) ])
+    parsed_files
 
 (* let parse_file_headline collection full = *)
 (*   match full with *)
diff --git a/lib/headlines.ml b/lib/headlines.ml
@@ -2,17 +2,16 @@ module Grep = Grep
 module Common = Common
 open Notty
 module Input_screen = Input_screen
+module Stitched_article = Stitched_article
 
 type state =
-  { tag : string option
-  ; pos : int * int
+  { pos : int * int
   ; scroll : int
   ; content : (string * string) array
   ; content_pretty : string array
   }
 
-let rec headline_screen t ({ tag; pos; scroll; content; content_pretty } as state) =
-  print_endline (Option.value ~default:"No tag" tag);
+let rec render t ({ pos; scroll; content; content_pretty } as state) =
   let x, y = pos in
   let img =
     let dot = I.string A.(st bold) ">" |> I.pad ~l:0 ~t:(y - scroll)
@@ -32,11 +31,11 @@ let rec headline_screen t ({ tag; pos; scroll; content; content_pretty } as stat
   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
-    headline_screen t @@ { state with pos = x, max (y - 1) 0; scroll }
+    render t @@ { state with pos = x, max (y - 1) 0; scroll }
   in
   let scroll_down () =
     let scroll = if y - scroll >= size_y - 1 then scroll + 1 else scroll in
-    headline_screen 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 }
   in
   match Common.Term.event t with
   | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> ()
@@ -44,42 +43,36 @@ let rec headline_screen t ({ tag; pos; scroll; content; content_pretty } as stat
     (match s with
      | `Down -> scroll_down ()
      | `Up -> scroll_up ())
-  | `Resize _ -> headline_screen t state
+  | `Resize _ -> render t state
   | `Mouse ((`Press _ | `Drag), (_, y), _) ->
-    headline_screen t { state with pos = 0, min y content_length }
-  | `Key (`ASCII 't', []) ->
+    render t { state with pos = 0, min y content_length }
+  | `Key (`ASCII '@', []) ->
+    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
+    Stitched_article.render
+      t
+      { pos = 0, 0; content = content |> Array.of_list; content_pretty; scroll = 0 }
+  | `Key (`ASCII 's', []) ->
     let (input_state : Input_screen.state) =
       { screen = img
       ; user_input = ""
-      ; prompt = "TAG: "
+      ; 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;
-            headline_screen
-              t
-              { state with content; content_pretty; pos = 0, 0; scroll = 0 })
-      ; on_cancel =
-          (fun _ ->
-            Common.Term.cursor t None;
-            headline_screen t state)
-      }
-    in
-    Input_screen.render t input_state
-  | `Key (`ASCII 's', []) ->
-    let (input_state : Input_screen.state) =
-      { screen = img
-      ; user_input = ""
-      ; prompt = "SEARCH: "
-      ; on_enter =
-          (fun _ ->
-            Common.Term.cursor t None;
-            headline_screen t { state with pos = 0, 0; scroll = 0 })
+            render t { content; content_pretty; pos = 0, 0; scroll = 0 })
       ; on_cancel =
           (fun _ ->
             Common.Term.cursor t None;
-            headline_screen t state)
+            render t state)
       }
     in
     Input_screen.render t input_state
@@ -89,7 +82,7 @@ let rec headline_screen t ({ tag; pos; scroll; content; content_pretty } as stat
     (match d with
      | `Up -> scroll_up ()
      | `Down -> scroll_down ()
-     | _ -> headline_screen t state)
+     | _ -> 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) =
@@ -112,13 +105,13 @@ let rec headline_screen t ({ tag; pos; scroll; content; content_pretty } as stat
       match Unix.wait () with
       | _, _ ->
         Common.Term.cursor t None;
-        headline_screen t state
+        render t state
       (* Capture resizing events *)
       | exception Unix.Unix_error (Unix.EINTR, _, _) -> run_editor ()
       | exception Unix.Unix_error (_, _, _) -> failwith "ERROR"
     in
     run_editor ()
-  | _ -> headline_screen t state
+  | _ -> render t state
 
 
 let start (tag : string) () =
@@ -134,6 +127,4 @@ let start (tag : string) () =
     exit 0)
   else (
     let content_pretty = content |> Grep.pretty_format in
-    headline_screen
-      (Common.Term.create ())
-      { tag; pos = 0, 0; scroll = 0; content; content_pretty })
+    render (Common.Term.create ()) { pos = 0, 0; scroll = 0; content; content_pretty })
diff --git a/lib/stitched_article.ml b/lib/stitched_article.ml
@@ -0,0 +1,68 @@
+module Grep = Grep
+module Common = Common
+open Notty
+module Input_screen = Input_screen
+
+type state =
+  { pos : int * int
+  ; scroll : int
+  ; content : (string * int * string) array
+  ; content_pretty : Grep.display_type array
+  }
+
+let render_line size_x y scroll (el : Grep.display_type) i =
+  match el with
+  | Bold el ->
+    if i == y - scroll
+    then (
+      let fill = String.make (max (size_x - String.length el) 0) ' ' in
+      I.strf "%s%s" ~attr:A.(st bold ++ st reverse) el fill |> I.pad ~l:0 ~t:i)
+    else I.strf "%s" ~attr:A.(st bold) el |> I.pad ~l:0 ~t:i
+  | Normal el ->
+    if i == y - scroll
+    then (
+      let fill = String.make (max (size_x - String.length el) 0) ' ' in
+      I.strf "%s%s" ~attr:A.(st reverse) el fill |> I.pad ~l:0 ~t:i)
+    else I.strf "%s" el |> I.pad ~l:0 ~t:i
+
+
+let rec render t ({ pos; scroll; content_pretty; _ } as state) =
+  let size_x, size_y = Common.Term.size t in
+  let x, y = pos in
+  let img =
+    let elements =
+      Array.mapi
+        (fun i el -> render_line size_x y scroll el i)
+        (* 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
+  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 }
+  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 }
+  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_length }
+  | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down ()
+  | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up ()
+  | `Key (`Arrow d, _) ->
+    (match d with
+     | `Up -> scroll_up ()
+     | `Down -> scroll_down ()
+     | _ -> render t state)
+  | _ -> render t state