stitch

Note taking for messy minimalists

git clone git://mccd.space/stitch

stitched_article.ml (9824B)

      1 module Grep = Grep
      2 module Common = Common
      3 open Notty
      4 module Input_prompt = Input_prompt
      5 module Help_screen = Help_screen
      6 
      7 type state =
      8   { pos : int * int
      9   ; scroll : int
     10   ; content : (string * int * string * int) array
     11   ; go_back : unit -> unit
     12   ; content_pretty : (int * Grep.display_type) array
     13   ; goto_todos_view : (unit -> unit) -> unit
     14   ; goto_done_view : (unit -> unit) -> unit
     15   ; output : string option
     16   ; tag : string
     17   }
     18 
     19 let title ~tag =
     20   match tag with
     21   | "" -> I.strf ~attr:A.(st bold) "%s" "Notes" |> I.pad ~l:0 ~t:0
     22   | a -> I.strf ~attr:A.(st bold) "%s > %s" "Notes" a |> I.pad ~l:0 ~t:0
     23 
     24 
     25 let refresh tag =
     26   let content = Grep.get_file_names tag |> Array.of_list in
     27   let oc = open_out "/tmp/stitch-output" in
     28   Printf.fprintf oc "%s\n" (String.concat "\n" (content |> Array.to_list));
     29   close_out oc;
     30   let content =
     31     Array.map (fun file_name -> Grep.get_full_file_content_content file_name) content
     32     |> Array.to_list
     33   in
     34   let content = Grep.parse_full_content content in
     35   let content_pretty = Grep.pretty_print_parsed_content content |> Array.of_list in
     36   content |> Array.of_list, content_pretty
     37 
     38 
     39 let content_start = 1
     40 
     41 (* TODO: Use grep -l to filter notes by regexp and rerender those files*)
     42 let rec render
     43   t
     44   ({ output
     45    ; pos
     46    ; scroll
     47    ; content_pretty
     48    ; go_back
     49    ; content
     50    ; goto_todos_view
     51    ; goto_done_view
     52    ; tag
     53    } as state)
     54   =
     55   let size_x, size_y = Common.Term.size t in
     56   let x, y = pos in
     57   let img =
     58     let output_info =
     59       match output with
     60       | Some line ->
     61         I.strf "%s%s" (String.escaped line) (String.make size_x ' ')
     62         |> I.pad ~t:(size_y - 1)
     63       | None -> I.empty
     64     and elements =
     65       Array.mapi
     66         (fun i (_, el) -> Compontent.current_line size_x y scroll el (i + content_start))
     67         (* TODO: Fix this ugly slow conversion *)
     68         (Array.to_seq content_pretty |> Seq.drop scroll |> Array.of_seq)
     69     in
     70     let open I in
     71     output_info </> Array.fold_left (fun sum el -> el </> sum) (title ~tag) elements
     72   in
     73   Common.Term.image t img;
     74   let content_length = Array.length content_pretty in
     75   let scroll_up ?(amount = 1) () =
     76     let scroll =
     77       if y - content_start - scroll - amount < 0 then max (scroll - amount) 0 else scroll
     78     in
     79     render t { state with pos = x, max (y - amount) content_start; scroll; output = None }
     80   in
     81   let scroll_down ?(amount = 1) () =
     82     let scroll = if y - scroll >= size_y - amount then scroll + amount else scroll in
     83     render
     84       t
     85       { state with pos = x, min (y + amount) content_length; scroll; output = None }
     86   in
     87   match Common.Term.event t with
     88   | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> ()
     89   | `Mouse (`Press (`Scroll s), _, _) ->
     90     (match s with
     91      | `Down -> scroll_down ()
     92      | `Up -> scroll_up ())
     93   | `Resize _ -> render t state
     94   | `Key (`ASCII 'f', []) -> go_back ()
     95   | `Mouse ((`Press _ | `Drag), (_, y), _) ->
     96     render t { state with pos = 0, min y content_length }
     97   | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down ()
     98   | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up ()
     99   | `Key (`ASCII 'o', []) ->
    100     let (input_state : Input_prompt.state) =
    101       { screen = img
    102       ; user_input_pre = ""
    103       ; user_input_post = ""
    104       ; prompt = "EXPORT TO"
    105       ; on_enter =
    106           (fun path ->
    107             Common.Term.cursor t None;
    108             if String.equal (String.trim path) String.empty |> not
    109             then (
    110               let export =
    111                 String.concat
    112                   "\n"
    113                   (Array.to_list content_pretty
    114                    |> List.map (fun (_, b) -> Grep.display_type_line b))
    115               in
    116               let result = Export.to_path path ~content:export in
    117               render t { state with output = Some result })
    118             else render t { state with output = Some "Export Blank; skipping" })
    119       ; on_cancel =
    120           (fun _ ->
    121             Common.Term.cursor t None;
    122             render t { state with output = Some "Export Cancelled" })
    123       }
    124     in
    125     Input_prompt.render t input_state
    126   | `Key (`ASCII 'D', [ `Ctrl ]) | `Key (`Page `Down, []) ->
    127     scroll_down ~amount:(size_y / 2) ()
    128   | `Key (`ASCII 'U', [ `Ctrl ]) | `Key (`Page `Up, []) ->
    129     scroll_up ~amount:(size_y / 2) ()
    130   | `Key (`ASCII '!', []) ->
    131     let file_number_offset, content_line = Array.get content_pretty (y - content_start) in
    132     let content_line = Grep.display_type_line content_line in
    133     let selected_file, _, _, _ =
    134       Array.get content (y - content_start - file_number_offset)
    135     in
    136     let selected_file = Grep.execution_directory ^ "/" ^ selected_file in
    137     let (input_state : Input_prompt.state) =
    138       { screen = img
    139       ; user_input_pre = ""
    140       ; user_input_post = ""
    141       ; prompt = "COMMAND"
    142       ; on_enter =
    143           (fun command ->
    144             if String.equal (String.trim command) String.empty
    145             then (
    146               Common.Term.cursor t None;
    147               render t state)
    148             else (
    149               let content = Grep.get_file_names tag |> Array.of_list in
    150               let content =
    151                 Array.map
    152                   (fun file_name -> Grep.get_full_file_content_content file_name)
    153                   content
    154                 |> Array.to_list
    155               in
    156               let content = Grep.parse_full_content content in
    157               let content_pretty =
    158                 Grep.pretty_print_parsed_content content |> Array.of_list
    159               in
    160               Arbitrary_command.run
    161                 t
    162                 ~command:(Scanf.unescaped command)
    163                 ~content:content_line
    164                 ~selected_file
    165                 ~on_return:(fun result ->
    166                   Common.Term.cursor t None;
    167                   render
    168                     t
    169                     { state with
    170                       content = Array.of_list content
    171                     ; content_pretty
    172                     ; pos = 0, content_start
    173                     ; scroll = 0
    174                     ; output = Some result
    175                     })))
    176       ; on_cancel =
    177           (fun _ ->
    178             Common.Term.cursor t None;
    179             render t state)
    180       }
    181     in
    182     Input_prompt.render t input_state
    183   | `Key (`ASCII 's', []) ->
    184     let (input_state : Input_prompt.state) =
    185       { screen = img
    186       ; user_input_pre = ""
    187       ; user_input_post = ""
    188       ; prompt = "REGEXP"
    189       ; on_enter =
    190           (fun tag ->
    191             try
    192               let content = Grep.get_file_names tag |> Array.of_list in
    193               let oc = open_out "/tmp/stitch-output" in
    194               Printf.fprintf oc "%s\n" (String.concat "\n" (content |> Array.to_list));
    195               close_out oc;
    196               let content =
    197                 Array.map
    198                   (fun file_name -> Grep.get_full_file_content_content file_name)
    199                   content
    200                 |> Array.to_list
    201               in
    202               let content = Grep.parse_full_content content in
    203               let content_pretty =
    204                 Grep.pretty_print_parsed_content content |> Array.of_list
    205               in
    206               Common.Term.cursor t None;
    207               render
    208                 t
    209                 { state with
    210                   content = content |> Array.of_list
    211                 ; content_pretty
    212                 ; pos = 0, content_start
    213                 ; scroll = 0
    214                 ; tag
    215                 }
    216             with
    217             | _ ->
    218               let oc = open_out "/tmp/stitch-error" in
    219               Printf.fprintf oc "%s\n" (Printexc.get_backtrace ());
    220               close_out oc;
    221               raise (Invalid_argument "FAILURE"))
    222       ; on_cancel =
    223           (fun _ ->
    224             Common.Term.cursor t None;
    225             render t state)
    226       }
    227     in
    228     Input_prompt.render t input_state
    229   | `Key (`ASCII '?', []) -> Help_screen.render t { go_back = (fun () -> render t state) }
    230   | `Key (`ASCII '2', []) -> goto_todos_view (fun () -> render t state)
    231   | `Key (`ASCII '3', []) -> goto_done_view (fun () -> render t state)
    232   | `Key (`ASCII 'e', []) | `Key (`Enter, []) ->
    233     (* Editor might be set with extra args, in that case we need to separate these *)
    234     let[@warning "-8"] (editor :: args) =
    235       String.split_on_char ' ' (Sys.getenv "EDITOR")
    236     in
    237     let file_number_offset, _ = Array.get content_pretty (y - content_start) in
    238     let selected_file, line_number, _, _ =
    239       Array.get content (y - content_start - file_number_offset)
    240     in
    241     let full_path_file = Grep.execution_directory ^ "/" ^ selected_file in
    242     (* Because each file title consists of two lines, we need to account for the offset
    243        it adds by removing the file_number *)
    244     let line_number_arg = "+" ^ Int.to_string line_number in
    245     let full_args =
    246       Array.append (Array.of_list args) [| ""; line_number_arg; full_path_file |]
    247     in
    248     Common.Term.cursor t (Some (0, 0));
    249     let _ =
    250       Unix.create_process_env
    251         editor
    252         full_args
    253         (Unix.environment ())
    254         Unix.stdin
    255         Unix.stdout
    256         Unix.stderr
    257     in
    258     let rec run_editor () =
    259       match Unix.wait () with
    260       | _, _ ->
    261         Common.Term.cursor t None;
    262         let content, content_pretty = refresh tag in
    263         render t { state with content; content_pretty }
    264       (* Capture resizing events *)
    265       | exception Unix.Unix_error (Unix.EINTR, _, _) -> run_editor ()
    266       | exception Unix.Unix_error (_, str_err, str_err_2) ->
    267         let oc = open_out "/tmp/stitch-error" in
    268         Printf.fprintf oc "%s: %s" str_err str_err_2;
    269         close_out oc;
    270         failwith "ERROR"
    271     in
    272     run_editor ()
    273   | `Key (`Arrow d, _) ->
    274     (match d with
    275      | `Up -> scroll_up ()
    276      | `Down -> scroll_down ()
    277      | _ -> render t state)
    278   | _ -> render t state