Author: Kevin Schoon [kevinschoon@gmail.com]
Hash: 5be58e6259f024f5b86ec6528d0cddbe7fc67dd2
Timestamp: Thu, 01 Jul 2021 15:15:08 +0000 (3 years ago)

+220 -149 +/-7 browse
first semi-working manifest implementation
1diff --git a/bin/note.ml b/bin/note.ml
2index 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
183index 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
196deleted file mode 100644
197index 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
245index 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
260index 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
436new file mode 100644
437index 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
452index 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 ]