stitch
Note taking for messy minimalists
git clone git://mccd.space/stitch| Log | Files | Refs | README | LICENSE | Mail |
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