From 0bc0958e789847d3065f4d084a96117d62d18691 Mon Sep 17 00:00:00 2001 From: Marc Coquand Date: Tue, 14 May 2024 16:20:57 -0500 Subject: Add nice headings --- README.org | 22 ++++++++++----- lib/grep.ml | 71 ++++++++++++++++++++++++++++++++++++------------- lib/headlines.ml | 63 +++++++++++++++++++------------------------ lib/stitched_article.ml | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 60 deletions(-) create mode 100644 lib/stitched_article.ml diff --git a/README.org b/README.org index 48e54c6..924a93f 100644 --- 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 index 9638c37..7e5c3b9 100644 --- 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 index aec8567..853603c 100644 --- 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 new file mode 100644 index 0000000..41efa75 --- /dev/null +++ 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 -- cgit v1.2.3