module cli import term import strings const ( base_indent_len = 2 min_description_indent_len = 20 spacing = 2 ) fn help_flag(with_abbrev bool) Flag { sabbrev := if with_abbrev { 'h' } else { '' } return Flag{ flag: .bool name: 'help' abbrev: sabbrev description: 'Prints help information.' } } fn help_cmd() Command { return Command{ name: 'help' usage: '' description: 'Prints help information.' execute: print_help_for_command } } // print_help_for_command outputs the help message of `help_cmd`. pub fn print_help_for_command(help_cmd Command) ! { if help_cmd.args.len > 0 { mut cmd := help_cmd.parent for arg in help_cmd.args { mut found := false for sub_cmd in cmd.commands { if sub_cmd.name == arg { cmd = unsafe { &sub_cmd } found = true break } } if !found { args := help_cmd.args.join(' ') println('Invalid command: ${args}') return } } print(cmd.help_message()) } else { if unsafe { help_cmd.parent != 0 } { print(help_cmd.parent.help_message()) } } } // help_message returns a generated help message as a `string` for the `Command`. pub fn (cmd Command) help_message() string { mut help := '' help += 'Usage: ${cmd.full_name()}' if cmd.flags.len > 0 { help += ' [flags]' } if cmd.commands.len > 0 { help += ' [commands]' } if cmd.usage.len > 0 { help += ' ${cmd.usage}' } else { for i in 0 .. cmd.required_args { help += ' ' } } help += '\n' if cmd.description != '' { help += '\n${cmd.description}\n' } mut abbrev_len := 0 mut name_len := cli.min_description_indent_len if cmd.posix_mode { for flag in cmd.flags { if flag.abbrev != '' { abbrev_len = max(abbrev_len, flag.abbrev.len + cli.spacing + 1) // + 1 for '-' in front } name_len = max(name_len, abbrev_len + flag.name.len + cli.spacing + 2) // + 2 for '--' in front } for command in cmd.commands { name_len = max(name_len, command.name.len + cli.spacing) } } else { for flag in cmd.flags { if flag.abbrev != '' { abbrev_len = max(abbrev_len, flag.abbrev.len + cli.spacing + 1) // + 1 for '-' in front } name_len = max(name_len, abbrev_len + flag.name.len + cli.spacing + 1) // + 1 for '-' in front } for command in cmd.commands { name_len = max(name_len, command.name.len + cli.spacing) } } if cmd.flags.len > 0 { help += '\nFlags:\n' for flag in cmd.flags { mut flag_name := '' prefix := if cmd.posix_mode { '--' } else { '-' } if flag.abbrev != '' { abbrev_indent := ' '.repeat(abbrev_len - flag.abbrev.len - 1) // - 1 for '-' in front flag_name = '-${flag.abbrev}${abbrev_indent}${prefix}${flag.name}' } else { abbrev_indent := ' '.repeat(abbrev_len) flag_name = '${abbrev_indent}${prefix}${flag.name}' } mut required := '' if flag.required { required = ' (required)' } base_indent := ' '.repeat(cli.base_indent_len) description_indent := ' '.repeat(name_len - flag_name.len) help += '${base_indent}${flag_name}${description_indent}' + pretty_description(flag.description + required, cli.base_indent_len + name_len) + '\n' } } if cmd.commands.len > 0 { help += '\nCommands:\n' for command in cmd.commands { base_indent := ' '.repeat(cli.base_indent_len) description_indent := ' '.repeat(name_len - command.name.len) help += '${base_indent}${command.name}${description_indent}' + pretty_description(command.description, name_len) + '\n' } } return help } // pretty_description resizes description text depending on terminal width. // Essentially, smart wrap-around fn pretty_description(s string, indent_len int) string { width, _ := term.get_terminal_size() // Don't prettify if the terminal is that small, it won't be pretty anyway. if indent_len > width { return s } indent := ' '.repeat(indent_len) chars_per_line := width - indent_len // Give us enough room, better a little bigger than smaller mut acc := strings.new_builder(((s.len / chars_per_line) + 1) * (width + 1)) for k, line in s.split('\n') { if k != 0 { acc.write_string('\n${indent}') } mut i := chars_per_line - 2 mut j := 0 for ; i < line.len; i += chars_per_line - 2 { for j > 0 && line[j] != ` ` { j-- } // indent was already done the first iteration if j != 0 { acc.write_string(indent) } acc.writeln(line[j..i].trim_space()) j = i } // We need this even though it should never happen if j != 0 { acc.write_string(indent) } acc.write_string(line[j..].trim_space()) } return acc.str() } fn max(a int, b int) int { res := if a > b { a } else { b } return res }