Commit
+220 -149 +/-7 browse
1 | diff --git a/bin/note.ml b/bin/note.ml |
2 | index 85394a5..cb27b59 100644 |
3 | --- a/bin/note.ml |
4 | +++ b/bin/note.ml |
5 | @@ -1,22 +1,16 @@ |
6 | open Core |
7 | open Note_lib |
8 | |
9 | - let cfg = Config.config_path |> Config.load |
10 | - |
11 | - let manifest = cfg.state_dir |> Manifest.load_or_init |
12 | + (* todo global locking *) |
13 | |
14 | - let root = match manifest |> Manifest.find ~path:"/" with |
15 | - | Some item -> (item.slug |> Slug.to_string |> In_channel.read_all |> Note.of_string) |
16 | - | None -> |
17 | - let manifest = manifest |> Manifest.create ~path:"/" in |
18 | - let last = manifest.items |> List.hd_exn in |
19 | - let slug = last.slug |> Slug.to_string in |
20 | - let root = Note.root_template |> Note.of_string in |
21 | - slug |> Out_channel.write_all ~data: (root |> Note.to_string) ; |
22 | - manifest |> Manifest.save ; |
23 | - root |
24 | + let cfg = Config.config_path |> Config.load |
25 | |
26 | - let notes = (Note.Tree (root, manifest |> Note.resolve_manifest ~path:"/")) |
27 | + let options : Note.Adapter.options = |
28 | + { |
29 | + state_dir = cfg.state_dir; |
30 | + on_modification = cfg.on_modification; |
31 | + editor = cfg.editor; |
32 | + } |
33 | |
34 | let get_title (note : Note.note) = note.frontmatter.path |
35 | |
36 | @@ -46,16 +40,10 @@ let key_arg = |
37 | string_keys) |
38 | Config.Key.of_string |
39 | |
40 | - let last_slug = manifest.items |> List.map ~f:(fun item -> item.slug) |> List.hd |
41 | - |
42 | (* |
43 | * commands |
44 | *) |
45 | |
46 | - let cat_note = |
47 | - Command.basic ~summary:"show the current configuration" |
48 | - (Command.Param.return (fun () -> ())) |
49 | - |
50 | let config_show = |
51 | Command.basic ~summary:"show the current configuration" |
52 | (Command.Param.return (fun () -> print_string (Config.to_string cfg))) |
53 | @@ -76,6 +64,25 @@ let config_set = |
54 | let cfg = Config.set cfg key value in |
55 | Config.save cfg] |
56 | |
57 | + let cat_notes = |
58 | + let open Command.Let_syntax in |
59 | + Command.basic ~summary:"list existing notes" |
60 | + ~readme:(fun () -> |
61 | + {| |
62 | + List one or more notes that match the filter criteria, if no filter criteria |
63 | + is provided then all notes will be listed. |
64 | + |}) |
65 | + [%map_open |
66 | + let paths = anon (sequence ("path" %: string)) in |
67 | + fun () -> |
68 | + let paths = match paths with [] -> [ "/" ] | paths -> paths in |
69 | + paths |
70 | + |> List.map ~f:(fun path -> options |> Note.Adapter.load ~path) |
71 | + |> List.iter ~f:(fun notes -> |
72 | + let note = notes |> Note.fst in |
73 | + note |> Note.to_string |> print_endline) |
74 | + ] |
75 | + |
76 | let create_note = |
77 | let open Command.Let_syntax in |
78 | Command.basic ~summary:"create a new note" |
79 | @@ -85,26 +92,21 @@ Create a new note and save it to disk in your configured state_dir. The |
80 | on_modification callback will be invoked if the file is committed to disk. |
81 | |}) |
82 | [%map_open |
83 | - let _ = |
84 | + let stdin = |
85 | flag "stdin" (optional bool) |
86 | ~doc:"read content from stdin and copy it into the note body" |
87 | - and path = flag "path" (required name_arg) ~doc:"path" |
88 | + and path = anon ("path" %: name_arg) |
89 | and tags = flag "tag" (listed tag_arg) ~doc:"tag" |
90 | and description = |
91 | flag "description" (optional string) ~doc:"description" |
92 | in |
93 | fun () -> |
94 | - let manifest = manifest |> Manifest.create ~path in |
95 | - let last = List.hd_exn manifest.items in |
96 | - let note : Note.note = |
97 | - { |
98 | - frontmatter = { path = last.path; description; tags }; |
99 | - content = ""; |
100 | - } |
101 | + let content = |
102 | + match stdin with |
103 | + | Some _ -> Some (In_channel.stdin |> In_channel.input_all) |
104 | + | None -> None |
105 | in |
106 | - Io.create ~callback:None ~content:(note |> Note.to_string) |
107 | - (Slug.to_string last.slug); |
108 | - manifest |> Manifest.save] |
109 | + options |> Note.Adapter.create ~description ~tags ~content ~path] |
110 | |
111 | let delete_note = |
112 | let open Command.Let_syntax in |
113 | @@ -114,10 +116,8 @@ let delete_note = |
114 | Delete the first note that matches the filter criteria. |
115 | |}) |
116 | [%map_open |
117 | - let path = flag "path" (required name_arg) ~doc:"path" in |
118 | - fun () -> |
119 | - let manifest = manifest |> Manifest.remove ~path in |
120 | - manifest |> Manifest.save] |
121 | + let path = anon ("path" %: name_arg) in |
122 | + fun () -> options |> Note.Adapter.remove ~path] |
123 | |
124 | let edit_note = |
125 | let open Command.Let_syntax in |
126 | @@ -127,12 +127,12 @@ let edit_note = |
127 | Select a note that matches the filter criteria and open it in your text editor. |
128 | |}) |
129 | [%map_open |
130 | - let _ = flag "path" (required name_arg) ~doc:"path" |
131 | + let path = anon ("path" %: name_arg) |
132 | and _ = flag "tag" (listed tag_arg) ~doc:"tag" |
133 | and _ = |
134 | flag "description" (optional_with_default "" string) ~doc:"description" |
135 | in |
136 | - fun () -> ()] |
137 | + fun () -> options |> Note.Adapter.edit ~path] |
138 | |
139 | let list_notes = |
140 | let open Command.Let_syntax in |
141 | @@ -143,24 +143,14 @@ List one or more notes that match the filter criteria, if no filter criteria |
142 | is provided then all notes will be listed. |
143 | |}) |
144 | [%map_open |
145 | - let _ = anon (sequence ("path" %: string)) in |
146 | + let paths = anon (sequence ("path" %: string)) in |
147 | fun () -> |
148 | - notes |> Display.convert_tree |> Display.Hierarchical.to_string |
149 | - |> print_endline |
150 | - (* |
151 | - let items = |
152 | - match paths |> List.length with |
153 | - | 0 -> [ manifest.items ] |
154 | - | _ -> |
155 | - paths |> List.map ~f:(fun path -> manifest |> Manifest.list ~path) |
156 | - in |
157 | - items |
158 | - |> List.iter ~f:(fun items -> |
159 | - items |
160 | - |> List.iter ~f:(fun item -> |
161 | - print_endline |
162 | - (item |> Manifest.Item.to_json |> Ezjsonm.to_string))) |
163 | - *)] |
164 | + let paths = match paths with [] -> [ "/" ] | paths -> paths in |
165 | + paths |
166 | + |> List.map ~f:(fun path -> options |> Note.Adapter.load ~path) |
167 | + |> List.iter ~f:(fun notes -> |
168 | + notes |> Display.convert_tree |> Display.Hierarchical.to_string |
169 | + |> print_endline)] |
170 | |
171 | let sync = |
172 | Command.basic ~summary:"sync notes to a remote server" |
173 | @@ -179,7 +169,7 @@ let run = |
174 | Command.run ~version ~build_info:"" |
175 | (Command.group ~summary:"Note is a simple CLI based note taking application" |
176 | [ |
177 | - ("cat", cat_note); |
178 | + ("cat", cat_notes); |
179 | ("create", create_note); |
180 | ( "config", |
181 | Command.group ~summary:"config management" |
182 | diff --git a/lib/display.ml b/lib/display.ml |
183 | index 57dc53d..408a575 100644 |
184 | --- a/lib/display.ml |
185 | +++ b/lib/display.ml |
186 | @@ -185,7 +185,7 @@ end |
187 | |
188 | let rec convert_tree tree = |
189 | let (Note.Tree (note, others)) = tree in |
190 | - let title = note.frontmatter.path in |
191 | + let title = Filename.basename note.frontmatter.path in |
192 | let title = "[" ^ title ^ "]" in |
193 | Hierarchical.Tree (title, List.map ~f:convert_tree others) |
194 | |
195 | diff --git a/lib/io.ml b/lib/io.ml |
196 | deleted file mode 100644 |
197 | index f950e67..0000000 |
198 | --- a/lib/io.ml |
199 | +++ /dev/null |
200 | @@ -1,43 +0,0 @@ |
201 | - open Core |
202 | - |
203 | - let create ~callback ~content dest = |
204 | - Out_channel.write_all ~data:content dest; |
205 | - match callback with Some cmd -> Sys.command_exn cmd | None -> () |
206 | - |
207 | - let create_on_change ~callback ~editor ~content dest = |
208 | - let tmp_file = Filename.temp_file "note" ".md" in |
209 | - Out_channel.write_all ~data:content tmp_file; |
210 | - let command = sprintf "%s %s" editor tmp_file in |
211 | - Sys.command_exn command; |
212 | - let new_content = In_channel.read_all tmp_file in |
213 | - if not (String.equal content new_content) then |
214 | - Out_channel.write_all ~data:new_content dest; |
215 | - match callback with Some cmd -> Sys.command_exn cmd | None -> () |
216 | - |
217 | - let edit ~callback ~editor path = |
218 | - let orig_content = In_channel.read_all path in |
219 | - let command = sprintf "%s %s" editor path in |
220 | - Sys.command_exn command; |
221 | - let new_content = In_channel.read_all path in |
222 | - if not (String.equal orig_content new_content) then |
223 | - match callback with Some cmd -> Sys.command_exn cmd | None -> () |
224 | - |
225 | - let delete ~callback ~title path = |
226 | - let colorize_title title = |
227 | - let open ANSITerminal in |
228 | - (sprintf [ ANSITerminal.Bold ] "%s" title) in |
229 | - print_endline (sprintf "Are you sure you want to delete the following note: %s?" (colorize_title title)) ; |
230 | - print_endline "Type YES to continue" ; |
231 | - let input = In_channel.(input_line stdin) in |
232 | - match input with |
233 | - | Some value -> |
234 | - if String.equal value "YES" then |
235 | - (Unix.remove path ; |
236 | - (match callback with Some cmd -> Sys.command_exn cmd | None -> ())) |
237 | - else |
238 | - print_endline "No changes made" |
239 | - | None -> () |
240 | - |
241 | - |
242 | - let read path = |
243 | - In_channel.read_all path |
244 | diff --git a/lib/manifest.ml b/lib/manifest.ml |
245 | index 7dc57a9..08dec95 100644 |
246 | --- a/lib/manifest.ml |
247 | +++ b/lib/manifest.ml |
248 | @@ -99,9 +99,7 @@ let load_or_init state_dir = |
249 | make state_dir |
250 | |
251 | let save manifest = |
252 | - manifest |> lock; |
253 | - Out_channel.write_all ~data:(to_string manifest) (manifest |> mpath); |
254 | - manifest |> unlock |
255 | + Out_channel.write_all ~data:(to_string manifest) (manifest |> mpath) |
256 | |
257 | let find ~path manifest = |
258 | manifest.items |> List.find ~f:(fun item -> Filename.equal item.path path) |
259 | diff --git a/lib/note.ml b/lib/note.ml |
260 | index db6dcd2..bbecba2 100644 |
261 | --- a/lib/note.ml |
262 | +++ b/lib/note.ml |
263 | @@ -45,6 +45,10 @@ type note = { frontmatter : Frontmatter.t; content : string } |
264 | |
265 | and tree = Tree of (note * tree list) |
266 | |
267 | + let fst tree = |
268 | + let (Tree (note, _)) = tree in |
269 | + note |
270 | + |
271 | let root_template = |
272 | {| |
273 | --- |
274 | @@ -54,9 +58,12 @@ tags: [] |
275 | --- |
276 | |
277 | # This is a Note! |
278 | - |
279 | |} |
280 | |
281 | + let rec flatten ~accm tree = |
282 | + let (Tree (note, others)) = tree in |
283 | + List.fold ~init:(note :: accm) ~f:(fun accm note -> flatten ~accm note) others |
284 | + |
285 | let rec extract_structured_data (accm : Ezjsonm.value list) (doc : Omd.doc) : |
286 | Ezjsonm.value list = |
287 | match doc with |
288 | @@ -102,11 +109,11 @@ let of_string ?(path = None) content = |
289 | let frontmatter = |
290 | meta_str |> Yaml.of_string_exn |> Frontmatter.of_json ~path |
291 | in |
292 | + (* read second half of note as "content" *) |
293 | + let content = String.slice content ((List.nth_exn indexes 1)+3) 0 in |
294 | { frontmatter; content } |
295 | else { frontmatter = Frontmatter.empty; content } |
296 | |
297 | - let root = Tree (of_string root_template, []) |
298 | - |
299 | let rec resolve_manifest ~path manifest = |
300 | let items = |
301 | match manifest |> Manifest.list ~path with |
302 | @@ -123,39 +130,99 @@ let rec resolve_manifest ~path manifest = |
303 | in |
304 | items |
305 | |
306 | - (* |
307 | - module Adapter (M : sig |
308 | - val db : Manifest.t |
309 | - end) = |
310 | - struct |
311 | - let read path = |
312 | - let result = M.db |> Manifest.find ~path in |
313 | - match result with |
314 | - | Some entry -> |
315 | - let note = entry.slug |> In_channel.read_all |> of_string in |
316 | - note |
317 | + (* high level adapter *) |
318 | + module Adapter = struct |
319 | + type options = { |
320 | + state_dir : string; |
321 | + editor : string; |
322 | + on_modification : string option; |
323 | + } |
324 | + |
325 | + let editor_command ~editor path = Format.sprintf "%s %s" editor path |
326 | + |
327 | + let run_or_noop command = |
328 | + match command with Some command -> command |> Sys.command_exn | None -> () |
329 | + |
330 | + let load ~path options = |
331 | + let manifest = options.state_dir |> Manifest.load_or_init in |
332 | + (* initialize the root note *) |
333 | + let root = |
334 | + match manifest |> Manifest.find ~path with |
335 | + | Some item -> |
336 | + item.slug |> Slug.to_string |> In_channel.read_all |> of_string |
337 | + | None -> ( |
338 | + match path with |
339 | + | "/" -> |
340 | + let manifest = manifest |> Manifest.create ~path:"/" in |
341 | + let last = manifest.items |> List.hd_exn in |
342 | + let slug = last.slug |> Slug.to_string in |
343 | + let root = root_template |> of_string in |
344 | + slug |> Out_channel.write_all ~data:(root |> to_string); |
345 | + manifest |> Manifest.save; |
346 | + root |
347 | + | _ -> failwith "not found") |
348 | + in |
349 | + Tree (root, manifest |> resolve_manifest ~path) |
350 | + |
351 | + let find ~path options = |
352 | + let manifest = options.state_dir |> Manifest.load_or_init in |
353 | + let item = manifest |> Manifest.find ~path in |
354 | + match item with |
355 | + | Some item -> |
356 | + let slug = item.slug in |
357 | + let note = slug |> Slug.to_string |> In_channel.read_all |> of_string in |
358 | + Some note |
359 | + | None -> failwith "not found" |
360 | + |
361 | + let create ?(description = None) ?(tags = []) ?(content = None) ~path options |
362 | + = |
363 | + let manifest = options.state_dir |> Manifest.load_or_init in |
364 | + let manifest = manifest |> Manifest.create ~path in |
365 | + let item = manifest.items |> List.hd_exn in |
366 | + let slug = item.slug in |
367 | + (match content with |
368 | + | Some content -> |
369 | + let note = { frontmatter = { path; description; tags }; content } in |
370 | + slug |> Slug.to_string |> Out_channel.write_all ~data:(note |> to_string) |
371 | + | None -> |
372 | + let note = |
373 | + { frontmatter = { path; description; tags }; content = "" } |
374 | + in |
375 | + slug |> Slug.to_string |> Out_channel.write_all ~data:(note |> to_string); |
376 | + slug |> Slug.to_string |
377 | + |> editor_command ~editor:options.editor |
378 | + |> Sys.command_exn); |
379 | + options.on_modification |> run_or_noop; |
380 | + manifest |> Manifest.save |
381 | + |
382 | + let remove ~path options = |
383 | + let manifest = options.state_dir |> Manifest.load_or_init in |
384 | + let item = manifest |> Manifest.find ~path in |
385 | + match item with |
386 | + | Some item -> |
387 | + let slug = item.slug in |
388 | + let manifest = manifest |> Manifest.remove ~path in |
389 | + slug |> Slug.to_string |> Unix.remove; |
390 | + options.on_modification |> run_or_noop; |
391 | + manifest |> Manifest.save |
392 | | None -> failwith "not found" |
393 | |
394 | - let save ~path note = |
395 | - let description = note.frontmatter.description in |
396 | - let tags = note.frontmatter.tags in |
397 | - M.db |> Manifest.update ~path ~description ~tags |
398 | + let edit ~path options = |
399 | + let manifest = options.state_dir |> Manifest.load_or_init in |
400 | + let item = manifest |> Manifest.find ~path in |
401 | + match item with |
402 | + | Some item -> |
403 | + let slug = item.slug in |
404 | + slug |> Slug.to_string |
405 | + |> editor_command ~editor:options.editor |
406 | + |> Sys.command_exn; |
407 | + let note = slug |> Slug.to_string |> In_channel.read_all |> of_string in |
408 | + let adjusted_path = note.frontmatter.path in |
409 | + (if not (Filename.equal adjusted_path item.path) then |
410 | + let manifest = |
411 | + manifest |> Manifest.move ~source:item.path ~dest:adjusted_path |
412 | + in |
413 | + manifest |> Manifest.save); |
414 | + options.on_modification |> run_or_noop |
415 | + | None -> failwith "not found" |
416 | end |
417 | - |
418 | - let rec resolve_manifest ~tree ~path manifest = |
419 | - let items = manifest |> Manifest.list ~path in |
420 | - let items = |
421 | - items |
422 | - |> List.map ~f:(fun item -> |
423 | - let logical_path = item |> Manifest.to_path ~manifest in |
424 | - let slug = item.slug |> Slug.of_string in |
425 | - let note = |
426 | - slug |> Slug.to_string |> In_channel.read_all |
427 | - |> of_string ~slug:(Some slug) |
428 | - in |
429 | - manifest |
430 | - |> resolve_manifest ~tree:(Tree (note, [])) ~path:logical_path) |
431 | - in |
432 | - let (Tree (root, _)) = tree in |
433 | - Tree (root, items) |
434 | - *) |
435 | diff --git a/lib/util.ml b/lib/util.ml |
436 | new file mode 100644 |
437 | index 0000000..1f494d3 |
438 | --- /dev/null |
439 | +++ b/lib/util.ml |
440 | @@ -0,0 +1,10 @@ |
441 | + open Core |
442 | + |
443 | + let prompt ~callback message = |
444 | + print_endline message; |
445 | + print_endline "Type YES to continue"; |
446 | + let input = In_channel.(input_line stdin) in |
447 | + match input with |
448 | + | Some value -> ( |
449 | + match value with "YES" -> callback () | _ -> print_endline "aborted") |
450 | + | None -> () |
451 | diff --git a/test/note_test.ml b/test/note_test.ml |
452 | index 8fa6d3d..919e08b 100644 |
453 | --- a/test/note_test.ml |
454 | +++ b/test/note_test.ml |
455 | @@ -1,18 +1,38 @@ |
456 | open Core |
457 | open Note_lib |
458 | |
459 | + let parsing () = |
460 | + let n1 = {| |
461 | + --- |
462 | + path: /fuu |
463 | + tags: ["bar"] |
464 | + description: "baz" |
465 | + --- |
466 | + # Hello World|} in |
467 | + let n1 = n1 |> Note.of_string in |
468 | + Alcotest.(check string) "path" "/fuu" n1.frontmatter.path ; |
469 | + let tag = n1.frontmatter.tags |> List.hd_exn in |
470 | + Alcotest.(check string) "tag" "bar" tag ; |
471 | + let description = (Option.value_exn n1.frontmatter.description) in |
472 | + Alcotest.(check string) "description" "baz" description ; |
473 | + let content = n1.content in |
474 | + Alcotest.(check string) "content" "\n# Hello World" content |
475 | + |
476 | + |
477 | let load_manifest () = |
478 | let state_dir = Filename.temp_dir "note-test" "" in |
479 | let manifest = Manifest.load_or_init state_dir in |
480 | - let note_root = |
481 | - Note.of_string {| |
482 | + let note_root = |
483 | + Note.of_string |
484 | + {| |
485 | --- |
486 | path: "/" |
487 | description: "all notes desend from here" |
488 | tags: [] |
489 | --- |
490 | # Root Note |
491 | - |} in |
492 | + |} |
493 | + in |
494 | let note_0 = |
495 | Note.of_string |
496 | {| |
497 | @@ -36,25 +56,54 @@ tags: ["baz", "qux"] |
498 | let manifest = manifest |> Manifest.create ~path:"/" in |
499 | let item = manifest.items |> List.hd_exn in |
500 | let slug = item.slug |> Slug.to_string in |
501 | - Io.create ~callback:None ~content:(note_root |> Note.to_string) slug ; |
502 | + Out_channel.write_all ~data:(note_root |> Note.to_string) slug; |
503 | let manifest = manifest |> Manifest.create ~path:"/note-0" in |
504 | let item = manifest.items |> List.hd_exn in |
505 | let slug = item.slug |> Slug.to_string in |
506 | - Io.create ~callback:None ~content:(note_0 |> Note.to_string) slug ; |
507 | - manifest |> Manifest.save ; |
508 | + Out_channel.write_all ~data:(note_0 |> Note.to_string) slug; |
509 | + manifest |> Manifest.save; |
510 | let manifest = manifest |> Manifest.create ~path:"/note-0/note-1" in |
511 | let item = manifest.items |> List.hd_exn in |
512 | let slug = item.slug |> Slug.to_string in |
513 | - Io.create ~callback:None ~content:(note_1 |> Note.to_string) slug ; |
514 | - manifest |> Manifest.save ; |
515 | + Out_channel.write_all ~data:(note_1 |> Note.to_string) slug; |
516 | + manifest |> Manifest.save; |
517 | let manifest = Manifest.load_or_init state_dir in |
518 | - let root = (Note.Tree ((Note.of_string ~path:(Some "/") Note.root_template), (Note.resolve_manifest ~path:"/" manifest))) in |
519 | + let root = |
520 | + Note.Tree |
521 | + ( Note.of_string ~path:(Some "/") Note.root_template, |
522 | + Note.resolve_manifest ~path:"/" manifest ) |
523 | + in |
524 | let (Note.Tree (_, others)) = root in |
525 | - Alcotest.(check int) "one" 1 (others |> List.length) |
526 | + Alcotest.(check int) "one" 1 (others |> List.length) |
527 | + |
528 | + let adapter () = |
529 | + let options : Note.Adapter.options = |
530 | + { |
531 | + state_dir = Filename.temp_dir "note-test" ""; |
532 | + editor = "true"; |
533 | + on_modification = None; |
534 | + } |
535 | + in |
536 | + let tree = options |> Note.Adapter.load ~path:"/" in |
537 | + Alcotest.(check int) |
538 | + "initialized" 1 |
539 | + (tree |> Note.flatten ~accm:[] |> List.length); |
540 | + options |> Note.Adapter.create ~content:(Some "bar") ~path:"/fuu"; |
541 | + let tree = options |> Note.Adapter.load ~path:"/" in |
542 | + Alcotest.(check int) |
543 | + "note added" 2 |
544 | + (tree |> Note.flatten ~accm:[] |> List.length); |
545 | + options |> Note.Adapter.remove ~path:"/fuu"; |
546 | + let tree = options |> Note.Adapter.load ~path:"/" in |
547 | + Alcotest.(check int) |
548 | + "note removed" 1 |
549 | + (tree |> Note.flatten ~accm:[] |> List.length) |
550 | |
551 | let () = |
552 | Alcotest.run "Note" |
553 | [ |
554 | + ("parse", [ Alcotest.test_case "parse" `Quick parsing ]); |
555 | ( "load_manifest", |
556 | [ Alcotest.test_case "load manifest" `Quick load_manifest ] ); |
557 | + ("adapter", [ Alcotest.test_case "adapter" `Quick adapter ]); |
558 | ] |