From 33fefac24f5f0a7b745b92ea3bc1a3823379822b Mon Sep 17 00:00:00 2001 From: Richard Viney Date: Sat, 8 Jun 2024 10:10:36 +1200 Subject: [PATCH 1/5] Align and wrap description, usage, flags, and commands --- birdie_snapshots/cmd1_help.accepted | 16 ++-- birdie_snapshots/cmd2_help.accepted | 4 +- birdie_snapshots/cmd3_help.accepted | 9 ++- birdie_snapshots/cmd4_help.accepted | 6 +- birdie_snapshots/cmd6_help.accepted | 6 +- birdie_snapshots/root_help.accepted | 15 ++-- src/glint.gleam | 110 +++++++++++++++++++++------- src/glint/internal/utils.gleam | 57 ++++++++++++++ test/glint_test.gleam | 4 +- 9 files changed, 173 insertions(+), 54 deletions(-) create mode 100644 src/glint/internal/utils.gleam diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index 5787211..f601e4a 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -11,14 +11,16 @@ Command: cmd1 This is cmd1 USAGE: - gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= --flag5= --global= ] + gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= --flag5= + --global= ] FLAGS: - --flag2= This is flag2 - --flag5= This is flag5 - --global= This is a global flag - --help Print help information + --flag2= This is flag2 + --flag5= This is flag5 with a really really really really really really + long description + --global= This is a global flag + --help Print help information SUBCOMMANDS: - cmd3 This is cmd3 - cmd4 This is cmd4 \ No newline at end of file + cmd3 This is cmd3 + cmd4 This is cmd4 \ No newline at end of file diff --git a/birdie_snapshots/cmd2_help.accepted b/birdie_snapshots/cmd2_help.accepted index 57216ba..2cc1d5a 100644 --- a/birdie_snapshots/cmd2_help.accepted +++ b/birdie_snapshots/cmd2_help.accepted @@ -14,5 +14,5 @@ USAGE: gleam run -m test cmd2 [ --global= ] FLAGS: - --global= This is a global flag - --help Print help information \ No newline at end of file + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd3_help.accepted b/birdie_snapshots/cmd3_help.accepted index 7695b29..b6e93a9 100644 --- a/birdie_snapshots/cmd3_help.accepted +++ b/birdie_snapshots/cmd3_help.accepted @@ -11,9 +11,10 @@ Command: cmd1 cmd3 This is cmd3 USAGE: - gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= --global= ] + gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= + --global= ] FLAGS: - --flag3= This is flag3 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag3= This is flag3 + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd4_help.accepted b/birdie_snapshots/cmd4_help.accepted index 51f32ba..732d183 100644 --- a/birdie_snapshots/cmd4_help.accepted +++ b/birdie_snapshots/cmd4_help.accepted @@ -14,6 +14,6 @@ USAGE: gleam run -m test cmd1 cmd4 [ --flag4= --global= ] FLAGS: - --flag4= This is flag4 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag4= This is flag4 + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index ce82e80..926df73 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -14,8 +14,8 @@ USAGE: gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] FLAGS: - --global= This is a global flag - --help Print help information + --global= This is a global flag + --help Print help information SUBCOMMANDS: - cmd7 This is cmd7 \ No newline at end of file + cmd7 This is cmd7 \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index 2279070..62c511e 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -9,14 +9,15 @@ Some awesome global help text! This is the root command USAGE: - gleam run -m test ( cmd1 | cmd2 | cmd5 ) [ ARGS ] [ --flag1= --global= ] + gleam run -m test ( cmd1 | cmd2 | cmd5 ) [ ARGS ] [ --flag1= + --global= ] FLAGS: - --flag1= This is flag1 - --global= This is a global flag - --help Print help information + --flag1= This is flag1 + --global= This is a global flag + --help Print help information SUBCOMMANDS: - cmd1 This is cmd1 - cmd2 This is cmd2 - cmd5 \ No newline at end of file + cmd1 This is cmd1 + cmd2 This is cmd2 + cmd5 \ No newline at end of file diff --git a/src/glint.gleam b/src/glint.gleam index 00659d1..e270862 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -12,6 +12,7 @@ import gleam/string_builder as sb import gleam_community/ansi import gleam_community/colour.{type Colour} import glint/constraint +import glint/internal/utils import snag.{type Snag} // --- CONFIGURATION --- @@ -393,7 +394,7 @@ pub fn group_flag( @internal pub fn execute(glint: Glint(a), args: List(String)) -> Result(Out(a), String) { // create help flag to check for - let help_flag = help_flag() + let help_flag = prefix <> help_flag.meta.name // check if help flag is present let #(help, args) = case list.pop(args, fn(s) { s == help_flag }) { @@ -607,16 +608,7 @@ fn is_not_empty(s: String) -> Bool { s != "" } -const help_flag_name = "help" - -const help_flag_message = "--help\t\t\tPrint help information" - -/// Function to create the help flag string -/// Exported for testing purposes only -/// -fn help_flag() -> String { - prefix <> help_flag_name -} +const help_flag = FlagHelp(Metadata("help", "Print help information"), "") // -- HELP: FUNCTIONS -- @@ -680,6 +672,20 @@ type CommandHelp { ) } +/// The maximum width for console output, (e.g. command, flag, and subcommand +/// descriptions). +/// +const max_output_width = 100 + +/// THe minimum width of the column containing flag/command names and the +/// descripion. +/// +const min_column_width = 24 + +/// The width in spaces of a \t tab character. +/// +const tab_width = 8 + // -- HELP - FUNCTIONS - BUILDERS -- fn build_app_help(config: Config, command_name: String, node: CommandNode(_)) { AppHelp(config: config, command: build_command_help(command_name, node)) @@ -750,7 +756,10 @@ fn app_help_to_string(help: AppHelp) -> String { |> option.unwrap(""), help.command.meta.name |> string_map(string.append("Command: ", _)), - help.command.meta.description, + string.join( + utils.wordwrap(help.command.meta.description, max_output_width), + "\n", + ), command_help_to_usage_string(help.command, help.config), flags_help_to_string(help.command.flags, help.config), subcommands_help_to_string(help.command.subcommands, help.config), @@ -825,12 +834,16 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { Some(pretty) -> heading_style(usage_heading, pretty.usage) } <> "\n\t" - <> app_name - <> string_map(help.meta.name, string.append(" ", _)) - <> string_map(subcommands, string.append(" ", _)) - <> string_map(named_args, string.append(" ", _)) - <> string_map(unnamed_args, string.append(" ", _)) - <> string_map(flags, string.append(" ", _)) + <> utils.wordwrap( + app_name + <> string_map(help.meta.name, string.append(" ", _)) + <> string_map(subcommands, string.append(" ", _)) + <> string_map(named_args, string.append(" ", _)) + <> string_map(unnamed_args, string.append(" ", _)) + <> string_map(flags, string.append(" ", _)), + max_output_width - tab_width - 2, + ) + |> string.join("\n\t ") } // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- @@ -840,12 +853,19 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { fn flags_help_to_string(help: List(FlagHelp), config: Config) -> String { use <- bool.guard(help == [], "") + let longest_flag_length = + help + |> list.map(flag_help_to_string) + |> utils.max_string_length + |> int.max(min_column_width) + case config.pretty_help { None -> flags_heading Some(pretty) -> heading_style(flags_heading, pretty.flags) } <> { - [help_flag_message, ..list.map(help, flag_help_to_string_with_description)] + [help_flag, ..help] + |> list.map(flag_help_to_string_with_description(_, longest_flag_length + 2)) |> list.sort(string.compare) |> list.map(string.append("\n\t", _)) |> string.concat @@ -855,13 +875,34 @@ fn flags_help_to_string(help: List(FlagHelp), config: Config) -> String { /// generate the help text for a flag without a description /// fn flag_help_to_string(help: FlagHelp) -> String { - prefix <> help.meta.name <> "=<" <> help.type_ <> ">" + prefix + <> help.meta.name + <> case help.type_ { + "" -> "" + _ -> "=<" <> help.type_ <> ">" + } } /// generate the help text for a flag with a description /// -fn flag_help_to_string_with_description(help: FlagHelp) -> String { - flag_help_to_string(help) <> "\t\t" <> help.meta.description +fn flag_help_to_string_with_description( + help: FlagHelp, + longest_flag_length: Int, +) -> String { + { + help + |> flag_help_to_string + |> string.pad_right(longest_flag_length, " ") + } + <> { + let description_width = + { max_output_width - longest_flag_length - tab_width } + |> int.max(min_column_width) + + help.meta.description + |> utils.wordwrap(description_width) + |> string.join("\n\t" <> string.repeat(" ", longest_flag_length)) + } } // -- HELP - FUNCTIONS - STRINGIFIERS - SUBCOMMANDS -- @@ -871,13 +912,19 @@ fn flag_help_to_string_with_description(help: FlagHelp) -> String { fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { use <- bool.guard(help == [], "") + let longest_subcommand_length = + help + |> list.map(fn(h) { h.name }) + |> utils.max_string_length + |> int.max(min_column_width) + case config.pretty_help { None -> subcommands_heading Some(pretty) -> heading_style(subcommands_heading, pretty.subcommands) } <> { help - |> list.map(subcommand_help_to_string) + |> list.map(subcommand_help_to_string(_, longest_subcommand_length + 2)) |> list.sort(string.compare) |> list.map(string.append("\n\t", _)) |> string.concat @@ -886,10 +933,19 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { /// generate the help text for a single subcommand given its name and description /// -fn subcommand_help_to_string(help: Metadata) -> String { - case help.description { - "" -> help.name - _ -> help.name <> "\t\t" <> help.description +fn subcommand_help_to_string( + help: Metadata, + longest_subcommand_length: Int, +) -> String { + string.pad_right(help.name, longest_subcommand_length, " ") + <> { + let description_width = + { max_output_width - longest_subcommand_length - tab_width } + |> int.max(min_column_width) + + help.description + |> utils.wordwrap(description_width) + |> string.join("\n\t" <> string.repeat(" ", longest_subcommand_length)) } } diff --git a/src/glint/internal/utils.gleam b/src/glint/internal/utils.gleam new file mode 100644 index 0000000..5b334d1 --- /dev/null +++ b/src/glint/internal/utils.gleam @@ -0,0 +1,57 @@ +import gleam/int +import gleam/list +import gleam/string + +/// Returns the length of the longest string in the list. +/// +pub fn max_string_length(strings: List(String)) -> Int { + strings |> list.fold(0, fn(max, f) { f |> string.length |> int.max(max) }) +} + +/// Wraps the given string so that no lines exceed the given width. Newlines in +/// the input string are retained. +/// +pub fn wordwrap(s: String, max_width: Int) -> List(String) { + s + |> string.split("\n") + |> list.map(fn(s) { + s + |> string.split(" ") + |> do_wordwrap(max_width, "", []) + }) + |> list.flatten +} + +fn do_wordwrap( + tokens: List(String), + max_width: Int, + current_line: String, + lines: List(String), +) -> List(String) { + case tokens { + [] -> + case current_line { + "" -> lines + _ -> [current_line, ..lines] + } + |> list.reverse + + [token, ..new_tokens] -> { + let line_length = string.length(current_line) + let token_length = string.length(token) + + case line_length { + 0 -> do_wordwrap(new_tokens, max_width, token, lines) + _ -> + case line_length + 1 + token_length <= max_width { + True -> { + let current_line = current_line <> " " <> token + do_wordwrap(new_tokens, max_width, current_line, lines) + } + + False -> do_wordwrap(tokens, max_width, "", [current_line, ..lines]) + } + } + } + } +} diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 2be61f6..6d99596 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -124,7 +124,9 @@ pub fn help_test() { let flag_5 = "flag5" |> glint.floats_flag() - |> glint.flag_help("This is flag5") + |> glint.flag_help( + "This is flag5 with a really really really really really really long description", + ) let cli = glint.new() From 7eea04652d6212b98829d52f9017fae1554372b3 Mon Sep 17 00:00:00 2001 From: Richard Viney Date: Mon, 10 Jun 2024 17:26:09 +1200 Subject: [PATCH 2/5] Make wrapping configurable. Refactoring. --- birdie_snapshots/cmd1_help.accepted | 20 +++--- birdie_snapshots/cmd2_help.accepted | 4 +- birdie_snapshots/cmd3_help.accepted | 10 +-- birdie_snapshots/cmd4_help.accepted | 9 +-- birdie_snapshots/cmd6_help.accepted | 6 +- birdie_snapshots/root_help.accepted | 18 +++--- src/glint.gleam | 98 ++++++++++++++++++++--------- src/glint/internal/utils.gleam | 60 +++++++++--------- test/glint_test.gleam | 12 +++- 9 files changed, 144 insertions(+), 93 deletions(-) diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index f601e4a..628273b 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -11,16 +11,18 @@ Command: cmd1 This is cmd1 USAGE: - gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= --flag5= - --global= ] + gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= + --global= --very-very-very-long-flag= ] FLAGS: - --flag2= This is flag2 - --flag5= This is flag5 with a really really really really really really - long description - --global= This is a global flag - --help Print help information + --flag2= This is flag2 + --global= This is a global flag + --help Print help information + --very-very-very-long-flag= This is a very long flag with a + very very very very very very + long description SUBCOMMANDS: - cmd3 This is cmd3 - cmd4 This is cmd4 \ No newline at end of file + cmd3 This is cmd3 + cmd4 This is cmd4 which has a very very very very very + very very very long description \ No newline at end of file diff --git a/birdie_snapshots/cmd2_help.accepted b/birdie_snapshots/cmd2_help.accepted index 2cc1d5a..7b18ce8 100644 --- a/birdie_snapshots/cmd2_help.accepted +++ b/birdie_snapshots/cmd2_help.accepted @@ -14,5 +14,5 @@ USAGE: gleam run -m test cmd2 [ --global= ] FLAGS: - --global= This is a global flag - --help Print help information \ No newline at end of file + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd3_help.accepted b/birdie_snapshots/cmd3_help.accepted index b6e93a9..7e5b59d 100644 --- a/birdie_snapshots/cmd3_help.accepted +++ b/birdie_snapshots/cmd3_help.accepted @@ -11,10 +11,10 @@ Command: cmd1 cmd3 This is cmd3 USAGE: - gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= - --global= ] + gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ + --flag3= --global= ] FLAGS: - --flag3= This is flag3 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag3= This is flag3 + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd4_help.accepted b/birdie_snapshots/cmd4_help.accepted index 732d183..2bd514e 100644 --- a/birdie_snapshots/cmd4_help.accepted +++ b/birdie_snapshots/cmd4_help.accepted @@ -8,12 +8,13 @@ Some awesome global help text! Command: cmd1 cmd4 -This is cmd4 +This is cmd4 which has a very very very very very very very very long +description USAGE: gleam run -m test cmd1 cmd4 [ --flag4= --global= ] FLAGS: - --flag4= This is flag4 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag4= This is flag4 + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index 926df73..e2563a1 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -14,8 +14,8 @@ USAGE: gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] FLAGS: - --global= This is a global flag - --help Print help information + --global= This is a global flag + --help Print help information SUBCOMMANDS: - cmd7 This is cmd7 \ No newline at end of file + cmd7 This is cmd7 \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index 62c511e..9f23077 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -9,15 +9,17 @@ Some awesome global help text! This is the root command USAGE: - gleam run -m test ( cmd1 | cmd2 | cmd5 ) [ ARGS ] [ --flag1= - --global= ] + gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long + ) [ ARGS ] [ --flag1= --global= ] FLAGS: - --flag1= This is flag1 - --global= This is a global flag - --help Print help information + --flag1= This is flag1 + --global= This is a global flag + --help Print help information SUBCOMMANDS: - cmd1 This is cmd1 - cmd2 This is cmd2 - cmd5 \ No newline at end of file + cmd1 This is cmd1 + cmd2 This is cmd2 + cmd5 + cmd8-very-very-very-very-long This is cmd8 with a very very very very + very very very long description \ No newline at end of file diff --git a/src/glint.gleam b/src/glint.gleam index e270862..d20a21e 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -28,6 +28,9 @@ type Config { as_module: Bool, description: Option(String), exit: Bool, + max_output_width: Int, + min_first_column_width: Int, + column_gap: Int, ) } @@ -47,6 +50,9 @@ const default_config = Config( as_module: False, description: None, exit: True, + max_output_width: 80, + min_first_column_width: 20, + column_gap: 2, ) // -- CONFIGURATION: FUNCTIONS -- @@ -89,6 +95,30 @@ pub fn as_module(glint: Glint(a)) -> Glint(a) { config(glint, Config(..glint.config, as_module: True)) } +/// Adjusts the output width at which help text will wrap onto a new line. +/// +/// Default: 80. +/// +pub fn with_max_output_width(glint: Glint(a), width: Int) -> Glint(a) { + Glint(..glint, config: Config(..glint.config, max_output_width: width)) +} + +/// Adjusts the minimum width of the column containing flag and command names. +/// +/// Default: 20. +/// +pub fn with_min_first_column_width(glint: Glint(a), width: Int) -> Glint(a) { + Glint(..glint, config: Config(..glint.config, min_first_column_width: width)) +} + +/// Adjusts the size of the gap between columns in the help output. +/// +/// Default: 2. +/// +pub fn with_column_gap(glint: Glint(a), gap: Int) -> Glint(a) { + Glint(..glint, config: Config(..glint.config, column_gap: gap)) +} + // --- CORE --- // -- CORE: TYPES -- @@ -672,16 +702,6 @@ type CommandHelp { ) } -/// The maximum width for console output, (e.g. command, flag, and subcommand -/// descriptions). -/// -const max_output_width = 100 - -/// THe minimum width of the column containing flag/command names and the -/// descripion. -/// -const min_column_width = 24 - /// The width in spaces of a \t tab character. /// const tab_width = 8 @@ -757,7 +777,10 @@ fn app_help_to_string(help: AppHelp) -> String { help.command.meta.name |> string_map(string.append("Command: ", _)), string.join( - utils.wordwrap(help.command.meta.description, max_output_width), + utils.wordwrap( + help.command.meta.description, + help.config.max_output_width, + ), "\n", ), command_help_to_usage_string(help.command, help.config), @@ -829,6 +852,10 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { option.map(help.unnamed_args, args_count_to_usage_string) |> option.unwrap("[ ARGS ]") + // The max width of the usage accounts for the one tab indent plus two spaces + // of indentation on wrapped lines + let max_usage_width = config.max_output_width - tab_width - 2 + case config.pretty_help { None -> usage_heading Some(pretty) -> heading_style(usage_heading, pretty.usage) @@ -841,7 +868,7 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { <> string_map(named_args, string.append(" ", _)) <> string_map(unnamed_args, string.append(" ", _)) <> string_map(flags, string.append(" ", _)), - max_output_width - tab_width - 2, + max_usage_width, ) |> string.join("\n\t ") } @@ -857,7 +884,7 @@ fn flags_help_to_string(help: List(FlagHelp), config: Config) -> String { help |> list.map(flag_help_to_string) |> utils.max_string_length - |> int.max(min_column_width) + |> int.max(config.min_first_column_width) case config.pretty_help { None -> flags_heading @@ -865,7 +892,11 @@ fn flags_help_to_string(help: List(FlagHelp), config: Config) -> String { } <> { [help_flag, ..help] - |> list.map(flag_help_to_string_with_description(_, longest_flag_length + 2)) + |> list.map(flag_help_to_string_with_description( + _, + longest_flag_length + config.column_gap, + config, + )) |> list.sort(string.compare) |> list.map(string.append("\n\t", _)) |> string.concat @@ -888,21 +919,23 @@ fn flag_help_to_string(help: FlagHelp) -> String { fn flag_help_to_string_with_description( help: FlagHelp, longest_flag_length: Int, + config: Config, ) -> String { - { + let name = help |> flag_help_to_string |> string.pad_right(longest_flag_length, " ") - } - <> { - let description_width = - { max_output_width - longest_flag_length - tab_width } - |> int.max(min_column_width) + let description_width = + { config.max_output_width - longest_flag_length - tab_width } + |> int.max(config.min_first_column_width) + + let description = help.meta.description |> utils.wordwrap(description_width) |> string.join("\n\t" <> string.repeat(" ", longest_flag_length)) - } + + name <> description } // -- HELP - FUNCTIONS - STRINGIFIERS - SUBCOMMANDS -- @@ -916,7 +949,7 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { help |> list.map(fn(h) { h.name }) |> utils.max_string_length - |> int.max(min_column_width) + |> int.max(config.min_first_column_width) case config.pretty_help { None -> subcommands_heading @@ -924,7 +957,11 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { } <> { help - |> list.map(subcommand_help_to_string(_, longest_subcommand_length + 2)) + |> list.map(subcommand_help_to_string( + _, + longest_subcommand_length + config.column_gap, + config, + )) |> list.sort(string.compare) |> list.map(string.append("\n\t", _)) |> string.concat @@ -936,17 +973,20 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { fn subcommand_help_to_string( help: Metadata, longest_subcommand_length: Int, + config: Config, ) -> String { - string.pad_right(help.name, longest_subcommand_length, " ") - <> { - let description_width = - { max_output_width - longest_subcommand_length - tab_width } - |> int.max(min_column_width) + let name = string.pad_right(help.name, longest_subcommand_length, " ") + let description_width = + { config.max_output_width - longest_subcommand_length - tab_width } + |> int.max(config.min_first_column_width) + + let description = help.description |> utils.wordwrap(description_width) |> string.join("\n\t" <> string.repeat(" ", longest_subcommand_length)) - } + + name <> description } fn string_map(s: String, f: fn(String) -> String) -> String { diff --git a/src/glint/internal/utils.gleam b/src/glint/internal/utils.gleam index 5b334d1..f4a5aaf 100644 --- a/src/glint/internal/utils.gleam +++ b/src/glint/internal/utils.gleam @@ -5,53 +5,53 @@ import gleam/string /// Returns the length of the longest string in the list. /// pub fn max_string_length(strings: List(String)) -> Int { - strings |> list.fold(0, fn(max, f) { f |> string.length |> int.max(max) }) + use max, f <- list.fold(strings, 0) + + f + |> string.length + |> int.max(max) } /// Wraps the given string so that no lines exceed the given width. Newlines in /// the input string are retained. /// pub fn wordwrap(s: String, max_width: Int) -> List(String) { - s - |> string.split("\n") - |> list.map(fn(s) { - s - |> string.split(" ") - |> do_wordwrap(max_width, "", []) - }) - |> list.flatten + use line <- list.flat_map(string.split(s, "\n")) + + line + |> string.split(" ") + |> do_wordwrap(max_width, "", []) } fn do_wordwrap( tokens: List(String), max_width: Int, - current_line: String, + line: String, lines: List(String), ) -> List(String) { case tokens { - [] -> - case current_line { - "" -> lines - _ -> [current_line, ..lines] - } - |> list.reverse - - [token, ..new_tokens] -> { - let line_length = string.length(current_line) + // Handle the next token + [token, ..tokens] -> { let token_length = string.length(token) + let line_length = string.length(line) - case line_length { - 0 -> do_wordwrap(new_tokens, max_width, token, lines) - _ -> - case line_length + 1 + token_length <= max_width { - True -> { - let current_line = current_line <> " " <> token - do_wordwrap(new_tokens, max_width, current_line, lines) - } - - False -> do_wordwrap(tokens, max_width, "", [current_line, ..lines]) - } + case line, line_length + 1 + token_length <= max_width { + // When the current line is empty the next token always goes on it + // regardless of its length + "", _ -> do_wordwrap(tokens, max_width, token, lines) + + // Add the next token to the current line if it fits + _, True -> do_wordwrap(tokens, max_width, line <> " " <> token, lines) + + // Start a new line with the next token as it exceeds the max width if + // added to the current line + _, False -> do_wordwrap(tokens, max_width, token, [line, ..lines]) } } + + // There are no more more tokens, so add the current line to the result if + // it's not empty + [] if line == "" -> list.reverse(lines) + [] -> list.reverse([line, ..lines]) } } diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 6d99596..52b4640 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -122,10 +122,10 @@ pub fn help_test() { |> glint.flag_help("This is flag4") let flag_5 = - "flag5" + "very-very-very-long-flag" |> glint.floats_flag() |> glint.flag_help( - "This is flag5 with a really really really really really really long description", + "This is a very long flag with a very very very very very very long description", ) let cli = @@ -155,7 +155,9 @@ pub fn help_test() { glint.command(nil) }) |> glint.add(at: ["cmd1", "cmd4"], do: { - use <- glint.command_help("This is cmd4") + use <- glint.command_help( + "This is cmd4 which has a very very very very very very very very long description", + ) use _flag4 <- glint.flag(flag_4) use <- glint.unnamed_args(glint.EqArgs(0)) glint.command(nil) @@ -172,6 +174,10 @@ pub fn help_test() { do: glint.command_help("This is cmd6", fn() { glint.command(nil) }), ) |> glint.path_help(["cmd5", "cmd6", "cmd7"], "This is cmd7") + |> glint.path_help( + ["cmd8-very-very-very-very-long"], + "This is cmd8 with a very very very very very very very long description", + ) // execute root command glint.execute(cli, ["a", "b"]) From 23527f81ae5ae89134a2e428436bed07501208d9 Mon Sep 17 00:00:00 2001 From: Richard Viney Date: Tue, 11 Jun 2024 07:36:09 +1200 Subject: [PATCH 3/5] Fix typo --- src/glint/internal/utils.gleam | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/glint/internal/utils.gleam b/src/glint/internal/utils.gleam index f4a5aaf..8e40c7b 100644 --- a/src/glint/internal/utils.gleam +++ b/src/glint/internal/utils.gleam @@ -49,8 +49,8 @@ fn do_wordwrap( } } - // There are no more more tokens, so add the current line to the result if - // it's not empty + // There are no more tokens so return the final result, adding the current + // line to it if it's not empty [] if line == "" -> list.reverse(lines) [] -> list.reverse([line, ..lines]) } From 2b3319aedec0ab955c8ef1dfeb37ca63ebe9bc9c Mon Sep 17 00:00:00 2001 From: Richard Viney Date: Tue, 11 Jun 2024 10:18:05 +1200 Subject: [PATCH 4/5] Replace \t with spaces and make indent width configurable (default 4 spaces) --- birdie_snapshots/cmd1_help.accepted | 22 ++++++------- birdie_snapshots/cmd2_help.accepted | 6 ++-- birdie_snapshots/cmd3_help.accepted | 10 +++--- birdie_snapshots/cmd4_help.accepted | 8 ++--- birdie_snapshots/cmd6_help.accepted | 8 ++--- birdie_snapshots/cmd7_help.accepted | 2 +- birdie_snapshots/root_help.accepted | 20 ++++++------ src/glint.gleam | 50 ++++++++++++++++++++--------- test/examples/hello.gleam | 18 +++++------ test/glint_test.gleam | 5 +-- 10 files changed, 83 insertions(+), 66 deletions(-) diff --git a/birdie_snapshots/cmd1_help.accepted b/birdie_snapshots/cmd1_help.accepted index 628273b..8e1247e 100644 --- a/birdie_snapshots/cmd1_help.accepted +++ b/birdie_snapshots/cmd1_help.accepted @@ -11,18 +11,18 @@ Command: cmd1 This is cmd1 USAGE: - gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= - --global= --very-very-very-long-flag= ] + gleam run -m test cmd1 ( cmd3 | cmd4 ) [ ARGS ] [ --flag2= + --global= --very-very-very-long-flag= ] FLAGS: - --flag2= This is flag2 - --global= This is a global flag - --help Print help information - --very-very-very-long-flag= This is a very long flag with a - very very very very very very - long description + --flag2= This is flag2 + --global= This is a global flag + --help Print help information + --very-very-very-long-flag= This is a very long flag with a + very very very very very very long + description SUBCOMMANDS: - cmd3 This is cmd3 - cmd4 This is cmd4 which has a very very very very very - very very very long description \ No newline at end of file + cmd3 This is cmd3 + cmd4 This is cmd4 which has a very very very very very very + very very long description \ No newline at end of file diff --git a/birdie_snapshots/cmd2_help.accepted b/birdie_snapshots/cmd2_help.accepted index 7b18ce8..168c626 100644 --- a/birdie_snapshots/cmd2_help.accepted +++ b/birdie_snapshots/cmd2_help.accepted @@ -11,8 +11,8 @@ Command: cmd2 This is cmd2 USAGE: - gleam run -m test cmd2 [ --global= ] + gleam run -m test cmd2 [ --global= ] FLAGS: - --global= This is a global flag - --help Print help information \ No newline at end of file + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd3_help.accepted b/birdie_snapshots/cmd3_help.accepted index 7e5b59d..3be6824 100644 --- a/birdie_snapshots/cmd3_help.accepted +++ b/birdie_snapshots/cmd3_help.accepted @@ -11,10 +11,10 @@ Command: cmd1 cmd3 This is cmd3 USAGE: - gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ - --flag3= --global= ] + gleam run -m test cmd1 cmd3 [ 2 or more arguments ] [ --flag3= + --global= ] FLAGS: - --flag3= This is flag3 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag3= This is flag3 + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd4_help.accepted b/birdie_snapshots/cmd4_help.accepted index 2bd514e..d248e60 100644 --- a/birdie_snapshots/cmd4_help.accepted +++ b/birdie_snapshots/cmd4_help.accepted @@ -12,9 +12,9 @@ This is cmd4 which has a very very very very very very very very long description USAGE: - gleam run -m test cmd1 cmd4 [ --flag4= --global= ] + gleam run -m test cmd1 cmd4 [ --flag4= --global= ] FLAGS: - --flag4= This is flag4 - --global= This is a global flag - --help Print help information \ No newline at end of file + --flag4= This is flag4 + --global= This is a global flag + --help Print help information \ No newline at end of file diff --git a/birdie_snapshots/cmd6_help.accepted b/birdie_snapshots/cmd6_help.accepted index e2563a1..3bebc52 100644 --- a/birdie_snapshots/cmd6_help.accepted +++ b/birdie_snapshots/cmd6_help.accepted @@ -11,11 +11,11 @@ Command: cmd5 cmd6 This is cmd6 USAGE: - gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] + gleam run -m test cmd5 cmd6 ( cmd7 ) [ ARGS ] [ --global= ] FLAGS: - --global= This is a global flag - --help Print help information + --global= This is a global flag + --help Print help information SUBCOMMANDS: - cmd7 This is cmd7 \ No newline at end of file + cmd7 This is cmd7 \ No newline at end of file diff --git a/birdie_snapshots/cmd7_help.accepted b/birdie_snapshots/cmd7_help.accepted index 4f913ce..33d5103 100644 --- a/birdie_snapshots/cmd7_help.accepted +++ b/birdie_snapshots/cmd7_help.accepted @@ -11,4 +11,4 @@ Command: cmd5 cmd6 cmd7 This is cmd7 USAGE: - gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] \ No newline at end of file + gleam run -m test cmd5 cmd6 cmd7 [ ARGS ] \ No newline at end of file diff --git a/birdie_snapshots/root_help.accepted b/birdie_snapshots/root_help.accepted index 9f23077..9706479 100644 --- a/birdie_snapshots/root_help.accepted +++ b/birdie_snapshots/root_help.accepted @@ -9,17 +9,17 @@ Some awesome global help text! This is the root command USAGE: - gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long - ) [ ARGS ] [ --flag1= --global= ] + gleam run -m test ( cmd1 | cmd2 | cmd5 | cmd8-very-very-very-very-long ) + [ ARGS ] [ --flag1= --global= ] FLAGS: - --flag1= This is flag1 - --global= This is a global flag - --help Print help information + --flag1= This is flag1 + --global= This is a global flag + --help Print help information SUBCOMMANDS: - cmd1 This is cmd1 - cmd2 This is cmd2 - cmd5 - cmd8-very-very-very-very-long This is cmd8 with a very very very very - very very very long description \ No newline at end of file + cmd1 This is cmd1 + cmd2 This is cmd2 + cmd5 + cmd8-very-very-very-very-long This is cmd8 with a very very very very very + very very long description \ No newline at end of file diff --git a/src/glint.gleam b/src/glint.gleam index d20a21e..732b821 100644 --- a/src/glint.gleam +++ b/src/glint.gleam @@ -28,6 +28,7 @@ type Config { as_module: Bool, description: Option(String), exit: Bool, + indent_width: Int, max_output_width: Int, min_first_column_width: Int, column_gap: Int, @@ -50,6 +51,7 @@ const default_config = Config( as_module: False, description: None, exit: True, + indent_width: 4, max_output_width: 80, min_first_column_width: 20, column_gap: 2, @@ -95,6 +97,15 @@ pub fn as_module(glint: Glint(a)) -> Glint(a) { config(glint, Config(..glint.config, as_module: True)) } +/// Adjusts the indent width used to indent content under the usage, flags, +/// and subcommands headings. +/// +/// Default: 4. +/// +pub fn with_indent_width(glint: Glint(a), width: Int) -> Glint(a) { + Glint(..glint, config: Config(..glint.config, indent_width: width)) +} + /// Adjusts the output width at which help text will wrap onto a new line. /// /// Default: 80. @@ -702,10 +713,6 @@ type CommandHelp { ) } -/// The width in spaces of a \t tab character. -/// -const tab_width = 8 - // -- HELP - FUNCTIONS - BUILDERS -- fn build_app_help(config: Config, command_name: String, node: CommandNode(_)) { AppHelp(config: config, command: build_command_help(command_name, node)) @@ -852,15 +859,15 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { option.map(help.unnamed_args, args_count_to_usage_string) |> option.unwrap("[ ARGS ]") - // The max width of the usage accounts for the one tab indent plus two spaces - // of indentation on wrapped lines - let max_usage_width = config.max_output_width - tab_width - 2 + // The max width of the usage accounts for the constant indent + let max_usage_width = config.max_output_width - config.indent_width case config.pretty_help { None -> usage_heading Some(pretty) -> heading_style(usage_heading, pretty.usage) } - <> "\n\t" + <> "\n" + <> string.repeat(" ", config.indent_width) <> utils.wordwrap( app_name <> string_map(help.meta.name, string.append(" ", _)) @@ -870,7 +877,7 @@ fn command_help_to_usage_string(help: CommandHelp, config: Config) -> String { <> string_map(flags, string.append(" ", _)), max_usage_width, ) - |> string.join("\n\t ") + |> string.join("\n" <> string.repeat(" ", config.indent_width * 2)) } // -- HELP - FUNCTIONS - STRINGIFIERS - FLAGS -- @@ -898,7 +905,10 @@ fn flags_help_to_string(help: List(FlagHelp), config: Config) -> String { config, )) |> list.sort(string.compare) - |> list.map(string.append("\n\t", _)) + |> list.map(string.append( + "\n" <> string.repeat(" ", config.indent_width), + _, + )) |> string.concat } } @@ -927,13 +937,16 @@ fn flag_help_to_string_with_description( |> string.pad_right(longest_flag_length, " ") let description_width = - { config.max_output_width - longest_flag_length - tab_width } + config.max_output_width + |> int.subtract(longest_flag_length + config.indent_width) |> int.max(config.min_first_column_width) let description = help.meta.description |> utils.wordwrap(description_width) - |> string.join("\n\t" <> string.repeat(" ", longest_flag_length)) + |> string.join( + "\n" <> string.repeat(" ", config.indent_width + longest_flag_length), + ) name <> description } @@ -963,7 +976,10 @@ fn subcommands_help_to_string(help: List(Metadata), config: Config) -> String { config, )) |> list.sort(string.compare) - |> list.map(string.append("\n\t", _)) + |> list.map(string.append( + "\n" <> string.repeat(" ", config.indent_width), + _, + )) |> string.concat } } @@ -978,13 +994,17 @@ fn subcommand_help_to_string( let name = string.pad_right(help.name, longest_subcommand_length, " ") let description_width = - { config.max_output_width - longest_subcommand_length - tab_width } + config.max_output_width + |> int.subtract(longest_subcommand_length + config.indent_width) |> int.max(config.min_first_column_width) let description = help.description |> utils.wordwrap(description_width) - |> string.join("\n\t" <> string.repeat(" ", longest_subcommand_length)) + |> string.join( + "\n" + <> string.repeat(" ", config.indent_width + longest_subcommand_length), + ) name <> description } diff --git a/test/examples/hello.gleam b/test/examples/hello.gleam index efab0e1..ca785e7 100644 --- a/test/examples/hello.gleam +++ b/test/examples/hello.gleam @@ -29,15 +29,15 @@ //// Prints Hello, ! //// //// USAGE: -//// gleam run -m examples/hello ( single ) [ 1 or more arguments ] [ --caps= --repeat= ] +//// gleam run -m examples/hello ( single ) [ 1 or more arguments ] [ --caps= --repeat= ] //// //// FLAGS: -//// --caps= Capitalize the hello message -//// --help Print help information -//// --repeat= Repeat the message n-times +//// --caps= Capitalize the hello message +//// --help Print help information +//// --repeat= Repeat the message n-times //// //// SUBCOMMANDS: -//// single Prints Hello, ! +//// single Prints Hello, ! //// ``` //// //// Here is the help text for the `single` command: @@ -50,12 +50,12 @@ //// Prints Hello, ! //// //// USAGE: -//// gleam run -m examples/hello single [ --caps= --repeat= ] +//// gleam run -m examples/hello single [ --caps= --repeat= ] //// //// FLAGS: -//// --caps= Capitalize the hello message -//// --help Print help information -//// --repeat= Repeat the message n-times +//// --caps= Capitalize the hello message +//// --help Print help information +//// --repeat= Repeat the message n-times //// ``` // stdlib imports diff --git a/test/glint_test.gleam b/test/glint_test.gleam index 52b4640..b0ba418 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -10,10 +10,7 @@ pub fn main() { pub fn path_clean_test() { glint.new() - |> glint.add( - ["", " ", " cmd", "subcmd\t"], - glint.command(fn(_, _, _) { Nil }), - ) + |> glint.add(["", " ", " cmd", "subcmd"], glint.command(fn(_, _, _) { Nil })) |> glint.execute(["cmd", "subcmd"]) |> should.be_ok() } From 069a99ba6ce6a3f1726b9d19a126c7269c5cc177 Mon Sep 17 00:00:00 2001 From: Richard Viney Date: Tue, 11 Jun 2024 12:17:24 +1200 Subject: [PATCH 5/5] Add back \t --- test/glint_test.gleam | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/glint_test.gleam b/test/glint_test.gleam index b0ba418..52b4640 100644 --- a/test/glint_test.gleam +++ b/test/glint_test.gleam @@ -10,7 +10,10 @@ pub fn main() { pub fn path_clean_test() { glint.new() - |> glint.add(["", " ", " cmd", "subcmd"], glint.command(fn(_, _, _) { Nil })) + |> glint.add( + ["", " ", " cmd", "subcmd\t"], + glint.command(fn(_, _, _) { Nil }), + ) |> glint.execute(["cmd", "subcmd"]) |> should.be_ok() }