claide 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,177 +1,193 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'claide/command/banner/prettifier'
4
+
3
5
  module CLAide
4
6
  class Command
5
-
6
7
  # Creates the formatted banner to present as help of the provided command
7
8
  # class.
8
9
  #
9
10
  class Banner
10
-
11
- # @return [Class]
11
+ # @return [Class] The command for which the banner should be created.
12
12
  #
13
13
  attr_accessor :command
14
14
 
15
- # @return [Bool]
16
- #
17
- attr_accessor :ansi_output
18
- alias_method :ansi_output?, :ansi_output
19
-
20
- def colorize_output
21
- warn "[!] The use of `CLAide::Command::Banner#colorize_output` has " \
22
- "been deprecated. Use `CLAide::Command::Banner#ansi_output` " \
23
- "instead. (Called from: #{caller.first})"
24
- ansi_output
25
- end
26
- alias_method :colorize_output?, :colorize_output
27
-
28
- def colorize_output=(flag)
29
- warn "[!] The use of `CLAide::Command::Banner#colorize_output=` has " \
30
- "been deprecated. Use `CLAide::Command::Banner#ansi_output=` " \
31
- "instead. (Called from: #{caller.first})"
32
- self.ansi_output = flag
33
- end
34
-
35
15
  # @param [Class] command @see command
36
- # @param [Class] ansi_output @see ansi_output
37
16
  #
38
- def initialize(command, ansi_output = false)
17
+ def initialize(command)
39
18
  @command = command
40
- @ansi_output = ansi_output
41
19
  end
42
20
 
43
- # @return [String]
21
+ # @return [String] The banner for the command.
44
22
  #
45
23
  def formatted_banner
46
- banner = []
47
- if command.abstract_command?
48
- banner << command.description if command.description
49
- elsif usage = formatted_usage_description
50
- banner << 'Usage:'
51
- banner << usage
52
- end
53
- if commands = formatted_subcommand_summaries
54
- banner << 'Commands:'
55
- banner << commands
56
- end
57
- banner << 'Options:'
58
- banner << formatted_options_description
59
- banner.join("\n\n")
24
+ sections = [
25
+ ['Usage', formatted_usage_description],
26
+ ['Commands', formatted_subcommand_summaries],
27
+ ['Options', formatted_options_description]
28
+ ]
29
+ banner = sections.map do |(title, body)|
30
+ ["#{prettify_title(title)}:", body] unless body.empty?
31
+ end.compact.join("\n\n")
32
+ banner
60
33
  end
61
34
 
62
35
  private
63
36
 
64
37
  # @!group Banner sections
65
-
66
38
  #-----------------------------------------------------------------------#
67
39
 
68
- # @return [String]
40
+ # @return [String] The indentation of the text.
69
41
  #
70
- def formatted_options_description
71
- opts = command.options
72
- max_key_size = opts.map { |opt| opt.first.size }.max
73
-
74
- desc_start = max_key_size + 7 # fixed whitespace in `result` var
75
- desc_width = terminal_width - desc_start
76
-
77
- opts.map do |key, desc|
78
- space = ' ' * (max_key_size - key.size)
79
- result = " #{prettify_option_name(key)}#{space} "
80
- if terminal_width == 0
81
- result << desc
82
- else
83
- space = ' ' * desc_start
84
- result << word_wrap(desc, desc_width).split("\n").join("\n#{space}")
85
- end
86
- end.join("\n")
42
+ TEXT_INDENT = 6
43
+
44
+ # @return [Fixnum] The maximum width of the text.
45
+ #
46
+ MAX_WIDTH = TEXT_INDENT + 80
47
+
48
+ # @return [Fixnum] The minimum between a name and its description.
49
+ #
50
+ DESCRIPTION_SPACES = 3
51
+
52
+ # @return [Fixnum] The minimum between a name and its description.
53
+ #
54
+ SUBCOMMAND_BULLET_SIZE = 2
55
+
56
+ # @return [String] The section describing the usage of the command.
57
+ #
58
+ def formatted_usage_description
59
+ message = command.description || command.summary || ''
60
+ message = Helper.format_markdown(message, TEXT_INDENT, MAX_WIDTH)
61
+ message = prettify_message(command, message)
62
+ "#{signature}\n\n#{message}"
87
63
  end
