Author: Kevin Schoon [kevinschoon@gmail.com]
Hash: d11f3e3f5e54ea926a6be4b8563d963e48f8040a
Timestamp: Wed, 03 Feb 2021 17:57:15 +0000 (3 years ago)

+177 -112 +/-2 browse
support richer config data types
1diff --git a/lib/cmd.ml b/lib/cmd.ml
2index e85a23d..5dc9ec4 100644
3--- a/lib/cmd.ml
4+++ b/lib/cmd.ml
5 @@ -1,12 +1,13 @@
6 open Core
7- open Config
8+
9+ let cfg = Config.load
10
11 let get_notes =
12 List.map
13 ~f:(fun slug ->
14 let data = In_channel.read_all (Slug.get_path slug) in
15 Note.of_string ~data slug)
16- (Slug.load (get_string load `StateDir))
17+ (Slug.load cfg.state_dir)
18
19 let filter_arg =
20 Command.Arg_type.create
21 @@ -24,11 +25,37 @@ let filter_arg =
22 let key_arg =
23 Command.Arg_type.create
24 ~complete:(fun _ ~part ->
25- let string_keys = List.map ~f:Key.to_string Key.all in
26+ let string_keys = List.map ~f:Config.Key.to_string Config.Key.all in
27+ List.filter
28+ ~f:(fun key -> String.is_substring ~substring:part key)
29+ string_keys)
30+ Config.Key.of_string
31+
32+ let column_list_arg =
33+ Command.Arg_type.create (fun value ->
34+ List.map ~f:Config.Column.of_string (String.split ~on:',' value))
35+
36+ let encoding_arg =
37+ Command.Arg_type.create
38+ ~complete:(fun _ ~part ->
39+ let string_keys =
40+ List.map ~f:Config.Encoding.to_string Config.Encoding.all
41+ in
42 List.filter
43 ~f:(fun key -> String.is_substring ~substring:part key)
44 string_keys)
45- Key.of_string
46+ Config.Encoding.of_string
47+
48+ let list_style_arg =
49+ Command.Arg_type.create
50+ ~complete:(fun _ ~part ->
51+ let string_keys =
52+ List.map ~f:Config.ListStyle.to_string Config.ListStyle.all
53+ in
54+ List.filter
55+ ~f:(fun key -> String.is_substring ~substring:part key)
56+ string_keys)
57+ Config.ListStyle.of_string
58
59 (*
60 * commands
61 @@ -50,9 +77,7 @@ json for consumption by other tools.
62 ~doc:"perform a fulltext search instead of just key comparison"
63 and encoding =
64 flag "encoding"
65- (optional_with_default
66- (Encoding.of_string (value_to_string (get load `Encoding)))
67- (Command.Arg_type.create Encoding.of_string))
68+ (optional_with_default cfg.encoding encoding_arg)
69 ~doc:"format [json | yaml | raw] (default: raw)"
70 in
71 fun () ->
72 @@ -68,14 +93,14 @@ json for consumption by other tools.
73
74 let config_show =
75 Command.basic ~summary:"show the current configuration"
76- (Command.Param.return (fun () -> print_string (to_string (populate load))))
77+ (Command.Param.return (fun () -> print_string (Config.to_string cfg)))
78
79 let config_get =
80 let open Command.Let_syntax in
81 Command.basic ~summary:"get a config value"
82 [%map_open
83 let key = anon ("key" %: key_arg) in
84- fun () -> print_endline (value_to_string (get load key))]
85+ fun () -> print_endline (Config.get cfg key)]
86
87 let config_set =
88 let open Command.Let_syntax in
89 @@ -83,9 +108,8 @@ let config_set =
90 [%map_open
91 let key = anon ("key" %: key_arg) and value = anon ("value" %: string) in
92 fun () ->
93- let cfg = load in
94- let cfg = set cfg key (value_of_string key value) in
95- save cfg]
96+ let cfg = Config.set cfg key value in
97+ Config.save cfg]
98
99 let create_note =
100 let open Command.Let_syntax in
101 @@ -102,22 +126,18 @@ on_modification callback will be invoked if the file is committed to disk.
102 and title = anon ("title" %: string)
103 and tags = anon (sequence ("tag" %: string)) in
104 fun () ->
105- let cfg = load in
106- let slug = Slug.next (get_string cfg `StateDir) in
107+ let slug = Slug.next cfg.state_dir in
108 match open_stdin with
109 | Some _ ->
110 (* reading from stdin so write directly to note *)
111 let content = In_channel.input_all In_channel.stdin in
112 let note = Note.build ~tags ~content ~title slug in
113- Io.create
114- ~callback:(get_string_opt cfg `OnModification)
115+ Io.create ~callback:cfg.on_modification
116 ~content:(Note.to_string note) (Slug.get_path slug)
117 | None ->
118 let note = Note.build ~tags ~content:"" ~title slug in
119 let init_content = Note.to_string note in
120- Io.create_on_change
121- ~callback:(get_string_opt cfg `OnModification)
122- ~editor:(get_string cfg `Editor)
123+ Io.create_on_change ~callback:cfg.on_modification ~editor:cfg.editor
124 init_content (Slug.get_path slug)]
125
126 let delete_note =
127 @@ -142,9 +162,8 @@ Delete the first note that matches the filter criteria.
128 in
129 match note with
130 | Some note ->
131- Io.delete
132- ~callback:(get_string_opt load `OnModification)
133- ~title:(Note.get_title note) (Note.get_path note)
134+ Io.delete ~callback:cfg.on_modification ~title:(Note.get_title note)
135+ (Note.get_path note)
136 | None -> failwith "not found"]
137
138 let edit_note =
139 @@ -161,15 +180,12 @@ Select a note that matches the filter criteria and open it in your text editor.
140 ~doc:"perform a fulltext search instead of just key comparison"
141 in
142 fun () ->
143- let cfg = load in
144 let open Note.Filter in
145 let filter_kind = if fulltext then Fulltext else Keys in
146 let note = find_one ~strategy:filter_kind ~args:filter_args get_notes in
147 match note with
148 | Some note ->
149- Io.edit
150- ~callback:(get_string_opt cfg `OnModification)
151- ~editor:(get_string cfg `Editor)
152+ Io.edit ~callback:cfg.on_modification ~editor:cfg.editor
153 (Note.get_path note)
154 | None -> failwith "not found"]
155
156 @@ -189,15 +205,11 @@ is provided then all notes will be listed.
157 ~doc:"perform a fulltext search instead of just key comparison"
158 and style =
159 flag "style"
160- (optional_with_default
161- (ListStyle.of_string (value_to_string (get load `ListStyle)))
162- (Arg_type.create ListStyle.of_string))
163+ (optional_with_default cfg.list_style list_style_arg)
164 ~doc:"list style [fixed | wide | simple]"
165 and columns =
166 flag "columns"
167- (optional_with_default
168- (Column.of_string_list (value_to_string (get load `ColumnList)))
169- (Arg_type.create Column.of_string_list))
170+ (optional_with_default cfg.column_list column_list_arg)
171 ~doc:"columns to include in output"
172 in
173 fun () ->
174 @@ -211,18 +223,15 @@ is provided then all notes will be listed.
175
176 let sync =
177 Command.basic ~summary:"sync notes to a remote server"
178- (Command.Param.return (fun () ->
179- let cfg = load in
180- let on_sync = Config.get_string_opt cfg `OnSync in
181- Sync.sync on_sync))
182+ (Command.Param.return (fun () -> Sync.sync cfg.on_sync))
183
184- let version =
185- (match Build_info.V1.version () with
186- | None -> "n/a"
187- | Some v -> Build_info.V1.Version.to_string v)
188+ let version =
189+ match Build_info.V1.version () with
190+ | None -> "n/a"
191+ | Some v -> Build_info.V1.Version.to_string v
192
193 let run =
194- Command.run ~version:version ~build_info:""
195+ Command.run ~version ~build_info:""
196 (Command.group ~summary:"Note is a simple CLI based note taking application"
197 [
198 ("cat", cat_note);
199 diff --git a/lib/config.ml b/lib/config.ml
200index b2cb6c9..79cf4b5 100644
201--- a/lib/config.ml
202+++ b/lib/config.ml
203 @@ -14,6 +14,8 @@ let config_path =
204 module ListStyle = struct
205 type t = [ `Fixed | `Wide | `Simple ]
206
207+ let all = [ `Fixed; `Wide; `Simple ]
208+
209 let to_string = function
210 | `Fixed -> "fixed"
211 | `Wide -> "wide"
212 @@ -29,6 +31,8 @@ end
213 module Encoding = struct
214 type t = [ `Json | `Yaml | `Raw ]
215
216+ let all = [ `Json; `Yaml; `Raw ]
217+
218 let to_string = function `Json -> "json" | `Yaml -> "yaml" | `Raw -> "raw"
219
220 let of_string = function
221 @@ -55,10 +59,6 @@ module Column = struct
222 | "words" -> `WordCount
223 | "slug" -> `Slug
224 | key -> failwith (sprintf "unsupported column type: %s" key)
225-
226- let to_string_list t = String.concat ~sep:"," (List.map ~f:to_string t)
227-
228- let of_string_list str = List.map ~f:of_string (String.split ~on:',' str)
229 end
230
231 module Key = struct
232 @@ -106,92 +106,148 @@ module Key = struct
233 | `ColumnList -> "column_list"
234 end
235
236- type t = Yaml.value
237-
238- let to_string t = Yaml.to_string_exn t
239-
240- type value =
241- | String of string option
242- | ListStyle of ListStyle.t option
243- | Encoding of Encoding.t option
244- | ColumnList of Column.t list option
245-
246- let get_default = function
247- | `StateDir -> String (Some (Filename.concat base_xdg_share_path "/note"))
248- | `LockFile -> String (Some (Filename.concat base_xdg_share_path "/note"))
249- | `Editor -> String (Sys.getenv "EDITOR")
250- | `OnModification -> String None
251- | `OnSync -> String None
252- | `ListStyle -> ListStyle (Some `Fixed)
253- | `Encoding -> Encoding (Some `Raw)
254- | `ColumnList -> ColumnList (Some [ `Title; `Tags; `WordCount; `Slug ])
255-
256- let value_of_string key s =
257- match key with
258- | `StateDir -> String (Some s)
259- | `LockFile -> String (Some s)
260- | `Editor -> String (Some s)
261- | `OnModification -> String (Some s)
262- | `OnSync -> String (Some s)
263- | `ListStyle -> ListStyle (Some (ListStyle.of_string s))
264- | `Encoding -> Encoding (Some (Encoding.of_string s))
265- | `ColumnList -> ColumnList (Some (Column.of_string_list s))
266-
267- let value_to_string value =
268- match value with
269- | String value -> ( match value with Some v -> v | None -> "" )
270- | ListStyle value -> (
271- match value with Some v -> ListStyle.to_string v | None -> "" )
272- | Encoding value -> (
273- match value with Some v -> Encoding.to_string v | None -> "" )
274- | ColumnList value -> (
275- match value with Some v -> Column.to_string_list v | None -> "" )
276+ type t = {
277+ state_dir : string;
278+ lock_file : string;
279+ editor : string;
280+ on_modification : string option;
281+ on_sync : string option;
282+ list_style : ListStyle.t;
283+ encoding : Encoding.t;
284+ column_list : Column.t list;
285+ }
286+
287+ let of_string str =
288+ let json = Yaml.of_string_exn str in
289+ let state_dir =
290+ match Ezjsonm.find_opt json [ Key.to_string `StateDir ] with
291+ | Some state_dir -> Ezjsonm.get_string state_dir
292+ | None -> Filename.concat base_xdg_share_path "/note"
293+ and lock_file =
294+ match Ezjsonm.find_opt json [ Key.to_string `LockFile ] with
295+ | Some lock_file -> Ezjsonm.get_string lock_file
296+ | None -> Filename.concat base_xdg_share_path "/note.lock"
297+ and editor =
298+ match Ezjsonm.find_opt json [ Key.to_string `Editor ] with
299+ | Some editor -> Ezjsonm.get_string editor
300+ | None -> Sys.getenv_exn "EDITOR"
301+ and on_modification =
302+ match Ezjsonm.find_opt json [ Key.to_string `OnModification ] with
303+ | Some on_modification -> Some (Ezjsonm.get_string on_modification)
304+ | None -> None
305+ and on_sync =
306+ match Ezjsonm.find_opt json [ Key.to_string `OnSync ] with
307+ | Some on_sync -> Some (Ezjsonm.get_string on_sync)
308+ | None -> None
309+ and list_style =
310+ match Ezjsonm.find_opt json [ Key.to_string `ListStyle ] with
311+ | Some list_style -> ListStyle.of_string (Ezjsonm.get_string list_style)
312+ | None -> `Fixed
313+ and encoding =
314+ match Ezjsonm.find_opt json [ Key.to_string `Encoding ] with
315+ | Some encoding -> Encoding.of_string (Ezjsonm.get_string encoding)
316+ | None -> `Raw
317+ and column_list =
318+ match Ezjsonm.find_opt json [ Key.to_string `ColumnList ] with
319+ | Some column_list ->
320+ List.map ~f:Column.of_string (Ezjsonm.get_strings column_list)
321+ | None -> [ `Title; `Tags; `WordCount; `Slug ]
322+ in
323+ {
324+ state_dir;
325+ lock_file;
326+ editor;
327+ on_modification;
328+ on_sync;
329+ list_style;
330+ encoding;
331+ column_list;
332+ }
333+
334+ let to_string t =
335+ let state_dir = Ezjsonm.string t.state_dir
336+ and lock_file = Ezjsonm.string t.lock_file
337+ and editor = Ezjsonm.string t.editor
338+ and on_modification =
339+ if Option.is_some t.on_modification then
340+ Ezjsonm.string (Option.value_exn t.on_modification)
341+ else Ezjsonm.unit ()
342+ and on_sync =
343+ if Option.is_some t.on_sync then Ezjsonm.string (Option.value_exn t.on_sync)
344+ else Ezjsonm.unit ()
345+ and list_style = Ezjsonm.string (ListStyle.to_string t.list_style)
346+ and encoding = Ezjsonm.string (Encoding.to_string t.encoding)
347+ and column_list =
348+ Ezjsonm.strings (List.map ~f:Column.to_string t.column_list)
349+ in
350+ Yaml.to_string_exn
351+ (Ezjsonm.dict
352+ [
353+ (Key.to_string `StateDir, state_dir);
354+ (Key.to_string `LockFile, lock_file);
355+ (Key.to_string `Editor, editor);
356+ (Key.to_string `OnModification, on_modification);
357+ (Key.to_string `OnSync, on_sync);
358+ (Key.to_string `ListStyle, list_style);
359+ (Key.to_string `Encoding, encoding);
360+ (Key.to_string `ColumnList, column_list);
361+ ])
362
363 let get t key =
364- match Ezjsonm.find_opt t [ Key.to_string key ] with
365- | Some json -> value_of_string key (Ezjsonm.get_string json)
366- | None -> get_default key
367+ match key with
368+ | `StateDir -> t.state_dir
369+ | `LockFile -> t.lock_file
370+ | `Editor -> t.editor
371+ | `OnModification -> (
372+ match t.on_modification with Some value -> value | None -> "null" )
373+ | `OnSync -> ( match t.on_sync with Some value -> value | None -> "null" )
374+ | `ListStyle -> ListStyle.to_string t.list_style
375+ | `Encoding -> Encoding.to_string t.encoding
376+ | `ColumnList ->
377+ String.concat ~sep:" " (List.map ~f:Column.to_string t.column_list)
378
379 let set t key value =
380- Ezjsonm.update t [ Key.to_string key ]
381- (Some (Ezjsonm.string (value_to_string value)))
382-
383- let get_string_opt t key =
384- match get t key with
385- | String value -> value
386- | _ ->
387- failwith
388- (sprintf "BUG: you asked for a string but provided a %s"
389- (Key.to_string key))
390-
391- let get_string t key =
392- match get_string_opt t key with
393- | Some value -> value
394- | None -> failwith (sprintf "%s not defined" (Key.to_string key))
395+ match key with
396+ | `StateDir -> { t with state_dir = value }
397+ | `LockFile -> { t with lock_file = value }
398+ | `Editor -> { t with editor = value }
399+ | `OnModification ->
400+ if String.length value = 0 then { t with on_modification = None }
401+ else { t with on_modification = Some value }
402+ | `OnSync ->
403+ if String.length value = 0 then { t with on_sync = None }
404+ else { t with on_sync = Some value }
405+ | `ListStyle -> { t with list_style = ListStyle.of_string value }
406+ | `Encoding -> { t with encoding = Encoding.of_string value }
407+ | `ColumnList ->
408+ {
409+ t with
410+ column_list = List.map ~f:Column.of_string (String.split ~on:' ' value);
411+ }
412
413 let load =
414 let cfg =
415 match Sys.file_exists config_path with
416- | `Yes -> Yaml.of_string_exn (In_channel.read_all config_path)
417+ | `Yes -> of_string (In_channel.read_all config_path)
418 | `No | `Unknown ->
419 Unix.mkdir_p (Filename.dirname config_path);
420 Out_channel.write_all config_path
421 ~data:(Ezjsonm.to_string (Ezjsonm.dict []));
422- Yaml.of_string_exn (In_channel.read_all config_path)
423+ of_string (In_channel.read_all config_path)
424 in
425
426 (* intiailize the state directory if it is missing *)
427- let state_dir = get_string cfg `StateDir in
428- match Sys.file_exists state_dir with
429+ match Sys.file_exists cfg.state_dir with
430 | `Yes -> cfg
431 | `No | `Unknown ->
432- Unix.mkdir_p state_dir;
433+ Unix.mkdir_p cfg.state_dir;
434 cfg
435
436 let populate t =
437- List.fold ~init:t ~f: (fun accm key ->
438- let value = get accm key in
439- set accm key value
440- ) Key.all
441+ List.fold ~init:t
442+ ~f:(fun accm key ->
443+ let value = get accm key in
444+ set accm key value)
445+ Key.all
446
447 let save t = Out_channel.write_all ~data:(to_string t) config_path