Skip to content

Commit

Permalink
Allow color in help messages
Browse files Browse the repository at this point in the history
For now all help messages will use the default color scheme copied from
gleam. In a future change it will become possible to configure the
colours.

Signed-off-by: Stephen Sherratt <[email protected]>
  • Loading branch information
gridbugs committed Nov 29, 2024
1 parent bf3d2b0 commit 70d3318
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 100 deletions.
8 changes: 7 additions & 1 deletion examples/fake_git.ml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ let commit =
let+ _amend = flag [ "amend" ] ~desc:"Amend a commit"
and+ _all = flag [ "a" ] ~desc:"Commit all changes"
and+ _branch = named_opt [ "b"; "branch" ] branch_conv
and+ _message = named_opt [ "m"; "message" ] string ~desc:"The commit message"
and+ _message =
named_opt
[ "m"; "message" ]
string
~desc:
"The commit message. This description is extra long to exercise text wrapping in \
help messages."
and+ _files = pos_all file ~desc:"The files to commit" in
()
;;
Expand Down
44 changes: 44 additions & 0 deletions src/climate/ansi_style.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
open Import

module Color = struct
type t =
[ `Red
| `Green
| `Yellow
| `Blue
| `Magenta
| `Cyan
]
end

type t =
{ bold : bool
; underline : bool
; color : Color.t option
}

let default = { bold = false; underline = false; color = None }
let reset = "\x1b[0m"

let escape { bold; underline; color } =
let effects =
List.append (if bold then [ ";1" ] else []) (if underline then [ ";4" ] else [])
in
let color_code =
match (color : Color.t option) with
| None -> 0
| Some `Red -> 31
| Some `Green -> 32
| Some `Yellow -> 33
| Some `Blue -> 34
| Some `Magenta -> 35
| Some `Cyan -> 36
in
Printf.sprintf "\x1b[%d%sm" color_code (String.concat ~sep:"" effects)
;;

let pp_with_style t ppf ~f =
Format.pp_print_string ppf (escape t);
f ppf;
Format.pp_print_string ppf reset
;;
19 changes: 19 additions & 0 deletions src/climate/ansi_style.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Color : sig
type t =
[ `Red
| `Green
| `Yellow
| `Blue
| `Magenta
| `Cyan
]
end

type t =
{ bold : bool
; underline : bool
; color : Color.t option
}

val default : t
val pp_with_style : t -> Format.formatter -> f:(Format.formatter -> unit) -> unit
2 changes: 1 addition & 1 deletion src/climate/climate.ml
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ module Arg_parser = struct
;;

let pp_help ppf arg_spec command_line ~desc ~child_subcommands =
Help.pp ppf (help arg_spec command_line ~desc ~child_subcommands)
Help.pp Help.Style.default ppf (help arg_spec command_line ~desc ~child_subcommands)
;;

let help_spec =
Expand Down
67 changes: 43 additions & 24 deletions src/climate/help.ml
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
open Import

module Style = struct
type t =
{ name : Ansi_style.t
; heading : Ansi_style.t
}

let default =
{ name = { Ansi_style.default with color = Some `Green; bold = true }
; heading = { Ansi_style.default with color = Some `Yellow; bold = true }
}
;;
end

