Commit
+167 -230 +/-4 browse
1 | diff --git a/bin/note.ml b/bin/note.ml |
2 | index cf884f0..fb08fc7 100644 |
3 | --- a/bin/note.ml |
4 | +++ b/bin/note.ml |
5 | @@ -1,8 +1,6 @@ |
6 | open Core |
7 | open Note_lib |
8 | |
9 | - (* todo global locking *) |
10 | - |
11 | let cfg = Config.config_path |> Config.load |
12 | |
13 | let options : Note.options = |
14 | @@ -12,7 +10,78 @@ let options : Note.options = |
15 | editor = cfg.editor; |
16 | } |
17 | |
18 | + module Display = struct |
19 | + include Note_lib.Display |
20 | + |
21 | + let noop text = text |
22 | + |
23 | + let header columns = |
24 | + columns |
25 | + |> List.map ~f:(fun column -> (column |> Config.Column.to_string, noop)) |
26 | + |
27 | + let to_rows ~columns (notes : Note.t list) : row list = |
28 | + notes |
29 | + |> List.map ~f:(fun note -> |
30 | + columns |
31 | + |> List.map ~f:(fun column -> |
32 | + match column with |
33 | + | `Title -> |
34 | + ( (note |> Note.frontmatter).path |> Filename.basename, |
35 | + noop ) |
36 | + | `Description -> ( |
37 | + match (note |> Note.frontmatter).description with |
38 | + | Some description -> (description, noop) |
39 | + | None -> ("", noop)) |
40 | + | `Tags -> |
41 | + ( (note |> Note.frontmatter).tags |> String.concat ~sep:" ", |
42 | + noop ))) |
43 | + |
44 | + let rec convert_tree tree = |
45 | + let (Note.Tree (note, others)) = tree in |
46 | + let title = |
47 | + "[" ^ ((note |> Note.frontmatter).path |> Filename.basename) ^ "]" |
48 | + in |
49 | + Hierarchical.Tree (title, List.map ~f:convert_tree others) |
50 | + |
51 | + let convert_rows ~columns tree : row list = |
52 | + let (Note.Tree (_, others)) = tree in |
53 | + others |
54 | + |> List.map ~f:(fun other -> |
55 | + let (Note.Tree (note, _)) = other in |
56 | + note) |
57 | + |> to_rows ~columns |
58 | + |
59 | + let to_stdout ~style ~columns notes = |
60 | + match style with |
61 | + | `Tree -> |
62 | + notes |> convert_tree |> Display.Hierarchical.to_string |> print_endline |
63 | + | `Simple -> |
64 | + notes |
65 | + |> convert_rows ~columns:[ `Title ] |
66 | + |> List.iter ~f:(fun row -> print_endline (fst (row |> List.hd_exn))) |
67 | + | `Fixed -> |
68 | + (columns |> header) :: (notes |> convert_rows ~columns) |
69 | + |> Tabular.fixed |
70 | + |> List.iter ~f:(fun row -> print_endline row) |
71 | + | `Wide -> |
72 | + let width, _ = ANSITerminal.size () in |
73 | + (columns |> header) :: (notes |> convert_rows ~columns) |
74 | + |> Tabular.wide ~width |
75 | + |> List.iter ~f:(fun row -> print_endline row) |
76 | + end |
77 | + |
78 | module Args = struct |
79 | + let list_style = |
80 | + let styles = |
81 | + Config.ListStyle.all |> List.map ~f:Config.ListStyle.to_string |
82 | + in |
83 | + Command.Arg_type.create |
84 | + ~complete:(fun _ ~part -> |
85 | + styles |
86 | + |> List.filter ~f:(fun style -> |
87 | + style |> String.is_substring ~substring:part)) |
88 | + (fun key -> key |> Config.ListStyle.of_string) |
89 | + |
90 | let path = |
91 | Command.Arg_type.create |
92 | ~complete:(fun _ ~part -> |
93 | @@ -26,12 +95,11 @@ module Args = struct |
94 | (fun filter -> filter) |
95 | |
96 | let config_key = |
97 | + let keys = List.map ~f:Config.Key.to_string Config.Key.all in |
98 | Command.Arg_type.create |
99 | ~complete:(fun _ ~part -> |
100 | - let string_keys = List.map ~f:Config.Key.to_string Config.Key.all in |
101 | - List.filter |
102 | - ~f:(fun key -> String.is_substring ~substring:part key) |
103 | - string_keys) |
104 | + keys |
105 | + |> List.filter ~f:(fun key -> String.is_substring ~substring:part key)) |
106 | Config.Key.of_string |
107 | end |
108 | |
109 | @@ -143,14 +211,19 @@ List one or more notes that match the filter criteria, if no filter criteria |
110 | is provided then all notes will be listed. |
111 | |}) |
112 | [%map_open |
113 | - let paths = anon (sequence ("path" %: Args.path)) in |
114 | + let paths = anon (sequence ("path" %: Args.path)) |
115 | + and style = |
116 | + flag "style" |
117 | + (optional_with_default cfg.list_style Args.list_style) |
118 | + ~doc:"style" |
119 | + in |
120 | fun () -> |
121 | let paths = match paths with [] -> [ "/" ] | paths -> paths in |
122 | + let columns = cfg.column_list in |
123 | paths |
124 | |> List.map ~f:(fun path -> options |> Note.load ~path) |
125 | |> List.iter ~f:(fun notes -> |
126 | - notes |> Display.convert_tree |> Display.Hierarchical.to_string |
127 | - |> print_endline)] |
128 | + notes |> Display.to_stdout ~style ~columns)] |
129 | |
130 | let sync = |
131 | Command.basic ~summary:"sync notes to a remote server" |
132 | diff --git a/lib/config.ml b/lib/config.ml |
133 | index fa5180d..8bf09ee 100644 |
134 | --- a/lib/config.ml |
135 | +++ b/lib/config.ml |
136 | @@ -132,21 +132,17 @@ module StylePair = struct |
137 | end |
138 | |
139 | module Column = struct |
140 | - type t = [ `Title | `Description | `Tags | `LineCount | `Slug ] |
141 | + type t = [ `Title | `Description | `Tags ] |
142 | |
143 | let to_string = function |
144 | | `Title -> "title" |
145 | | `Description -> "description" |
146 | | `Tags -> "tags" |
147 | - | `LineCount -> "lines" |
148 | - | `Slug -> "slug" |
149 | |
150 | let of_string = function |
151 | | "title" -> `Title |
152 | | "description" -> `Description |
153 | | "tags" -> `Tags |
154 | - | "lines" -> `LineCount |
155 | - | "slug" -> `Slug |
156 | | key -> failwith (sprintf "unsupported column type: %s" key) |
157 | end |
158 | |
159 | @@ -250,7 +246,7 @@ let of_string str = |
160 | match Ezjsonm.find_opt json [ Key.to_string `ColumnList ] with |
161 | | Some column_list -> |
162 | List.map ~f:Column.of_string (Ezjsonm.get_strings column_list) |
163 | - | None -> [ `Title; `Tags; `LineCount; `Slug ] |
164 | + | None -> [ `Title; `Tags; ] |
165 | and styles = |
166 | match Ezjsonm.find_opt json [ Key.to_string `Styles ] with |
167 | | Some values -> StylePair.of_json values |
168 | diff --git a/lib/display.ml b/lib/display.ml |
169 | index 37f468f..276b6f7 100644 |
170 | --- a/lib/display.ml |
171 | +++ b/lib/display.ml |
172 | @@ -1,148 +1,59 @@ |
173 | open Core |
174 | - open ANSITerminal |
175 | |
176 | - type cell = string * int * int |
177 | + type cell = string * (string -> string) |
178 | |
179 | and row = cell list |
180 | |
181 | - type cells = (string * int * int) list list |
182 | - |
183 | module Tabular = struct |
184 | - let paint_tag (styles : Config.StylePair.t list) text : string = |
185 | - match |
186 | - List.find ~f:(fun entry -> String.equal entry.pattern text) styles |
187 | - with |
188 | - | Some entry -> sprintf entry.styles "%s" text |
189 | - | None -> sprintf [ Foreground Default ] "%s" text |
190 | - |
191 | - let to_cells ?(paint = false) ~columns ~styles (notes : Note.t list) = |
192 | - let header = |
193 | - List.map |
194 | - ~f:(fun column -> |
195 | - let text_value = Config.Column.to_string column in |
196 | - let text_length = String.length text_value in |
197 | - let text_value = |
198 | - if paint then sprintf [ Bold; Underlined ] "%s" text_value |
199 | - else text_value |
200 | - in |
201 | - (text_value, text_length, 1)) |
202 | - columns |
203 | - in |
204 | - let note_cells = |
205 | - let default_padding = 1 in |
206 | - List.fold ~init:[] |
207 | - ~f:(fun accm note -> |
208 | - accm |
209 | - @ [ |
210 | - List.map |
211 | - ~f:(fun column -> |
212 | - match column with |
213 | - | `Title -> |
214 | - let text_value = (note |> Note.frontmatter).path in |
215 | - (text_value, String.length text_value, default_padding) |
216 | - | `Description -> |
217 | - let text_value = |
218 | - match (note |> Note.frontmatter).description with |
219 | - | Some text_value -> text_value |
220 | - | None -> "" |
221 | - in |
222 | - (text_value, String.length text_value, default_padding) |
223 | - | `Tags -> |
224 | - let text_value = |
225 | - String.concat ~sep:"|" (note |> Note.frontmatter).tags |
226 | - in |
227 | - let text_length = String.length text_value in |
228 | - let tags = (note |> Note.frontmatter).tags in |
229 | - let tags = |
230 | - if paint then |
231 | - List.map ~f:(fun tag -> paint_tag styles tag) tags |
232 | - else tags |
233 | - in |
234 | - let text_value = String.concat ~sep:"|" tags in |
235 | - (text_value, text_length, default_padding) |
236 | - | `LineCount -> |
237 | - let count = |
238 | - note |> Note.content |> String.split_lines |> List.length |
239 | + let maximum_lengths rows = |
240 | + rows |
241 | + |> List.foldi ~init:[] ~f:(fun index accm row -> |
242 | + let row_maximums = |
243 | + row |> List.map ~f:(fun cell -> fst cell |> String.length) |
244 | + in |
245 | + if Int.equal index 0 then row_maximums |
246 | + else List.map2_exn accm row_maximums ~f:Int.max) |
247 | + |
248 | + let fixed rows = |
249 | + let dimensions = rows |> maximum_lengths in |
250 | + let lines = |
251 | + rows |
252 | + |> List.fold ~init:[] ~f:(fun accm row -> |
253 | + let line = |
254 | + row |
255 | + |> List.foldi ~init:"" ~f:(fun index line cell -> |
256 | + let content, paint_fn = cell in |
257 | + let padding = |
258 | + List.nth_exn dimensions index |
259 | + - String.length content + 1 |
260 | in |
261 | - let text_value = count |> Core.sprintf "%d" in |
262 | - (text_value, String.length text_value, default_padding)) |
263 | - columns; |
264 | - ]) |
265 | - notes |
266 | - in |
267 | - [ header ] @ note_cells |
268 | - |
269 | - let fixed cells = |
270 | - (* find the maximum cell length per column *) |
271 | - let maximum_values = |
272 | - List.fold ~init:[] |
273 | - ~f:(fun accm row -> |
274 | - List.mapi |
275 | - ~f:(fun i col -> |
276 | - let col_length = snd3 col in |
277 | - let current_max = |
278 | - match List.nth accm i with Some len -> len | None -> 0 |
279 | - in |
280 | - if col_length > current_max then col_length + 2 else current_max) |
281 | - row) |
282 | - cells |
283 | - in |
284 | - maximum_values |
285 | + let padding = String.make padding ' ' in |
286 | + let line = line ^ (content |> paint_fn) ^ padding in |
287 | + line) |
288 | + in |
289 | |
290 | - let fixed_right cells = |
291 | - let widths = cells |> fixed in |
292 | - let term_width, _ = size () in |
293 | - let _, right = List.split_n widths 1 in |
294 | - let col_one = List.nth_exn widths 0 in |
295 | - [ col_one + (term_width - List.fold ~init:5 ~f:( + ) widths) ] @ right |
296 | - |
297 | - let apply ~widths cells = |
298 | - (* let maximums = fixed_spacing cells in *) |
299 | - let cells = |
300 | - List.map |
301 | - ~f:(fun row -> |
302 | - List.mapi |
303 | - ~f:(fun i entry -> |
304 | - let max = List.nth_exn widths i in |
305 | - let text, length, padding = entry in |
306 | - let padding = padding + (max - length) in |
307 | - let padding = if padding > 0 then padding else 0 in |
308 | - (text, length, padding)) |
309 | - row) |
310 | - cells |
311 | + line :: accm) |
312 | in |
313 | - List.fold ~init:[] |
314 | - ~f:(fun accm row -> |
315 | - accm |
316 | - @ [ |
317 | - List.fold ~init:"" |
318 | - ~f:(fun accm cell -> |
319 | - let text, _, padding = cell in |
320 | - String.concat [ accm; text; String.make padding ' ' ]) |
321 | - row; |
322 | - ]) |
323 | - cells |
324 | - |
325 | - let simple cells = |
326 | - let lines = |
327 | - List.slice |
328 | - (cells |> List.map ~f:(fun row -> row |> List.hd_exn |> fst3)) |
329 | - 1 0 |
330 | - |> String.concat ~sep:"\n" |
331 | - in |
332 | - "\n" ^ lines ^ "\n" |
333 | - |
334 | - let fixed cells = |
335 | - let lines = |
336 | - apply ~widths:(cells |> fixed) cells |> String.concat ~sep:"\n" |
337 | + lines |> List.rev |
338 | + |
339 | + let wide ?(pivot = 1) ~width rows = |
340 | + let dimensions = rows |> maximum_lengths in |
341 | + let left, right = |
342 | + rows |
343 | + |> List.fold ~init:([], []) ~f:(fun accm row -> |
344 | + let left, right = List.split_n row pivot in |
345 | + (left :: fst accm, right :: snd accm)) |
346 | in |
347 | - "\n" ^ lines ^ "\n" |
348 | + let left = left |> fixed in |
349 | + let right = right |> fixed in |
350 | + let column_length = dimensions |> List.reduce_exn ~f:( + ) in |
351 | + let n_columns = dimensions |> List.length in |
352 | + let total_length = width - (column_length + n_columns) in |
353 | + let padding = String.make total_length ' ' in |
354 | + List.map2_exn left right ~f:(fun left right -> |
355 | + String.concat [ left; padding; right ]) |
356 | + |> List.rev |
357 | |
358 | - let wide cells = |
359 | - let lines = |
360 | - apply ~widths:(cells |> fixed_right) cells |> String.concat ~sep:"\n" |
361 | - in |
362 | - "\n" ^ lines ^ "\n" |
363 | end |
364 | |
365 | module Hierarchical = struct |
366 | @@ -182,18 +93,3 @@ module Hierarchical = struct |
367 | |
368 | let to_string tree = "\n" ^ (tree |> to_lines |> String.concat) |
369 | end |
370 | - |
371 | - let rec convert_tree tree = |
372 | - let (Note.Tree (note, others)) = tree in |
373 | - let title = Filename.basename (note |> Note.frontmatter).path in |
374 | - let title = "[" ^ title ^ "]" in |
375 | - Hierarchical.Tree (title, List.map ~f:convert_tree others) |
376 | - |
377 | - let to_string ?(style = `Tree) ?(columns = []) ?(styles = []) notes = |
378 | - let _ = styles in |
379 | - let _ = columns in |
380 | - match style with |
381 | - | `Tree -> notes |> convert_tree |> Hierarchical.to_string |
382 | - | `Simple -> failwith "not implemented" |
383 | - | `Fixed -> failwith "not implemented" |
384 | - | `Wide -> failwith "not implemented" |
385 | diff --git a/test/display_test.ml b/test/display_test.ml |
386 | index 1153d90..b060710 100644 |
387 | --- a/test/display_test.ml |
388 | +++ b/test/display_test.ml |
389 | @@ -1,75 +1,48 @@ |
390 | open Core |
391 | - open Note_lib |
392 | + open Note_lib.Display |
393 | |
394 | - let notes = |
395 | - [ |
396 | - {| |
397 | - --- |
398 | - path: fuu |
399 | - description: "fuu note" |
400 | - tags: [a,b,c] |
401 | - --- |
402 | - |}; |
403 | - {| |
404 | - --- |
405 | - path: bar |
406 | - description: "bar note with a very long description" |
407 | - tags: [d,e,f] |
408 | - --- |
409 | - |}; |
410 | - {| |
411 | - --- |
412 | - path: baz |
413 | - description: "baz note" |
414 | - tags: [h,i,j] |
415 | - --- |
416 | - |}; |
417 | - {| |
418 | - --- |
419 | - path: qux |
420 | - description: "qux note" |
421 | - tags: [k,l,m] |
422 | - --- |
423 | - |}; |
424 | - ] |
425 | - |> List.map ~f:Note.of_string |
426 | - |
427 | - let test_tabular_display_simple () = |
428 | - let open Display in |
429 | - let expected = {| |
430 | - fuu |
431 | - bar |
432 | - baz |
433 | - qux |
434 | - |} in |
435 | - let result = |
436 | - notes |> Tabular.to_cells ~columns:[ `Title ] ~styles:[] |> Tabular.simple |
437 | - in |
438 | - Alcotest.(check string) "tabular_simple" expected result |
439 | + let noop s = s |
440 | |
441 | let test_tabular_display_fixed () = |
442 | - let open Display in |
443 | - let expected = |
444 | - {| |
445 | - title description tags |
446 | - fuu fuu note a|b|c |
447 | - bar bar note with a very long description d|e|f |
448 | - baz baz note h|i|j |
449 | - qux qux note k|l|m |
450 | - |} |
451 | + let rows = |
452 | + [ |
453 | + [ ("AAA", noop); ("BBBB", noop); ("CCCC", noop) ]; |
454 | + [ ("aaaaaaa", noop); ("b", noop); ("cccccccc", noop) ]; |
455 | + [ ("aa", noop); ("bbb", noop); ("c", noop) ]; |
456 | + ] |
457 | in |
458 | - let result = |
459 | - notes |
460 | - |> Tabular.to_cells ~columns:[ `Title; `Description; `Tags ] ~styles:[] |
461 | - |> Tabular.fixed |
462 | + let expected = {| |
463 | + AAA BBBB CCCC |
464 | + aaaaaaa b cccccccc |
465 | + aa bbb c |} in |
466 | + let result = "\n" ^ (rows |> Tabular.fixed |> String.concat ~sep:"\n") in |
467 | + print_endline "EXPECTED:" ; |
468 | + print_endline (String.Hexdump.to_string_hum expected); |
469 | + print_endline "RESULT:" ; |
470 | + print_endline (String.Hexdump.to_string_hum result); |
471 | + Alcotest.(check string) "tabular_fixed" expected result |
472 | + |
473 | + let test_tabular_display_wide () = |
474 | + let rows = |
475 | + [ |
476 | + [ ("AAA", noop); ("BBBB", noop); ("CCCC", noop) ]; |
477 | + [ ("aaaaaaa", noop); ("b", noop); ("cccccccc", noop) ]; |
478 | + [ ("aa", noop); ("bbb", noop); ("c", noop) ]; |
479 | + ] |
480 | in |
481 | + let expected = {| |
482 | + AAA BBBB CCCC |
483 | + aaaaaaa b cccccccc |
484 | + aa bbb c |} in |
485 | + let result = "\n" ^ (rows |> Tabular.wide ~width:30 |> String.concat ~sep:"\n") in |
486 | + print_endline "EXPECTED:" ; |
487 | print_endline (String.Hexdump.to_string_hum expected); |
488 | + print_endline "RESULT:" ; |
489 | print_endline (String.Hexdump.to_string_hum result); |
490 | - (* TODO: somehow broken string result *) |
491 | - Alcotest.(check pass) "tabular_fixed" expected result |
492 | + Alcotest.(check string) "tabular_fixed" expected result |
493 | |
494 | let test_hierarchical_display () = |
495 | - let open Display.Hierarchical in |
496 | + let open Hierarchical in |
497 | let expected = {| |
498 | A |
499 | ├──B |
500 | @@ -89,11 +62,10 @@ let () = |
501 | ( "tree", |
502 | [ Alcotest.test_case "display a tree" `Quick test_hierarchical_display ] |
503 | ); |
504 | - ( "tabular-simple", |
505 | - [ |
506 | - Alcotest.test_case "tabular fixed" `Quick test_tabular_display_simple; |
507 | - ] ); |
508 | ( "tabular-fixed", |
509 | [ Alcotest.test_case "tabular fixed" `Quick test_tabular_display_fixed ] |
510 | ); |
511 | + ( "tabular-wide", |
512 | + [ Alcotest.test_case "tabular wide" `Quick test_tabular_display_wide ] |
513 | + ); |
514 | ] |