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