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