type 'name entry =
{ name : 'name
; desc : string option
Expand Down Expand Up @@ -111,6 +124,7 @@ module Print = struct
;;

let pp_padded
(style : Style.t)
ppf
~at_least_one_left_name
~right_names_left_padding
Expand All @@ -121,7 +135,8 @@ module Print = struct
let names_value_string =
names_value_padded_to_string ~at_least_one_left_name ~right_names_left_padding t
in
Format.pp_print_string ppf names_value_string;
Ansi_style.pp_with_style style.name ppf ~f:(fun ppf ->
Format.pp_print_string ppf names_value_string);
pp_print_spaces ppf 1;
Option.iter t.desc ~f:(fun desc ->
let padding = desc_left_padding - String.length names_value_string in
Expand Down Expand Up @@ -155,23 +170,25 @@ module Print = struct
|> Option.value ~default:0
;;

let pp ppf t =
let pp (style : Style.t) ppf t =
if List.is_empty t.entries
then ()
else (
let at_least_one_left_name =
List.exists t.entries ~f:(fun { Entry.names; _ } ->
not (List.is_empty names.left))
in
pp_print_newlines ppf 2;
Format.pp_print_string ppf t.heading;
pp_print_newlines ppf 1;
Ansi_style.pp_with_style style.heading ppf ~f:(fun ppf ->
Format.pp_print_string ppf t.heading);
pp_print_newlines ppf 1;
let right_names_left_padding = max_left_length t in
let desc_left_padding =
max_name_length ~at_least_one_left_name ~right_names_left_padding t
in
List.iter t.entries ~f:(fun entry ->
Entry.pp_padded
style
ppf
~at_least_one_left_name
~right_names_left_padding
Expand Down Expand Up @@ -220,7 +237,7 @@ module Positional_args = struct
pp_print_elipsis ppf ())
;;

let pp ppf t = Print.Section.pp ppf (to_print_section t)
let pp style ppf t = Print.Section.pp style ppf (to_print_section t)
end

module Named_args = struct
Expand Down Expand Up @@ -249,7 +266,7 @@ module Named_args = struct
}
;;

let pp ppf t = Print.Section.pp ppf (to_print_section t)
let pp style ppf t = Print.Section.pp style ppf (to_print_section t)
end

module Subcommands = struct
Expand All @@ -269,7 +286,7 @@ module Subcommands = struct
}
;;

let pp ppf t = Print.Section.pp ppf (to_print_section t)
let pp style ppf t = Print.Section.pp style ppf (to_print_section t)
end

module Arg_sections = struct
Expand All @@ -290,10 +307,10 @@ module Sections = struct
; subcommands : Subcommands.t
}

let pp ppf t =
Positional_args.pp ppf t.arg_sections.positional_args;
Named_args.pp ppf t.arg_sections.named_args;
Subcommands.pp ppf t.subcommands
let pp style ppf t =
Positional_args.pp style ppf t.arg_sections.positional_args;
Named_args.pp style ppf t.arg_sections.named_args;
Subcommands.pp style ppf t.subcommands
;;
end

Expand All @@ -309,23 +326,25 @@ let pp_command_base ppf t =
List.iter t.subcommand ~f:(Format.fprintf ppf " %s")
;;

let pp_usage ppf t =
Format.pp_print_string ppf "Usage: ";
if not (List.is_empty t.sections.subcommands)
then (
let pp_usage (style : Style.t) ppf t =
Ansi_style.pp_with_style style.heading ppf ~f:(fun ppf ->
Format.pp_print_string ppf "Usage: ");
Ansi_style.pp_with_style style.name ppf ~f:(fun ppf ->
if not (List.is_empty t.sections.subcommands)
then (
pp_command_base ppf t;
Format.pp_print_string ppf " [COMMAND]";
Format.pp_print_newline ppf ();
Format.pp_print_string ppf " ");
pp_command_base ppf t;
Format.pp_print_string ppf " [COMMAND]";
Format.pp_print_newline ppf ();
Format.pp_print_string ppf " ");
pp_command_base ppf t;
Arg_sections.pp_usage_args ppf t.sections.arg_sections
Arg_sections.pp_usage_args ppf t.sections.arg_sections)
;;

let pp ppf t =
let pp style ppf t =
Option.iter t.desc ~f:(fun desc ->
Format.pp_print_string ppf desc;
pp_print_newlines ppf 2);
pp_usage ppf t;
Sections.pp ppf t.sections;
pp_print_newlines ppf 1
pp_usage style ppf t;
pp_print_newlines ppf 1;
Sections.pp style ppf t.sections
;;
11 changes: 10 additions & 1 deletion src/climate/help.mli
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
open Import

module Style : sig
type t =
{ name : Ansi_style.t
; heading : Ansi_style.t
}

val default : t
end

type 'name entry =
{ name : 'name
; desc : string option
Expand Down Expand Up @@ -60,4 +69,4 @@ type t =
; sections : Sections.t
}

val pp : Format.formatter -> t -> unit
val pp : Style.t -> Format.formatter -> t -> unit
Loading

0 comments on commit 70d3318

Please sign in to comment.