Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
144
cli/lib/output.ml
Normal file
144
cli/lib/output.ml
Normal file
@@ -0,0 +1,144 @@
|
||||
type human
|
||||
type json
|
||||
type edn
|
||||
|
||||
module Mode = struct
|
||||
type _ t = Human : human t | Json : json t | Edn : edn t
|
||||
and packed = Packed : _ t -> packed
|
||||
|
||||
let default = Human
|
||||
|
||||
let of_string = function
|
||||
| "human" -> Some (Packed Human)
|
||||
| "json" -> Some (Packed Json)
|
||||
| "edn" -> Some (Packed Edn)
|
||||
| _ -> None
|
||||
|
||||
let to_string : type a. a t -> string = function
|
||||
| Human -> "human"
|
||||
| Json -> "json"
|
||||
| Edn -> "edn"
|
||||
|
||||
let structured : type a. a t -> bool = function
|
||||
| Human -> false
|
||||
| Json | Edn -> true
|
||||
end
|
||||
|
||||
module Human_output = struct
|
||||
type t = {
|
||||
headers : string Rrbvec.t option;
|
||||
rows : string Rrbvec.t Rrbvec.t;
|
||||
footer : string option;
|
||||
}
|
||||
|
||||
let create ?headers ?footer ~rows () = { headers; rows; footer }
|
||||
|
||||
let has_suffix ~suffix value =
|
||||
let suffix_len = String.length suffix in
|
||||
let value_len = String.length value in
|
||||
value_len >= suffix_len
|
||||
&& String.sub value (value_len - suffix_len) suffix_len = suffix
|
||||
|
||||
let is_datetime_header header =
|
||||
let header = String.lowercase_ascii header in
|
||||
has_suffix ~suffix:"created-at" header
|
||||
|| has_suffix ~suffix:"updated-at" header
|
||||
|
||||
let display_cell ~now_time ?header value =
|
||||
let value = if value = "" then "-" else value in
|
||||
match header with
|
||||
| Some header when is_datetime_header header -> (
|
||||
match Int64.of_string_opt value with
|
||||
| Some epoch_ms ->
|
||||
Humanize_types.relative_datetime
|
||||
~then_time:(Time.time_of_epoch_ms epoch_ms)
|
||||
~now_time
|
||||
| None -> value)
|
||||
| _ -> value
|
||||
|
||||
let display_rows t =
|
||||
let now_time = Time.now () in
|
||||
let headers = Option.value t.headers ~default:Vec.empty in
|
||||
Vec.map
|
||||
(fun row ->
|
||||
Vec.mapi
|
||||
(fun index value ->
|
||||
let header =
|
||||
match Vec.nth_opt headers index with
|
||||
| Some header when is_datetime_header header -> Some header
|
||||
| Some header
|
||||
when String.lowercase_ascii header = "value"
|
||||
&& Vec.length row >= 2 ->
|
||||
Vec.nth_opt row 0
|
||||
| header -> header
|
||||
in
|
||||
display_cell ~now_time ?header value)
|
||||
row)
|
||||
t.rows
|
||||
|
||||
let column_widths rows =
|
||||
rows
|
||||
|> Vec.fold_left
|
||||
(fun widths row ->
|
||||
let width = max (Vec.length widths) (Vec.length row) in
|
||||
Vec.init width (fun index ->
|
||||
max
|
||||
(Option.value (Vec.nth_opt widths index) ~default:0)
|
||||
(Display_width.width
|
||||
(Option.value (Vec.nth_opt row index) ~default:""))))
|
||||
Vec.empty
|
||||
|
||||
let pad_right width value =
|
||||
let padding = width - Display_width.width value in
|
||||
if padding <= 0 then value else value ^ String.make padding ' '
|
||||
|
||||
let render_row widths row =
|
||||
let last_index = Vec.length widths - 1 in
|
||||
widths
|
||||
|> Vec.mapi (fun index width ->
|
||||
let value = Option.value (Vec.nth_opt row index) ~default:"" in
|
||||
if index = last_index then value else pad_right width value)
|
||||
|> Vec.string_concat " "
|
||||
|
||||
let strip_leading_colon value =
|
||||
if String.length value > 0 && value.[0] = ':' then
|
||||
String.sub value 1 (String.length value - 1)
|
||||
else value
|
||||
|
||||
let is_keyword_namespace_char = function
|
||||
| 'a' .. 'z' | '0' .. '9' | '.' | '-' | '_' -> true
|
||||
| _ -> false
|
||||
|
||||
let is_keyword_namespace value =
|
||||
value <> "" && String.for_all is_keyword_namespace_char value
|
||||
|
||||
let display_header header =
|
||||
let header = strip_leading_colon header in
|
||||
let parts = Vec.split_on_char '/' header in
|
||||
if Vec.length parts = 2 then
|
||||
let namespace = Vec.nth parts 0 in
|
||||
let name = Vec.nth parts 1 in
|
||||
if is_keyword_namespace namespace && name <> "" then name else header
|
||||
else header
|
||||
|
||||
let lines t =
|
||||
let rows = display_rows t in
|
||||
let table_rows =
|
||||
match t.headers with
|
||||
| None -> rows
|
||||
| Some headers when Vec.is_empty headers -> rows
|
||||
| Some headers -> Vec.push_front rows (Vec.map display_header headers)
|
||||
in
|
||||
let widths = column_widths table_rows in
|
||||
let lines = Vec.map (render_row widths) table_rows in
|
||||
match t.footer with
|
||||
| Some footer -> Vec.push_back lines footer
|
||||
| None -> lines
|
||||
|
||||
let to_string t = Vec.string_concat "\n" (lines t)
|
||||
end
|
||||
|
||||
type _ t =
|
||||
| Human : Human_output.t -> human t
|
||||
| Edn : Melange_edn_melange.any -> edn t
|
||||
| Json : Melange_edn_melange.any -> json t
|
||||
Reference in New Issue
Block a user