Skip to content

Commit

Permalink
Refactor pretty-printing; implement wcswidth
Browse files Browse the repository at this point in the history
Closes #88

The implementation of wcswidth is taken from the corresponding Python
library (https://github.com/jquast/wcwidth) which seems to have the most
updated list of wide characters. However, note that there are still some
emoji that aren't recognised correctly; see e.g. jquast/wcwidth#57.

At some point in time, the wcswidth implementation should be refactored
into its own library.
  • Loading branch information
yongrenjie committed Apr 13, 2023
1 parent 36255d3 commit 68bfc69
Show file tree
Hide file tree
Showing 7 changed files with 1,249 additions and 84 deletions.
2 changes: 1 addition & 1 deletion bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ let ww_main notify no_color quiet verbose codes_without codes_only project_subse
| Failure msg ->
let open ANSITerminal in
Log.pretty_print ~use_color ~verbose ~restrict_codes ~restrict_issues:None;
Utils.eprcol ~use_color [ Bold; Foreground Red ] "Fatal error: ";
Pretty.prerr ~use_color [ Bold; Foreground Red ] "Fatal error: ";
Printf.eprintf "%s\n" msg;
exit Cmd.Exit.internal_error (* Defined as 125. *)
;;
Expand Down
4 changes: 2 additions & 2 deletions lib/log.ml
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ let pretty_print_event ~use_color (_, e) =
| Debug -> "DEBUG", []
in

Utils.prcol ~use_color [ Bold ] header;
Pretty.prout ~use_color [ Bold ] header;
Printf.printf " ";
Utils.prcol ~use_color error_style error_code;
Pretty.prout ~use_color error_style error_code;
Printf.printf " ";
Printf.printf "%s\n" e.message
;;
Expand Down
116 changes: 116 additions & 0 deletions lib/pretty.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
(** Module for terminal pretty-printing utilities *)

open Utf8
module ANSI = ANSITerminal

(** ----- String manipulation ---------- *)

(** Encase a string in a box *)
let make_box (s : string) : string =
let n = wcswidth s in
let top_and_bottom_row = "+" ^ String.make (n + 2) '-' ^ "+" in
let middle_row = "| " ^ s ^ " |" in
String.concat "\n" [ top_and_bottom_row; middle_row; top_and_bottom_row ]
;;

(** Right-pad a string to a length of [n] with the character [fill_char]. For
example: [pad 'a' 10 'hello' -> 'helloaaaaa]. *)
let pad ?(fill_char = ' ') (n : int) (s : string) : string =
let len = wcswidth s in
if len >= n then s else s ^ String.make (n - len) fill_char
;;

(** Left-pad a string to a length of [n] with the character [fill_char]. For
example: [pad_left 'a' 10 'hello' -> 'aaaaahello']. *)
let pad_left ?(fill_char = ' ') (n : int) (s : string) : string =
let len = wcswidth s in
if len >= n then s else String.make (n - len) fill_char ^ s
;;

(** Pad a string on both sides to a length of [n] with the character
[fill_char]. If the number of padding characters needed is odd, the left
padding is arbitrarily chosen to be shorter. For example: [pad_center 'a' 10
'hello' -> 'aahelloaaa']. *)
let pad_center ?(fill_char = ' ') (n : int) (s : string) : string =
let len = wcswidth s in
if len >= n
then s
else (
let left_pad = (n - len) / 2 in
let right_pad = n - len - left_pad in
String.make left_pad fill_char ^ s ^ String.make right_pad fill_char)
;;

(** Construct a table from a list of lists. Each nested list is one row of the
table. The number of columns of the resulting table will correspond to the
length of the longest nested list. *)
let make_table
?(header_rows : int = 0)
?(column_padding : int = 0)
(rows : string list list)
: string
=
(* First pad all rows to the same length *)
let lengths = List.map List.length rows in
let n_columns = Utils.max_by ~default:0 Fun.id lengths in
let padded_rows =
List.map
(fun row ->
if List.length row < n_columns
then row @ List.init (n_columns - List.length row) (fun _ -> "")
else row)
rows
in

(* Then determine widths of each column *)
let columns = Utils.transpose padded_rows in
let widths = List.map (Utils.max_by ~default:0 wcswidth) columns in
let padded_widths = List.map (fun w -> w + (2 * column_padding)) widths in

(* Finally, construct the table *)
let horizontal_border =
"+" ^ String.concat "+" (List.map (fun w -> String.make w '-') padded_widths) ^ "+"
in
let make_row row =
"|"
^ String.concat
"|"
(List.map2
(fun w s ->
String.make column_padding ' '
^ pad ~fill_char:' ' w s
^ String.make column_padding ' ')
widths
row)
^ "|"
in
let headers, remainder = Utils.split_at header_rows padded_rows in
String.concat
"\n"
([ horizontal_border ]
@ List.map make_row headers
@ [ horizontal_border ]
@ List.map make_row remainder
@ [ horizontal_border ])
;;

(** ----- Printing --------------------- *)

(** Prints a string to standard output. If ~use_color is true, then the string
is printed with the given styles. *)
let prout ~(use_color : bool) (styles : ANSI.style list) (string : string) : unit =
if use_color then ANSI.print_string styles string else print_string string
;;

(** Prints a string to standard error. If ~use_color is true, then the string
is printed with the given styles. *)
let prerr ~(use_color : bool) (styles : ANSI.style list) (string : string) : unit =
if use_color then ANSI.prerr_string styles string else prerr_string string
;;

(** Print a bold, underlined heading *)
let print_heading ~(use_color : bool) (heading : string) : unit =
let n = wcswidth heading in
prout ~use_color [ ANSI.Bold ] (heading ^ "\n");
prout ~use_color [ ANSI.Bold ] (String.make n '-' ^ "\n")
;;
115 changes: 51 additions & 64 deletions lib/project.ml
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
(** Functions to print stuff about projects *)
(** Print an overview of a project. *)

open Domain
open Pretty
open Utf8
module ANSI = ANSITerminal

(** Print a bold, underlined heading *)
let print_heading ~(use_color : bool) heading =
let n = String.length heading in
Utils.prcol ~use_color [ Bold ] (heading ^ "\n");
Utils.prcol ~use_color [ Bold ] (String.make n '-' ^ "\n")
;;

let ftes_of_assignments (prj : project) (asns : assignment list) : (string * FTE.t) list =
asns
|> List.filter (fun a -> a.project.number = prj.number)
Expand All @@ -30,51 +25,44 @@ let print_budget_and_assignments ~use_color (prj : project) (asns : assignment l
| this_asns ->
(* The assignments themselves *)
let entity_names = List.map (fun a -> get_entity_name a.entity) this_asns in
let name_fieldwidth = Utils.max_by ~default:0 String.length entity_names in
let name_fieldwidth = Utils.max_by ~default:0 wcswidth entity_names in
let print_and_return_string asn =
let name = get_entity_name asn.entity in
let is_people_required = Utils.contains name "People Required" in
let string =
Printf.sprintf
"%s %18s, %s to %s\n"
(Utils.pad name_fieldwidth (get_entity_name asn.entity))
(pad name_fieldwidth (get_entity_name asn.entity))
(FTE.show_t (ftes_of_assignment asn))
(CalendarLib.Printer.Date.to_string (get_first_day asn.allocation))
(CalendarLib.Printer.Date.to_string (get_last_day asn.allocation))
in
Utils.prcol ~use_color:(use_color && is_people_required) [ ANSI.red ] string;
prout ~use_color:(use_color && is_people_required) [ ANSI.red ] string;
string
in
let assignment_strings = List.map print_and_return_string this_asns in
(* Horizontal line *)
let max_length = Utils.max_by ~default:0 String.length assignment_strings in
let max_length = Utils.max_by ~default:0 wcswidth assignment_strings in
print_endline (String.make max_length '-');
(* Then the comparison of assignments vs budget *)
Printf.printf
"%s %18s"
(Utils.pad name_fieldwidth "Allocations found")
(pad name_fieldwidth "Allocations found")
(FTE.show_t total_fte_time);
Utils.prcol
prout
~use_color:(use_color && Float.abs discrepancy > 0.1)
[ ANSI.red ]
(Printf.sprintf " (%+.2f%%)" (100. *. discrepancy));
print_endline "";
Printf.printf
"%s %18s\n"
(Utils.pad name_fieldwidth "Allocations expected")
(pad name_fieldwidth "Allocations expected")
(FTE.show_t budget)
;;

let make_box s =
let n = String.length s in
let top_and_bottom_row = "+" ^ String.make (n + 2) '-' ^ "+" in
let middle_row = "| " ^ s ^ " |" in
String.concat "\n" [ top_and_bottom_row; middle_row; top_and_bottom_row ]
;;

let print_title ~(use_color : bool) (prj : project) =
let s = Printf.sprintf "Project %d: %s" prj.number prj.name in
Utils.prcol ~use_color [ Bold ] (make_box s);
prout ~use_color [ ANSI.Bold ] (make_box s);
Printf.printf "\n";
let url =
String.concat
Expand All @@ -86,7 +74,7 @@ let print_title ~(use_color : bool) (prj : project) =
; string_of_int prj.number
]
in
Utils.prcol ~use_color [ Bold ] url
prout ~use_color [ ANSI.Bold ] url
;;

let print_metadata ~(use_color : bool) (prj : project) =
Expand All @@ -104,10 +92,10 @@ let print_metadata ~(use_color : bool) (prj : project) =
in
printf "State : %s\n" (State.show_t prj.state);
(match prj.programme with
| None -> Utils.prcol ~use_color [ ANSI.red ] "Programme : Not found\n"
| None -> prout ~use_color [ ANSI.red ] "Programme : Not found\n"
| Some s -> printf "Programme : %s\n" s);
(match prj.plan.finance_codes with
| [] -> Utils.prcol ~use_color [ ANSI.red ] "Finance codes : Not found\n"
| [] -> prout ~use_color [ ANSI.red ] "Finance codes : Not found\n"
| xs -> printf "Finance codes : %s\n" (String.concat ", " xs));
printf "Earliest start date : %s\n" earliest_start_date_string;
printf
Expand All @@ -119,50 +107,49 @@ let print_metadata ~(use_color : bool) (prj : project) =
printf "Maximum FTE : %.0f%%\n" prj.plan.max_fte_percent
;;

open QueryReports

(* Reactions *)
(*
TODO: collapse people with multiple reactions (example: issue 1216)
This probably involves counting the reactions instead
then refactoring the table according to the counts.
*)
let get_reaction_table (issue : GithubRaw.issue_r) =
(* Get issue reactions then sort by most love -> least love,
then alphabetically *)
let issue_reactions =
issue.reactions
|> List.sort (fun (_, n1) (_, n2) -> compare_names n1 n2)
|> List.sort (fun (e1, _) (e2, _) -> compare_emojis e1 e2)
in

(* Get all emoji reactions and names *)
let all_emoji, all_names = List.split issue_reactions in
let all_emoji = List.map refactor_emoji all_emoji in
let all_names = List.map get_name all_names in

(* Find the longest name for cell size *)
let max_name_length = List.fold_left (fun x y -> max x (String.length y)) 0 all_names in

(* table format emojis*)
let table_format_emojis = List.map get_outcome all_emoji in
let table_body = List.map2 (body_list max_name_length) all_names table_format_emojis in

let bl = border_line max_name_length max_emoji_length in
let hl = header_line max_name_length max_emoji_length in
type emoji =
| LAUGH
| THUMBS_UP
| THUMBS_DOWN
| OTHER

(* Possible emoji responses*)
let parse_emoji e =
match e with
| "laugh" -> LAUGH
| "+1" -> THUMBS_UP
| "-1" -> THUMBS_DOWN
| _ -> OTHER
;;

bl, hl, table_body
let get_name (single_person : GithubRaw.person) =
if single_person.name <> None
then Option.get single_person.name
else single_person.login
;;

let print_reactions ~use_color (prj : Domain.project) =
let issue = GithubRaw.get_issue_r prj.number in
let bl, hl, table_body = get_reaction_table issue in
let sorted_reactions =
issue.reactions
|> List.map (fun (e, n) -> parse_emoji e, get_name n)
|> List.sort (fun (_, n1) (_, n2) -> compare n1 n2)
|> List.sort (fun (e1, _) (e2, _) -> compare e1 e2)
|> List.filter (fun (e, _) -> e <> OTHER)
in
let header = [ "Name"; "😄"; "👍"; "👎" ] in
let rows =
sorted_reactions
|> List.map (fun (e, n) ->
match e with
| LAUGH -> [ n; "x"; ""; "" ]
| THUMBS_UP -> [ n; ""; "x"; "" ]
| THUMBS_DOWN -> [ n; ""; ""; "x" ]
| OTHER -> [ n; ""; ""; "" ]
(* Should not happen *))
in
print_heading ~use_color "Reactions";
print_endline bl;
print_endline hl;
print_endline bl;
List.iter print_endline table_body;
print_endline bl
print_endline (make_table ~header_rows:1 ~column_padding:1 (header :: rows))
;;

let print ~(use_color : bool) (prj : project) (asns : assignment list) =
Expand Down
1 change: 0 additions & 1 deletion lib/queryReports.ml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ let header_line (max_name_length : int) (max_emoji_length : int) =
^ " |"
;;


let get_person_reaction (i : Raw.issue_r) (name : string) =
(* Get only the reactions of the person *)
i.reactions
Expand Down
Loading

0 comments on commit 68bfc69

Please sign in to comment.