v2 / vlib / cli / help.v
168 lines · 160 sloc · 4.46 KB · 7ab7149115ad367f929cf6d7f26da6c5c16b8498
Raw
1module cli
2
3import term
4import strings
5
6const base_indent_len = 2
7const min_description_indent_len = 20
8const spacing = 2
9
10fn help_flag(with_abbrev bool) Flag {
11 sabbrev := if with_abbrev { 'h' } else { '' }
12 return Flag{
13 flag: .bool
14 name: 'help'
15 abbrev: sabbrev
16 description: 'Print help information'
17 }
18}
19
20fn help_cmd() Command {
21 return Command{
22 name: 'help'
23 usage: '<command>'
24 description: 'Print help information'
25 execute: print_help_for_command
26 }
27}
28
29// print_help_for_command outputs the help message of `help_cmd`.
30pub fn print_help_for_command(cmd Command) ! {
31 if cmd.args.len > 0 {
32 for sub_cmd in cmd.commands {
33 if sub_cmd.matches(cmd.args[0]) {
34 cmd_ := unsafe { &sub_cmd }
35 print(cmd_.help_message())
36 return
37 }
38 }
39 print('Invalid command: ${cmd.args.join(' ')}')
40 } else if cmd.parent != unsafe { nil } {
41 print(cmd.parent.help_message())
42 }
43}
44
45// help_message returns a generated help message as a `string` for the `Command`.
46pub fn (cmd &Command) help_message() string {
47 mut help := ''
48 help += 'Usage: ${cmd.full_name()}'
49 if cmd.flags.len > 0 {
50 help += ' [flags]'
51 }
52 if cmd.commands.len > 0 {
53 help += ' [commands]'
54 }
55 if cmd.usage.len > 0 {
56 help += ' ${cmd.usage}'
57 } else {
58 for i in 0 .. cmd.required_args {
59 help += ' <arg${i}>'
60 }
61 }
62 help += '\n'
63 if cmd.description != '' {
64 help += '\n${cmd.description}\n'
65 }
66 mut abbrev_len := 0
67 mut name_len := min_description_indent_len
68 if cmd.posix_mode {
69 for flag in cmd.flags {
70 if flag.abbrev != '' {
71 abbrev_len = max(abbrev_len, flag.abbrev.len + spacing + 1) // + 1 for '-' in front
72 }
73 name_len =
74 max(name_len, abbrev_len + flag.name.len + spacing + 2) // + 2 for '--' in front
75 }
76 for command in cmd.commands {
77 name_len = max(name_len, command.display_name().len + spacing)
78 }
79 } else {
80 for flag in cmd.flags {
81 if flag.abbrev != '' {
82 abbrev_len = max(abbrev_len, flag.abbrev.len + spacing + 1) // + 1 for '-' in front
83 }
84 name_len =
85 max(name_len, abbrev_len + flag.name.len + spacing + 1) // + 1 for '-' in front
86 }
87 for command in cmd.commands {
88 name_len = max(name_len, command.display_name().len + spacing)
89 }
90 }
91 if cmd.flags.len > 0 {
92 help += '\nFlags:\n'
93 for flag in cmd.flags {
94 mut flag_name := ''
95 prefix := if cmd.posix_mode { '--' } else { '-' }
96 if flag.abbrev != '' {
97 abbrev_indent :=
98 ' '.repeat(abbrev_len - flag.abbrev.len - 1) // - 1 for '-' in front
99 flag_name = '-${flag.abbrev}${abbrev_indent}${prefix}${flag.name}'
100 } else {
101 abbrev_indent := ' '.repeat(abbrev_len)
102 flag_name = '${abbrev_indent}${prefix}${flag.name}'
103 }
104 mut required := ''
105 if flag.required {
106 required = ' (required)'
107 }
108 base_indent := ' '.repeat(base_indent_len)
109 description_indent := ' '.repeat(name_len - flag_name.len)
110 help += '${base_indent}${flag_name}${description_indent}' +
111 pretty_description(flag.description + required, base_indent_len + name_len) + '\n'
112 }
113 }
114 if cmd.commands.len > 0 {
115 help += '\nCommands:\n'
116 for command in cmd.commands {
117 command_name := command.display_name()
118 base_indent := ' '.repeat(base_indent_len)
119 description_indent := ' '.repeat(name_len - command_name.len)
120 help += '${base_indent}${command_name}${description_indent}' +
121 pretty_description(command.description, base_indent_len + name_len) + '\n'
122 }
123 }
124 return help
125}
126
127// pretty_description resizes description text depending on terminal width.
128// Essentially, smart wrap-around
129fn pretty_description(s string, indent_len int) string {
130 width, _ := term.get_terminal_size()
131 // Don't prettify if the terminal is that small, it won't be pretty anyway.
132 if indent_len > width {
133 return s
134 }
135 indent := ' '.repeat(indent_len)
136 chars_per_line := width - indent_len
137 // Give us enough room, better a little bigger than smaller
138 mut acc := strings.new_builder(((s.len / chars_per_line) + 1) * (width + 1))
139 for k, line in s.split('\n') {
140 if k != 0 {
141 acc.write_string('\n${indent}')
142 }
143 mut i := chars_per_line - 2
144 mut j := 0
145 for ; i < line.len; i += chars_per_line - 2 {
146 for j > 0 && line[j] != ` ` {
147 j--
148 }
149 // indent was already done the first iteration
150 if j != 0 {
151 acc.write_string(indent)
152 }
153 acc.writeln(line[j..i].trim_space())
154 j = i
155 }
156 // We need this even though it should never happen
157 if j != 0 {
158 acc.write_string(indent)
159 }
160 acc.write_string(line[j..].trim_space())
161 }
162 return acc.str()
163}
164
165fn max(a int, b int) int {
166 res := if a > b { a } else { b }
167 return res
168}
169