88
64
 
89
- # @return [String]
65
+ # @return [String] The signature of the command.
90
66
  #
91
- def prettify_option_name(name)
92
- name
67
+ def signature
68
+ result = prettify_signature(
69
+ command.full_command, signature_sub_command, signature_arguments)
70
+ result.insert(0, '$ ')
71
+ result.insert(0, ' ' * (TEXT_INDENT - '$ '.size))
93
72
  end
94
73
 
95
- # @return [String]
74
+ # @return [String] The subcommand indicator of the signature.
96
75
  #
97
- def formatted_usage_description
98
- if message = command.description || command.summary
99
- message = strip_heredoc(message)
100
- message = message.split("\n").map { |line| " #{line}" }.join("\n")
101
- args = " #{command.arguments}" if command.arguments
102
- cmd = "$ #{command.full_command}#{args}"
103
- " #{prettify_command_in_usage_description(cmd)}\n\n#{message}"
76
+ def signature_sub_command
77
+ if command.subcommands.any?
78
+ command.default_subcommand ? '[COMMAND]' : 'COMMAND'
104
79
  end
80
+ ''
105
81
  end
106
82
 
107
- # @return [String]
83
+ # @return [String] The arguments of the signature.
108
84
  #
109
- def prettify_command_in_usage_description(command)
110
- command
85
+ def signature_arguments
86
+ command.arguments.reduce('') do |memo, (name, type)|
87
+ name = "[#{name}]" if type == :optional
88
+ memo << ' ' << name
89
+ end.lstrip
111
90
  end
112
91
 
113
- # @return [String]
92
+ # @return [String] The section describing the subcommands of the command.
93
+ #
94
+ # @note The plus sign emphasizes the that the subcommands are added to
95
+ # the command. The square brackets conveys a sense of direction
96
+ # and indicates the gravitational force towards the default
97
+ # command.
114
98
  #
115
99
  def formatted_subcommand_summaries
116
- subcommands = command.subcommands_for_command_lookup.reject do |subcommand|
117
- subcommand.summary.nil?
118
- end.sort_by(&:command)
119
- unless subcommands.empty?
120
- command_size = subcommands.map { |cmd| cmd.command.size }.max
121
- subcommands.map do |subcommand|
122
- subcommand_string = subcommand.command.ljust(command_size)
123
- subcommand_string = prettify_subcommand_name(subcommand_string)
124
- is_default = subcommand.command == command.default_subcommand
125
- if is_default
126
- bullet_point = '-'
127
- else
128
- bullet_point = '*'
129
- end
130
- " #{bullet_point} #{subcommand_string} #{subcommand.summary}"
131
- end.join("\n")
132
- end
100
+ subcommands = subcommands_for_banner
101
+ subcommands.map do |subcommand|
102
+ name = subcommand.command
103
+ bullet = (name == command.default_subcommand) ? '>' : '+'
104
+ name = "#{bullet} #{name}"
105
+ pretty_name = prettify_subcommand(name)
106
+ entry_description(pretty_name, subcommand.summary, name.size)
107
+ end.join("\n")
133
108
  end
134
109
 
135
- # @return [String]
110
+ # @return [String] The section describing the options of the command.
136
111
  #
137
- def prettify_subcommand_name(name)
138
- ansi_output? ? name.green : name
112
+ def formatted_options_description
113
+ options = command.options
114
+ options.map do |name, description|
115
+ pretty_name = prettify_option_name(name)
116
+ entry_description(pretty_name, description, name.size)
117
+ end.join("\n")
139
118
  end
140
119
 
