Commit
+177 -112 +/-2 browse
1 | diff --git a/lib/cmd.ml b/lib/cmd.ml |
2 | index 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 |
200 | index 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 |