stitch

Note taking for messy minimalists

git clone git://mccd.space/stitch

todos.ml (8603B)

      1 module Grep = Grep
      2 module Common = Common
      3 open Notty
      4 module Help_screen = Help_screen
      5 
      6 type state =
      7   { pos : int * int
      8   ; scroll : int
      9   ; content : (string * string) array
     10   ; content_pretty : string array
     11   ; goto_headlines : (unit -> unit) -> unit
     12   ; goto_done : (unit -> unit) -> unit
     13   ; output : string option
     14   ; tag : string
     15   ; hide_file_name : bool
     16   }
     17 
     18 let title ~tag =
     19   match tag with
     20   | "" -> I.strf ~attr:A.(st bold) "%s" "Todo" |> I.pad ~l:0 ~t:0
     21   | a -> I.strf ~attr:A.(st bold) "%s > %s" "Todo" a |> I.pad ~l:0 ~t:0
     22 
     23 
     24 let content_start = 2
     25 
     26 let init ~goto_done ~goto_headlines =
     27   let content = Grep.get_todos () |> Grep.parse_todo_string in
     28   let hide_file_name = true in
     29   let content_pretty = Grep.pretty_format_todo ~hide_file_name content in
     30   { pos = 0, content_start
     31   ; scroll = 0
     32   ; content = content |> Array.of_list
     33   ; content_pretty = content_pretty |> Array.of_list
     34   ; goto_headlines
     35   ; goto_done
     36   ; output = None
     37   ; tag = ""
     38   ; hide_file_name
     39   }
     40 
     41 
     42 let load_todos ~hide_file_name () =
     43   let todo_content = Grep.get_todos () |> Grep.parse_todo_string in
     44   let todo_pretty = Grep.pretty_format_todo ~hide_file_name todo_content in
     45   todo_content, todo_pretty
     46 
     47 
     48 let refresh ~hide_file_name tag =
     49   let content = Grep.get_tagged_todo tag () |> Grep.parse_todo_string in
     50   let content_pretty = Grep.pretty_format_todo ~hide_file_name content in
     51   content, content_pretty
     52 
     53 
     54 let rec render
     55   t
     56   ({ pos
     57    ; scroll
     58    ; content
     59    ; content_pretty
     60    ; goto_headlines
     61    ; goto_done
     62    ; output
     63    ; hide_file_name
     64    ; tag
     65    } as state)
     66   =
     67   let x, y = pos in
     68   let size_x, size_y = Common.Term.size t in
     69   let content_position = y - content_start in
     70   let img =
     71     let output_info =
     72       match output with
     73       | Some line ->
     74         I.strf "%s%s" (String.escaped line) (String.make size_x ' ')
     75         |> I.pad ~t:(size_y - 1)
     76       | None -> I.empty
     77     in
     78     let dot =
     79       if Array.length content_pretty = 0
     80       then I.empty
     81       else I.string A.(st bold) ">" |> I.pad ~l:0 ~t:(y - scroll)
     82     and elements =
     83       Array.mapi
     84         (fun i el ->
     85           if i == y - scroll
     86           then I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start)
     87           else I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start))
     88         (content_pretty |> Basic.array_drop scroll)
     89     in
     90     let open I in
     91     Array.fold_left
     92       (fun sum el -> el </> sum)
     93       (title ~tag </> dot </> output_info)
     94       elements
     95   in
     96   Common.Term.image t img;
     97   let content_end = Array.length content_pretty + (content_start - 1) in
     98   let scroll_up ?(amount = 1) () =
     99     let scroll =
    100       if y - content_start - scroll - amount < 0 then max (scroll - amount) 0 else scroll
    101     in
    102     render t { state with pos = x, max (y - amount) content_start; scroll; output = None }
    103   in
    104   let scroll_down ?(amount = 1) () =
    105     let scroll = if y - scroll >= size_y - amount then scroll + amount else scroll in
    106     render t { state with pos = x, min (y + amount) content_end; scroll; output = None }
    107   in
    108   match Common.Term.event t with
    109   | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> ()
    110   | `Mouse (`Press (`Scroll s), _, _) ->
    111     (match s with
    112      | `Down -> scroll_down ()
    113      | `Up -> scroll_up ())
    114   | `Resize _ -> render t state
    115   | `Mouse ((`Press _ | `Drag), (_, y), _) ->
    116     render t { state with pos = 0, min y content_end }
    117   | `Key (`ASCII '?', []) -> Help_screen.render t { go_back = (fun () -> render t state) }
    118   | `Key (`ASCII '1', []) -> goto_headlines (fun () -> render t state)
    119   | `Key (`ASCII '3', []) -> goto_done (fun () -> render t state)
    120   | `Key (`ASCII 'D', [ `Ctrl ]) | `Key (`Page `Down, []) ->
    121     scroll_down ~amount:(size_y / 2) ()
    122   | `Key (`ASCII 'U', [ `Ctrl ]) | `Key (`Page `Up, []) ->
    123     scroll_up ~amount:(size_y / 2) ()
    124   | `Key (`ASCII 'g', []) ->
    125     let content, content_pretty = load_todos ~hide_file_name () in
    126     let y = min (List.length content_pretty + content_start) y in
    127     render
    128       t
    129       { state with
    130         pos = x, y
    131       ; content = content |> Array.of_list
    132       ; content_pretty = Array.of_list content_pretty
    133       }
    134   | `Key (`ASCII 's', []) ->
    135     let (input_state : Input_prompt.state) =
    136       { screen = img
    137       ; user_input_pre = ""
    138       ; user_input_post = ""
    139       ; prompt = "REGEXP"
    140       ; on_enter =
    141           (fun tag ->
    142             let content = Grep.get_tagged_todo tag () |> Grep.parse_todo_string in
    143             let content_pretty = Grep.pretty_format_todo content in
    144             Common.Term.cursor t None;
    145             render
    146               t
    147               { state with
    148                 content = content |> Array.of_list
    149               ; content_pretty = content_pretty ~hide_file_name |> Array.of_list
    150               ; tag
    151               })
    152       ; on_cancel =
    153           (fun _ ->
    154             Common.Term.cursor t None;
    155             render t state)
    156       }
    157     in
    158     Input_prompt.render t input_state
    159   | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down ()
    160   | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up ()
    161   | `Key (`ASCII '!', []) ->
    162     let selected_file, content = Array.get content content_position in
    163     let selected_file = Grep.execution_directory ^ "/" ^ selected_file in
    164     let (input_state : Input_prompt.state) =
    165       { screen = img
    166       ; user_input_post = ""
    167       ; user_input_pre = ""
    168       ; prompt = "COMMAND"
    169       ; on_enter =
    170           (fun command ->
    171             if String.equal (String.trim command) String.empty
    172             then (
    173               Common.Term.cursor t None;
    174               render t state)
    175             else
    176               Arbitrary_command.run
    177                 t
    178                 ~command:(Scanf.unescaped command)
    179                 ~content
    180                 ~selected_file
    181                 ~on_return:(fun result ->
    182                   let content, content_pretty = load_todos ~hide_file_name () in
    183                   let y = min (List.length content_pretty + content_start) y in
    184                   Common.Term.cursor t None;
    185                   render
    186                     t
    187                     { state with
    188                       content = Array.of_list content
    189                     ; content_pretty = Array.of_list content_pretty
    190                     ; pos = 0, y
    191                     ; scroll = 0
    192                     ; output = Some result
    193                     }))
    194       ; on_cancel =
    195           (fun _ ->
    196             Common.Term.cursor t None;
    197             render t state)
    198       }
    199     in
    200     Input_prompt.render t input_state
    201   | `Key (`ASCII 'h', []) ->
    202     let hide_file_name = not hide_file_name in
    203     let content_pretty =
    204       Grep.pretty_format_todo ~hide_file_name (content |> Array.to_list) |> Array.of_list
    205     in
    206     render t { state with hide_file_name; content_pretty }
    207   | `Key (`ASCII 'T', [ `Ctrl ]) ->
    208     let selected_file, _ = Array.get content (y - content_start) in
    209     let _ = Grep.toggle_done selected_file in
    210     let content, content_pretty = refresh ~hide_file_name tag in
    211     let y = min (List.length content_pretty + content_start) y in
    212     render
    213       t
    214       { state with
    215         pos = x, y
    216       ; content = content |> Array.of_list
    217       ; content_pretty = Array.of_list content_pretty
    218       }
    219   | `Key (`Arrow d, _) ->
    220     (match d with
    221      | `Up -> scroll_up ()
    222      | `Down -> scroll_down ()
    223      | _ -> render t state)
    224   | `Key (`ASCII 'e', []) | `Key (`Enter, []) ->
    225     (* Editor might be set with extra args, in that case we need to separate these *)
    226     let[@warning "-8"] (editor :: args) =
    227       String.split_on_char ' ' (Sys.getenv "EDITOR")
    228     in
    229     let selected_file, _ = Array.get content content_position in
    230     let full_path_file = Grep.execution_directory ^ "/" ^ selected_file in
    231     let full_args = Array.append (Array.of_list args) [| "+1"; full_path_file |] in
    232     Common.Term.cursor t (Some (0, 0));
    233     let _ =
    234       Unix.create_process_env
    235         editor
    236         full_args
    237         (Unix.environment ())
    238         Unix.stdin
    239         Unix.stdout
    240         Unix.stderr
    241     in
    242     let rec run_editor () =
    243       match Unix.wait () with
    244       | _, _ ->
    245         Common.Term.cursor t None;
    246         let content, content_pretty = load_todos ~hide_file_name () in
    247         render
    248           t
    249           { state with
    250             content = content |> Array.of_list
    251           ; content_pretty = Array.of_list content_pretty
    252           }
    253       (* Capture resizing events *)
    254       | exception Unix.Unix_error (Unix.EINTR, _, _) -> run_editor ()
    255       | exception Unix.Unix_error (_, _, _) -> failwith "ERROR"
    256     in
    257     run_editor ()
    258   | _ -> render t state