Commit
Author: Kevin Schoon [kevinschoon@gmail.com]
Hash: 6b30aaf4ef40aa1618b6977c167c2a906e037399
Timestamp: Fri, 09 Jul 2021 16:46:22 +0000 (3 years ago)

+167 -230 +/-4 browse
reimplement tabular output support
1diff --git a/bin/note.ml b/bin/note.ml
2index 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
133index 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
169index 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
386index 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 ]