From be2bf751536850e7bdfbd876ef908c5d6cf00087 Mon Sep 17 00:00:00 2001 From: Marc Coquand Date: Tue, 14 May 2024 10:46:28 -0500 Subject: Add search and tag search --- README.org | 4 +-- bin/main.ml | 17 +++++++++- lib/grep.ml | 34 +++++++++++++++++--- lib/headlines.ml | 92 +++++++++++++++++++++++++++++++++++++++++++---------- lib/input_screen.ml | 35 ++++++++++++++++++++ 5 files changed, 159 insertions(+), 23 deletions(-) create mode 100644 lib/input_screen.ml diff --git a/README.org b/README.org index 1f61c32..9e06ad3 100644 --- a/README.org +++ b/README.org @@ -30,8 +30,8 @@ Set the environment variables: STICH_DIRECTORY STICH_GREP_CMD (default "grep") -STITCH_HEADLINE_PATTERN (default "^\* ") -STITCH_TAG_PATTERN (default ":[a-z]:") +STITCH_HEADLINE_PATTERN (default "^\\* ") +STITCH_TAG_PATTERN (default ":[a-z_-]+:") ** SPEED UP diff --git a/bin/main.ml b/bin/main.ml index bb7dc1e..78d30ba 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -1,3 +1,18 @@ open Stitch +open Cmdliner -let () = Headlines.start () +let tag_arg = + let doc = "Search entries for a given tag." in + Arg.(value & opt string "" & info [ "t"; "tag" ] ~docv:"TAG" ~doc) + + +let headlines_t = Term.(const Headlines.start $ tag_arg $ const ()) + +let headlines_cmd = + let doc = "Show titles in a condensed list" in + let man = [ `S Manpage.s_bugs; `P "Email bug reports to marc@mccd.space" ] in + let info = Cmd.info "headlines" ~version:"0.1" ~doc ~man in + Cmd.v info headlines_t + + +let () = exit (Cmd.eval headlines_cmd) diff --git a/lib/grep.ml b/lib/grep.ml index a6ba300..e438d0a 100644 --- a/lib/grep.ml +++ b/lib/grep.ml @@ -4,16 +4,42 @@ 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 headline_pattern = + Sys.getenv_opt "STITCH_HEADLINE_PATTERN" |> Option.value ~default:"^\\* " + + let run_print ~dir args = let open Shexp_process in let open Shexp_process.Infix in eval (chdir dir (call args |- read_all)) -let get_headlines () = - run_print - ~dir:execution_directory - [ grep_cmd; "^\\*"; "-H"; "-r"; "-n"; "--separator=|" ] +let headline_args = + [ grep_cmd; "^\\*"; "-H"; "-r"; "-n"; "--separator=|"; "--no-messages" ] + + +let get_headlines () = run_print ~dir:execution_directory headline_args + +let get_tagged_headlines tag () = + let open Shexp_process in + let open Shexp_process.Infix in + eval + (chdir + execution_directory + (call headline_args |- call [ grep_cmd; "-E"; "--no-messages"; tag ] |- read_all)) + + +let get_tags () = + let open Shexp_process in + let open Shexp_process.Infix in + eval + (chdir + execution_directory + (call headline_args |- call [ grep_cmd; "-E"; tag_pattern; "-o" ] |- read_all)) exception Not_A_Tuple of string * string diff --git a/lib/headlines.ml b/lib/headlines.ml index 8fab7f6..f32b6ef 100644 --- a/lib/headlines.ml +++ b/lib/headlines.ml @@ -1,16 +1,27 @@ module Grep = Grep module Common = Common open Notty +module Input_screen = Input_screen -let content = Grep.get_headlines () |> Grep.parse_headlines -let content_pretty = content |> Grep.pretty_format +type state = + { tag : string option + ; pos : int * int + ; scroll : int + ; content : (string * string) array + ; content_pretty : string array + } -let rec headline_screen t (((x, y) as pos), scroll) = +let rec headline_screen t ({ tag; pos; scroll; content; content_pretty } as state) = + print_endline (Option.value ~default:"No tag" tag); + let x, y = pos in let img = - let dot = I.string A.(fg black) ">" |> I.pad ~l:0 ~t:(y - scroll) + let dot = I.string A.(st bold) ">" |> I.pad ~l:0 ~t:(y - scroll) and elements = Array.mapi - (fun i el -> I.strf ~attr:A.(fg black) "%s" el |> I.pad ~l:2 ~t:i) + (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) (Array.to_seq content_pretty |> Seq.drop scroll |> Array.of_seq) in let open I in @@ -21,11 +32,11 @@ let rec headline_screen t (((x, y) as pos), scroll) = 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 @@ ((x, max (y - 1) 0), scroll) + headline_screen 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 @@ ((x, min (y + 1) content_length), scroll) + headline_screen t @@ { state with pos = x, min (y + 1) content_length; scroll } in match Common.Term.event t with | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> () @@ -33,18 +44,52 @@ let rec headline_screen t (((x, y) as pos), scroll) = (match s with | `Down -> scroll_down () | `Up -> scroll_up ()) - | `Resize _ -> headline_screen t (pos, scroll) + | `Resize _ -> headline_screen t state | `Mouse ((`Press _ | `Drag), (_, y), _) -> - headline_screen t ((0, min y content_length), scroll) + headline_screen t { state with pos = 0, min y content_length } + | `Key (`ASCII 't', []) -> + let (input_state : Input_screen.state) = + { screen = img + ; user_input = "" + ; prompt = "TAG: " + ; 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 }) + ; on_cancel = + (fun _ -> + Common.Term.cursor t None; + headline_screen t state) + } + in + Input_screen.render t input_state | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down () - | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> - let scroll = if y - scroll = 0 then max (scroll - 1) 0 else scroll in - headline_screen t @@ ((x, max (y - 1) 0), scroll) + | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up () | `Key (`Arrow d, _) -> (match d with | `Up -> scroll_up () | `Down -> scroll_down () - | _ -> headline_screen t ((x, y), scroll)) + | _ -> headline_screen 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) = @@ -67,13 +112,28 @@ let rec headline_screen t (((x, y) as pos), scroll) = match Unix.wait () with | _, _ -> Common.Term.cursor t None; - headline_screen t ((x, y), scroll) + headline_screen 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 (pos, scroll) + | _ -> headline_screen t state -let start () = headline_screen (Common.Term.create ()) ((0, 0), 0) +let start (tag : string) () = + let tag = if String.equal tag "" then None else Some tag in + let content = + match tag with + | None -> Grep.get_headlines () |> Grep.parse_headlines + | Some tag -> Grep.get_tagged_headlines tag () |> Grep.parse_headlines + in + if Array.length content == 0 + then ( + print_endline "No entry for tag"; + 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 }) diff --git a/lib/input_screen.ml b/lib/input_screen.ml new file mode 100644 index 0000000..3fae53c --- /dev/null +++ b/lib/input_screen.ml @@ -0,0 +1,35 @@ +module Grep = Grep +module Common = Common +open Notty + +type state = + { user_input : string + ; on_enter : string -> unit + ; on_cancel : unit -> unit + ; prompt : string + ; screen : I.t + } + +let rec render t ({ user_input; on_enter; on_cancel; screen; prompt } as state) = + let _, size_y = Common.Term.size t in + Common.Term.cursor t (Some (String.length user_input + String.length prompt, size_y)); + let img = + let open I in + I.strf "%s%s" prompt user_input |> I.pad ~l:0 ~t:(size_y - 1) screen + in + Common.Term.image t img; + match Common.Term.event t with + | `End | `Key (`ASCII 'G', [ `Ctrl ]) | `Key (`ASCII 'C', [ `Ctrl ]) -> on_cancel () + | `Key (`Enter, []) -> on_enter user_input + | `Key (`Backspace, []) -> + let state = + { state with + user_input = String.sub user_input 0 (max (String.length user_input - 1) 0) + } + in + render t state + | `Resize _ -> render t state + | `Key (`ASCII c, []) -> + let state = { state with user_input = user_input ^ String.make 1 c } in + render t state + | _ -> render t state -- cgit v1.2.3