141
- private
142
-
143
- # @!group Private helpers
120
+ # @return [String] The line describing a single entry (subcommand or
121
+ # option).
122
+ #
123
+ def entry_description(name, description, name_width)
124
+ max_name_width = compute_max_name_width
125
+ desc_start = max_name_width + (TEXT_INDENT - 2) + DESCRIPTION_SPACES
126
+ result = ' ' * (TEXT_INDENT - 2)
127
+ result << name
128
+ result << ' ' * DESCRIPTION_SPACES
129
+ result << ' ' * (max_name_width - name_width)
130
+ result << Helper.wrap_with_indent(description, desc_start, MAX_WIDTH)
131
+ end
144
132
 
133
+ # @!group Overrides
145
134
  #-----------------------------------------------------------------------#
146
135
 
147
- # @return [String] Lifted straight from ActiveSupport. Thanks guys!
136
+ # @return [String] A decorated title.
148
137
  #
149
- def strip_heredoc(string)
150
- if min = string.scan(/^[ \t]*(?=\S)/).min
151
- string.gsub(/^[ \t]{#{min.size}}/, '')
152
- else
153
- string
154
- end
138
+ def prettify_title(title)
139
+ Prettifier.prettify_title(title)
155
140
  end
156
141
 
157
- # @return [String] Lifted straight from ActionView. Thanks guys!
142
+ # @return [String] A decorated textual representation of the command.
158
143
  #
159
- def word_wrap(line, line_width)
160
- line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip
144
+ def prettify_signature(command, subcommand, argument)
145
+ Prettifier.prettify_signature(command, subcommand, argument)
161
146
  end
162
147
 
163
- # @return [Fixnum] The width of the current terminal, unless being piped.
148
+ # @return [String] A decorated command description.
164
149
  #
165
- def terminal_width
166
- @terminal_width ||= begin
167
- if STDOUT.tty? && system('which tput > /dev/null 2>&1')
168
- `tput cols`.to_i
169
- else
170
- 0
171
- end
172
- end
150
+ def prettify_message(command, message)
151
+ Prettifier.prettify_message(command, message)
152
+ end
153
+
154
+ # @return [String] A decorated textual representation of the subcommand
155
+ # name.
156
+ #
157
+ def prettify_subcommand(name)
158
+ Prettifier.prettify_subcommand(name)
159
+ end
160
+
161
+ # @return [String] A decorated textual representation of the option name.
162
+ #
163
+ #
164
+ def prettify_option_name(name)
165
+ Prettifier.prettify_option_name(name)
166
+ end
167
+
168
+ # @!group Private helpers
169
+ #-----------------------------------------------------------------------#
170
+
171
+ # @return [Array<String>] The list of the subcommands to use in the
172
+ # banner.
173
+ #
174
+ def subcommands_for_banner
175
+ command.subcommands_for_command_lookup.reject do |subcommand|
176
+ subcommand.summary.nil?
177
+ end.sort_by(&:command)
173
178
  end
174
179
 
180
+ # @return [Fixnum] The width of the largest command name or of the
181
+ # largest option name. Used to align all the descriptions.
182
+ #
183
+ def compute_max_name_width
184
+ widths = []
185
+ widths << command.options.map { |option| option.first.size }
186
+ widths << subcommands_for_banner.map do |cmd|
187
+ cmd.command.size + SUBCOMMAND_BULLET_SIZE
188
+ end.max
189
+ widths.flatten.compact.max || 1
190
+ end
175
191
  end
176
192
  end
177
193
  end
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+
3
+ module CLAide
4
+ class Command
5
+ class Banner
6
+ # Implements the default logic to prettify the Banner.
7
+ #
8
+ module Prettifier
9
+ # @return [String] A decorated title.
10
+ #
11
+ def self.prettify_title(title)
12
+ title.ansi.underline
13
+ end
14
+
15
+ # @return [String] A decorated textual representation of the command.
16
+ #
17
+ def self.prettify_signature(command, subcommand, argument)
18
+ components = [
19
+ [command, :green],
20
+ [subcommand, :green],
21
+ [argument, :magenta]
22
+ ]
23
+ components.reduce('') do |memo, (string, ansi_key)|
24
+ memo << ' ' << string.ansi.apply(ansi_key) unless string.empty?
25
+ memo
26
+ end.lstrip
27
+ end
28
+
29
+ # @return [String] A decorated command description.
30
+ #
31
+ def self.prettify_message(command, message)
32
+ message = message.dup
33
+ [[command.arguments, :magenta],
34
+ [command.options, :blue]].each do |(list, ansi_key)|
35
+ list.map(&:first).each do |name|
36
+ message.gsub!(/`#{name}`/, "`#{name}`".ansi.apply(ansi_key))
37
+ end
38
+ end
39
+ message
40
+ end
41
+
42
+ # @return [String] A decorated textual representation of the subcommand
43
+ # name.
44
+ #
45
+ def self.prettify_subcommand(name)
46
+ name.chomp.ansi.green
47
+ end
48
+
49
+ # @return [String] A decorated textual representation of the option
50
+ # name.
51
+ #
52
+ #
53
+ def self.prettify_option_name(name)
54
+ name.chomp.ansi.blue
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+
3
+ module CLAide
4
+ class Command
5
+ # Provides support for the default options.
6
+ #
7
+ module Options
8
+ # @return [Array<Array<String, String>>] The default options for a root
9
+ # command implemented by CLAide.
10
+ #
11
+ DEFAULT_ROOT_OPTIONS = [
12
+ ['--completion-script', 'Print the auto-completion script'],
13
+ ['--version', 'Show the version of the tool']
14
+ ]
15
+
16
+ # @return [Array<Array<String, String>>] The default options implemented
17
+ # by CLAide.
18
+ #
19
+ DEFAULT_OPTIONS = [
20
+ ['--verbose', 'Show more debugging information'],
21
+ ['--no-ansi', 'Show output without ANSI codes'],
22
+ ['--help', 'Show help banner of specified command']
23
+ ]
24
+
25
+ # @return [Array<Array<String, String>>] The list of the default
26
+ # options for the given command.
27
+ #
28
+ # @param [Class] command_class
29
+ # The command class for which the options are needed.
30
+ #
31
+ def self.default_options(command_class)
32
+ if command_class.root_command?
33
+ Options::DEFAULT_ROOT_OPTIONS + Options::DEFAULT_OPTIONS
34
+ else
35
+ Options::DEFAULT_OPTIONS
36
+ end
37
+ end
38
+
39
+ # Handles root commands options if appropriate.
40
+ #
41
+ # @param [Command] command
42
+ # The invoked command.
43
+ #
44
+ # @param [ARGV] argv
45
+ # The parameters of the command.
46
+ #
47
+ # @return [Bool] Whether any root command option was handled.
48
+ #
49
+ def self.handle_root_option(command, argv)
50
+ argv = ARGV.coherce(argv)
51
+ return false unless command.class.root_command?
52
+ if argv.flag?('version')
53
+ print_version(command)
54
+ return true
55
+ elsif argv.flag?('completion-script')
56
+ print_completion_template(command)
57
+ return true
58
+ end
59
+ false
60
+ end
61
+
62
+ # Prints the version of the command optionally including plugins.
63
+ #
64
+ # @param [Command] command
65
+ # The invoked command.
66
+ #
67
+ def self.print_version(command)
68
+ puts command.class.version
69
+ if command.verbose?
70
+ prefix = command.class.plugin_prefix
71
+ PluginsHelper.plugin_load_paths(prefix).each do |path|
72
+ puts PluginsHelper.plugin_info(path)
73
+ end
74
+ end
75
+ end
76
+
77
+ # Prints an auto-completion script according to the user shell.
78
+ #
79
+ # @param [Command] command
80
+ # The invoked command.#
81
+ #
82
+ def self.print_completion_template(command)
83
+ puts ShellCompletionHelper.completion_template(command.class)
84
+ end
85
+ end
86
+ end
87
+ end