stitch
Note taking for messy minimalists
git clone git://mccd.space/stitch| Log | Files | Refs | README | LICENSE | Mail |
headlines.ml (9742B)
1 module Grep = Grep
2 module Common = Common
3 open Notty
4 module Input_prompt = Input_prompt
5 module Stitched_article = Stitched_article
6 module Help_screen = Help_screen
7
8 type state =
9 { pos : int * int
10 ; scroll : int
11 ; content : (string * string) array
12 ; content_pretty : string array
13 ; goto_todos_view : (unit -> unit) -> unit
14 ; goto_done_view : (unit -> unit) -> unit
15 ; output : string option
16 ; tag : string
17 ; hide_file_name : bool
18 }
19
20 let init ~goto_done_view ~goto_todos_view ~regexp =
21 let content = Grep.get_tagged_headlines regexp () |> Option.map Grep.parse_headlines in
22 match content with
23 | None ->
24 Error
25 "There are no notes to load. Make sure to create some in order to view them in \
26 stitch. Check stitch --help for how to get started."
27 | Some content ->
28 if Array.length content == 0
29 then (
30 print_endline "Unable to load any content. Check grep commands and make sure there are notes";
31 exit 0)
32 else (
33 let hide_file_name = true in
34 let content_pretty = content |> Grep.pretty_format ~hide_file_name in
35 Ok
36 { pos = 0, 2
37 ; scroll = 0
38 ; content
39 ; content_pretty
40 ; goto_done_view
41 ; goto_todos_view
42 ; output = None
43 ; tag = regexp
44 ; hide_file_name
45 })
46
47
48 let title ~tag =
49 match tag with
50 | "" -> I.strf ~attr:A.(st bold) "%s" "Notes" |> I.pad ~l:0 ~t:0
51 | a -> I.strf ~attr:A.(st bold) "%s > %s" "Notes" a |> I.pad ~l:0 ~t:0
52
53
54 let refresh ~hide_file_name regexp =
55 let content =
56 Grep.get_tagged_headlines regexp ()
57 |> Option.map Grep.parse_headlines
58 |> Option.value ~default:[||]
59 in
60 content, Grep.pretty_format ~hide_file_name content
61
62
63 let rec render
64 t
65 ({ pos
66 ; scroll
67 ; content
68 ; content_pretty
69 ; goto_todos_view
70 ; goto_done_view
71 ; tag
72 ; output
73 ; hide_file_name
74 } as state)
75 =
76 let content_start = 2 in
77 let size_x, size_y = Common.Term.size t in
78 let x, y = pos in
79 let content_position = y - content_start in
80 let img =
81 let output_info =
82 match output with
83 | Some line ->
84 I.strf "%s%s" (String.escaped line) (String.make size_x ' ')
85 |> I.pad ~t:(size_y - 1)
86 | None -> I.empty
87 in
88 let dot =
89 if Array.length content_pretty = 0
90 then I.empty
91 else I.string A.(st bold) ">" |> I.pad ~l:0 ~t:(y - scroll)
92 and elements =
93 Array.mapi
94 (fun i el ->
95 if i + 1 == y - scroll
96 then I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start)
97 else I.strf "%s" el |> I.pad ~l:2 ~t:(i + content_start))
98 (Basic.array_drop scroll content_pretty)
99 in
100 let open I in
101 Array.fold_left
102 (fun sum el -> sum </> el)
103 (title ~tag </> dot </> output_info)
104 elements
105 in
106 Common.Term.image t img;
107 let content_end = Array.length content_pretty + (content_start - 1) in
108 let scroll_up ?(amount = 1) () =
109 let scroll =
110 if y - content_start - scroll - amount < 0 then max (scroll - amount) 0 else scroll
111 in
112 render t { state with pos = x, max (y - amount) content_start; scroll; output = None }
113 in
114 let scroll_down ?(amount = 1) () =
115 let scroll = if y - scroll >= size_y - amount then scroll + amount else scroll in
116 render t { state with pos = x, min (y + amount) content_end; scroll; output = None }
117 in
118 match Common.Term.event t with
119 | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> ()
120 | `Mouse (`Press (`Scroll s), _, _) ->
121 (match s with
122 | `Down -> scroll_down ()
123 | `Up -> scroll_up ())
124 | `Resize _ -> render t state
125 | `Mouse ((`Press _ | `Drag), (_, y), _) ->
126 render t { state with pos = 0, min y content_end; output = None }
127 | `Key (`ASCII '?', []) -> Help_screen.render t { go_back = (fun () -> render t state) }
128 | `Key (`ASCII '2', []) -> goto_todos_view (fun () -> render t state)
129 | `Key (`ASCII 'o', []) ->
130 let (input_state : Input_prompt.state) =
131 { screen = img
132 ; user_input_pre = ""
133 ; user_input_post = ""
134 ; prompt = "EXPORT TO"
135 ; on_enter =
136 (fun path ->
137 Common.Term.cursor t None;
138 if String.equal (String.trim path) String.empty |> not
139 then (
140 let export = String.concat "\n" (Array.to_list content_pretty) in
141 let result = Export.to_path path ~content:export in
142 render t { state with output = Some result })
143 else render t { state with output = Some "Export Blank; skipping" })
144 ; on_cancel =
145 (fun _ ->
146 Common.Term.cursor t None;
147 render t { state with output = Some "Export Cancelled" })
148 }
149 in
150 Input_prompt.render t input_state
151 | `Key (`ASCII 'h', []) ->
152 let hide_file_name = not hide_file_name in
153 let content_pretty = content |> Grep.pretty_format ~hide_file_name in
154 render t { state with hide_file_name; content_pretty }
155 | `Key (`ASCII '3', []) -> goto_done_view (fun () -> render t state)
156 | `Key (`ASCII 'f', []) ->
157 let content =
158 Array.map
159 (fun (file_name, _) -> Grep.get_full_file_content_content file_name)
160 content
161 |> Array.to_list
162 in
163 let full_content = Grep.parse_full_content content in
164 let full_content_pretty =
165 Grep.pretty_print_parsed_content full_content |> Array.of_list
166 in
167 Stitched_article.render
168 t
169 { pos = 0, Stitched_article.content_start
170 ; content = full_content |> Array.of_list
171 ; content_pretty = full_content_pretty
172 ; scroll = 0
173 ; tag
174 ; output = None
175 ; go_back = (fun () -> render t state)
176 ; goto_todos_view
177 ; goto_done_view
178 }
179 | `Key (`ASCII 's', []) ->
180 let (input_state : Input_prompt.state) =
181 { screen = img
182 ; user_input_post = ""
183 ; user_input_pre = ""
184 ; prompt = "REGEXP"
185 ; on_enter =
186 (fun tag ->
187 let content =
188 Grep.get_tagged_headlines tag ()
189 |> Option.map Grep.parse_headlines
190 |> Option.value ~default:[||]
191 in
192 let content_pretty = Grep.pretty_format ~hide_file_name content in
193 Common.Term.cursor t None;
194 render
195 t
196 { state with
197 content
198 ; content_pretty
199 ; pos = 0, content_start
200 ; scroll = 0
201 ; tag
202 })
203 ; on_cancel =
204 (fun _ ->
205 Common.Term.cursor t None;
206 render t state)
207 }
208 in
209 Input_prompt.render t input_state
210 | `Key (`ASCII 'D', [ `Ctrl ]) | `Key (`Page `Down, []) ->
211 scroll_down ~amount:(size_y / 2) ()
212 | `Key (`ASCII 'U', [ `Ctrl ]) | `Key (`Page `Up, []) ->
213 scroll_up ~amount:(size_y / 2) ()
214 | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down ()
215 | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) -> scroll_up ()
216 | `Key (`Arrow d, _) ->
217 (match d with
218 | `Up -> scroll_up ()
219 | `Down -> scroll_down ()
220 | _ -> render t state)
221 | `Key (`ASCII '!', []) ->
222 let selected_file, content = Array.get content content_position in
223 let selected_file = Grep.execution_directory ^ "/" ^ selected_file in
224 let (input_state : Input_prompt.state) =
225 { screen = img
226 ; user_input_post = ""
227 ; user_input_pre = ""
228 ; prompt = "COMMAND"
229 ; on_enter =
230 (fun command ->
231 if String.equal (String.trim command) String.empty
232 then (
233 Common.Term.cursor t None;
234 render t state)
235 else
236 Arbitrary_command.run
237 t
238 ~command:(Scanf.unescaped command)
239 ~content
240 ~selected_file
241 ~on_return:(fun result ->
242 let content =
243 Grep.get_tagged_headlines tag ()
244 |> Option.map Grep.parse_headlines
245 |> Option.value ~default:[||]
246 in
247 let content_pretty = Grep.pretty_format ~hide_file_name content in
248 Common.Term.cursor t None;
249 render
250 t
251 { state with
252 content
253 ; content_pretty
254 ; pos = 0, content_start
255 ; scroll = 0
256 ; output = Some result
257 }))
258 ; on_cancel =
259 (fun _ ->
260 Common.Term.cursor t None;
261 render t state)
262 }
263 in
264 Input_prompt.render t input_state
265 | `Key (`ASCII 'e', []) | `Key (`Enter, []) ->
266 (* Editor might be set with extra args, in that case we need to separate these *)
267 let[@warning "-8"] (editor :: args) =
268 String.split_on_char ' ' (Sys.getenv "EDITOR")
269 in
270 let selected_file, _ = Array.get content content_position in
271 let full_path_file = Grep.execution_directory ^ "/" ^ selected_file in
272 let full_args = Array.append (Array.of_list args) [| "+1"; full_path_file |] in
273 Common.Term.cursor t (Some (0, 0));
274 let _ =
275 Unix.create_process_env
276 editor
277 full_args
278 (Unix.environment ())
279 Unix.stdin
280 Unix.stdout
281 Unix.stderr
282 in
283 let rec run_editor () =
284 match Unix.wait () with
285 | _, _ ->
286 Common.Term.cursor t None;
287 let content, content_pretty = refresh ~hide_file_name tag in
288 render t { state with content; content_pretty }
289 (* Capture resizing events *)
290 | exception Unix.Unix_error (Unix.EINTR, _, _) -> run_editor ()
291 | exception Unix.Unix_error (_, _, _) -> failwith "ERROR"
292 in
293 run_editor ()
294 | _ -> render t state