command_kit 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +4 -6
  3. data/.rubocop.yml +13 -0
  4. data/ChangeLog.md +18 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE.txt +1 -1
  7. data/README.md +18 -9
  8. data/command_kit.gemspec +0 -1
  9. data/examples/printing/tables.rb +141 -0
  10. data/gemspec.yml +3 -3
  11. data/lib/command_kit/bug_report.rb +105 -0
  12. data/lib/command_kit/colors.rb +4 -4
  13. data/lib/command_kit/edit.rb +54 -0
  14. data/lib/command_kit/env.rb +1 -1
  15. data/lib/command_kit/options/option.rb +5 -1
  16. data/lib/command_kit/options/option_value.rb +2 -2
  17. data/lib/command_kit/options/parser.rb +1 -1
  18. data/lib/command_kit/options/quiet.rb +1 -1
  19. data/lib/command_kit/options/verbose.rb +2 -2
  20. data/lib/command_kit/options/version.rb +10 -0
  21. data/lib/command_kit/options.rb +1 -1
  22. data/lib/command_kit/os.rb +1 -1
  23. data/lib/command_kit/printing/fields.rb +56 -0
  24. data/lib/command_kit/printing/indent.rb +1 -1
  25. data/lib/command_kit/printing/lists.rb +91 -0
  26. data/lib/command_kit/printing/tables/border_style.rb +169 -0
  27. data/lib/command_kit/printing/tables/cell_builder.rb +93 -0
  28. data/lib/command_kit/printing/tables/row_builder.rb +111 -0
  29. data/lib/command_kit/printing/tables/style.rb +198 -0
  30. data/lib/command_kit/printing/tables/table_builder.rb +145 -0
  31. data/lib/command_kit/printing/tables/table_formatter.rb +254 -0
  32. data/lib/command_kit/printing/tables.rb +208 -0
  33. data/lib/command_kit/stdio.rb +5 -1
  34. data/lib/command_kit/version.rb +1 -1
  35. data/spec/bug_report_spec.rb +266 -0
  36. data/spec/colors_spec.rb +6 -0
  37. data/spec/command_name_spec.rb +1 -1
  38. data/spec/edit_spec.rb +72 -0
  39. data/spec/options/option_spec.rb +12 -2
  40. data/spec/options/quiet_spec.rb +51 -0
  41. data/spec/options/verbose_spec.rb +51 -0
  42. data/spec/options/version_spec.rb +146 -0
  43. data/spec/pager_spec.rb +1 -1
  44. data/spec/printing/fields_spec.rb +167 -0
  45. data/spec/printing/lists_spec.rb +99 -0
  46. data/spec/printing/tables/border_style.rb +43 -0
  47. data/spec/printing/tables/cell_builer_spec.rb +135 -0
  48. data/spec/printing/tables/row_builder_spec.rb +165 -0
  49. data/spec/printing/tables/style_spec.rb +377 -0
  50. data/spec/printing/tables/table_builder_spec.rb +252 -0
  51. data/spec/printing/tables/table_formatter_spec.rb +1180 -0
  52. data/spec/printing/tables_spec.rb +1069 -0
  53. metadata +33 -7
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'command_kit/printing/tables/border_style'
4
+
5
+ module CommandKit
6
+ module Printing
7
+ module Tables
8
+ #
9
+ # Contains the table's style configuration.
10
+ #
11
+ # @api private
12
+ #
13
+ class Style
14
+
15
+ # Built-in border styles.
16
+ BORDER_STYLES = {
17
+ ascii: BorderStyle.new(
18
+ top_left_corner: '+',
19
+ top_border: '-',
20
+ top_joined_border: '+',
21
+ top_right_corner: '+',
22
+ left_border: '|',
23
+ left_joined_border: '+',
24
+ horizontal_separator: '-',
25
+ vertical_separator: '|',
26
+ inner_joined_border: '+',
27
+ right_border: '|',
28
+ right_joined_border: '+',
29
+ bottom_border: '-',
30
+ bottom_left_corner: '+',
31
+ bottom_joined_border: '+',
32
+ bottom_right_corner: '+'
33
+ ),
34
+
35
+ line: BorderStyle.new(
36
+ top_left_corner: '┌',
37
+ top_border: '─',
38
+ top_joined_border: '┬',
39
+ top_right_corner: '┐',
40
+ left_border: '│',
41
+ left_joined_border: '├',
42
+ horizontal_separator: '─',
43
+ vertical_separator: '│',
44
+ inner_joined_border: '┼',
45
+ right_border: '│',
46
+ right_joined_border: '┤',
47
+ bottom_border: '─',
48
+ bottom_left_corner: '└',
49
+ bottom_joined_border: '┴',
50
+ bottom_right_corner: '┘'
51
+ ),
52
+
53
+ double_line: BorderStyle.new(
54
+ top_left_corner: '╔',
55
+ top_border: '═',
56
+ top_joined_border: '╦',
57
+ top_right_corner: '╗',
58
+ left_border: '║',
59
+ left_joined_border: '╠',
60
+ horizontal_separator: '═',
61
+ vertical_separator: '║',
62
+ inner_joined_border: '╬',
63
+ right_border: '║',
64
+ right_joined_border: '╣',
65
+ bottom_border: '═',
66
+ bottom_left_corner: '╚',
67
+ bottom_joined_border: '╩',
68
+ bottom_right_corner: '╝'
69
+ )
70
+ }
71
+
72
+ # The border style.
73
+ #
74
+ # @return [BorderStyle]
75
+ attr_reader :border
76
+
77
+ # The padding to use for cells.
78
+ #
79
+ # @return [Integer]
80
+ attr_reader :padding
81
+
82
+ # The justification to use for cells.
83
+ #
84
+ # @return [:left, :right, :center]
85
+ attr_reader :justify
86
+
87
+ # The justification to use for header cells.
88
+ #
89
+ # @return [:left, :right, :center]
90
+ attr_reader :justify_header
91
+
92
+ # Specifies whether to separate rows with a border row.
93
+ #
94
+ # @return [Boolean]
95
+ attr_reader :separate_rows
96
+
97
+ #
98
+ # Initializes the style.
99
+ #
100
+ # @param [:line, :double_line, nil, Hash{Symbol => String}, :ascii] border
101
+ # The border style or a custom Hash of border characters.
102
+ #
103
+ # @option border [String] :top_left_corner (' ')
104
+ # The top-left-corner border character.
105
+ #
106
+ # @option border [String] :top_border (' ')
107
+ # The top-border character.
108
+ #
109
+ # @option border [String] :top_joined_border (' ')
110
+ # The top-joined-border character.
111
+ #
112
+ # @option border [String] :top_right_corner (' ')
113
+ # The top-right-corner border character.
114
+ #
115
+ # @option border [String] :left_border (' ')
116
+ # The left-hand-side border character.
117
+ #
118
+ # @option border [String] :left_joined_border (' ')
119
+ # The left-hand-side-joined-border character.
120
+ #
121
+ # @option border [String] :horizontal_separator (' ')
122
+ # The horizontal-separator character.
123
+ #
124
+ # @option border [String] :vertical_separator (' ')
125
+ # The vertical-separator character.
126
+ #
127
+ # @option border [String] :inner_joined_border (' ')
128
+ # The inner-joined border character.
129
+ #
130
+ # @option border [String] :right_border (' ')
131
+ # The right-hand-side border character.
132
+ #
133
+ # @option border [String] :right_joined_border (' ')
134
+ # The right-hand-side joined border character.
135
+ #
136
+ # @option border [String] :bottom_border (' ')
137
+ # The bottom border character.
138
+ #
139
+ # @option border [String] :bottom_left_corner (' ')
140
+ # The bottom-left-corner border character.
141
+ #
142
+ # @option border [String] :bottom_joined_border (' ')
143
+ # The bottom-joined border character.
144
+ #
145
+ # @option border [String] :bottom_right_corner (' ')
146
+ # The bottom-right-corner border character.
147
+ #
148
+ # @param [Integer] padding
149
+ # The number of characters to pad table cell values with.
150
+ #
151
+ # @param [:left, :right, :center] justify
152
+ # Specifies how to justify the table cell values.
153
+ #
154
+ # @param [:left, :right, :center] justify_header
155
+ # Specifies how to justify the table header cell values.
156
+ #
157
+ # @param [Boolean] separate_rows
158
+ # Specifies whether to add separator rows in between the rows.
159
+ #
160
+ def initialize(border: nil,
161
+ padding: 1,
162
+ justify: :left,
163
+ justify_header: :center,
164
+ separate_rows: false)
165
+ @border = case border
166
+ when Hash
167
+ BorderStyle.new(**border)
168
+ when Symbol
169
+ BORDER_STYLES.fetch(border) do
170
+ raise(ArgumentError,"unknown border style (#{border.inspect}) must be either #{BORDER_STYLES.keys.map(&:inspect).join(', ')}")
171
+ end
172
+ when nil then nil
173
+ else
174
+ raise(ArgumentError,"invalid border value (#{border.inspect}) must be either #{BORDER_STYLES.keys.map(&:inspect).join(', ')}, Hash, or nil")
175
+ end
176
+
177
+ @padding = padding
178
+
179
+ @justify = justify
180
+ @justify_header = justify_header
181
+
182
+ @separate_rows = separate_rows
183
+ end
184
+
185
+ #
186
+ # Determines if the rows should be separated.
187
+ #
188
+ # @return [Boolean]
189
+ # Specifies whether each row should be separated with a separator row.
190
+ #
191
+ def separate_rows?
192
+ @separate_rows
193
+ end
194
+
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,145 @@
1
+ require 'command_kit/printing/tables/row_builder'
2
+
3
+ module CommandKit
4
+ module Printing
5
+ module Tables
6
+ #
7
+ # Builds a table and calculates it's dimensions.
8
+ #
9
+ # @api private
10
+ #
11
+ class TableBuilder
12
+
13
+ include Enumerable
14
+
15
+ # The rows within the table.
16
+ #
17
+ # @return [Array<RowBuilder>]
18
+ attr_reader :rows
19
+
20
+ # Indicates whether the table has a header row.
21
+ #
22
+ # @return [Boolean]
23
+ attr_reader :header
24
+
25
+ # The height (in lines) of the table.
26
+ #
27
+ # @return [Integer]
28
+ attr_reader :height
29
+
30
+ # The width (in characters) of the table.
31
+ #
32
+ # @return [Integer]
33
+ attr_reader :width
34
+
35
+ # The maximum number of rows in the table.
36
+ #
37
+ # @return [Integer]
38
+ attr_reader :max_rows
39
+
40
+ # The maximum number of columns in the table.
41
+ #
42
+ # @return [Integer]
43
+ attr_reader :max_columns
44
+
45
+ # The widths of the columns in the table.
46
+ #
47
+ # @return [Array<Integer>]
48
+ attr_reader :column_widths
49
+
50
+ #
51
+ # Initializes the table.
52
+ #
53
+ # @param [Array<Array>] rows
54
+ # The rows for the table.
55
+ #
56
+ # @param [Array] header
57
+ # The header row.
58
+ #
59
+ def initialize(rows=[], header: nil)
60
+ @rows = []
61
+ @height = 0
62
+ @width = 0
63
+ @column_widths = []
64
+
65
+ @max_rows = 0
66
+ @max_columns = 0
67
+
68
+ @header = !header.nil?
69
+
70
+ self << header if header
71
+ rows.each { |row| self << row }
72
+ end
73
+
74
+ #
75
+ # Determines whether the table has a header row.
76
+ #
77
+ # @return [Boolean]
78
+ # Indicates whether the first row of the table is a header row.
79
+ #
80
+ def header?
81
+ @header
82
+ end
83
+
84
+ #
85
+ # Appends a row to the table.
86
+ #
87
+ # @param [Array] row
88
+ # The row to append.
89
+ #
90
+ # @return [self]
91
+ #
92
+ def <<(row)
93
+ new_row = RowBuilder.new(row)
94
+
95
+ new_row.each_with_index do |cell,index|
96
+ column_width = @column_widths[index] || 0
97
+
98
+ if cell.width > column_width
99
+ @column_widths[index] = cell.width
100
+ end
101
+ end
102
+
103
+ @height += new_row.height
104
+ @width = new_row.width if new_row.width > @width
105
+
106
+ @max_rows += 1
107
+ @max_columns = new_row.columns if new_row.columns > @max_columns
108
+
109
+ @rows << new_row
110
+ return self
111
+ end
112
+
113
+ #
114
+ # Fetches a row from the table.
115
+ #
116
+ # @param [Integer] row_index
117
+ # The row index to fetch the row at.
118
+ #
119
+ # @return [RowBuilder, nil]
120
+ # The row or `nil` if no row exists at the given row index.
121
+ #
122
+ def [](row_index)
123
+ @rows[row_index]
124
+ end
125
+
126
+ #
127
+ # Enumerates over each row in the table.
128
+ #
129
+ # @yield [row]
130
+ # If a block is given, it will be passed each row within the table.
131
+ #
132
+ # @yieldparam [RowBuilder] row
133
+ # A row within the table.
134
+ #
135
+ # @return [Enumerator]
136
+ # If no block is given, an Enumerator will be returned.
137
+ #
138
+ def each(&block)
139
+ @rows.each(&block)
140
+ end
141
+
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ module Printing
5
+ module Tables
6
+ #
7
+ # @api private
8
+ #
9
+ class TableFormatter
10
+
11
+ #
12
+ # Initialies the table formatter.
13
+ #
14
+ # @param [TableBuilder] table
15
+ # The table data to print.
16
+ #
17
+ # @param [Style] style
18
+ # The table's style configuration.
19
+ #
20
+ def initialize(table,style)
21
+ @table = table
22
+ @style = style
23
+
24
+ @padding = ' ' * @style.padding
25
+ @extra_padding = @style.padding * 2
26
+
27
+ @last_column = @table.max_columns - 1
28
+ @last_row = @table.max_rows - 1
29
+ end
30
+
31
+ #
32
+ # Formats the table.
33
+ #
34
+ # @yield [line]
35
+ # The given block will be passed each formatted line of the table.
36
+ #
37
+ # @yieldparam [String]
38
+ # A formatted line of the table.
39
+ #
40
+ def format(&block)
41
+ yield format_top_border if @style.border
42
+
43
+ separator_row = if @table.header? || @style.separate_rows?
44
+ format_separator_row
45
+ end
46
+
47
+ @table.max_rows.times do |row_index|
48
+ row = @table[row_index]
49
+
50
+ if @table.header? && row_index == 0
51
+ format_header_row(row,&block)
52
+
53
+ yield separator_row
54
+ else
55
+ format_row(row,&block)
56
+
57
+ if @style.separate_rows? && row_index < @last_row
58
+ yield separator_row
59
+ end
60
+ end
61
+ end
62
+
63
+ yield format_bottom_border if @style.border
64
+ end
65
+
66
+ private
67
+
68
+ #
69
+ # Builds a horizontal border row.
70
+ #
71
+ # @param [String] left_border
72
+ # The left-hand side/corner border character.
73
+ #
74
+ # @param [String] column_border
75
+ # The top/bottom/horizontal column border character.
76
+ #
77
+ # @param [String] joined_border
78
+ # A joined border character.
79
+ #
80
+ # @param [String] right_border
81
+ # The right-hand side/corner border character.
82
+ #
83
+ # @return [String]
84
+ # The formatted border row.
85
+ #
86
+ def format_border_row(left_border: ,
87
+ column_border: ,
88
+ joined_border: ,
89
+ right_border: )
90
+ line = String.new
91
+ line << left_border
92
+
93
+ @table.max_columns.times do |column_index|
94
+ column_width = @table.column_widths[column_index] + @extra_padding
95
+
96
+ line << (column_border * column_width)
97
+
98
+ line << if column_index < @last_column
99
+ joined_border
100
+ else
101
+ right_border
102
+ end
103
+ end
104
+
105
+ return line
106
+ end
107
+
108
+ #
109
+ # Builds the top border row.
110
+ #
111
+ # @return [String]
112
+ # The formatted top border row.
113
+ #
114
+ def format_top_border
115
+ format_border_row(left_border: @style.border.top_left_corner,
116
+ column_border: @style.border.top_border,
117
+ joined_border: @style.border.top_joined_border,
118
+ right_border: @style.border.top_right_corner)
119
+ end
120
+
121
+ #
122
+ # Builds the separator row.
123
+ #
124
+ # @return [String]
125
+ # The formatted separator row.
126
+ #
127
+ def format_separator_row
128
+ if @style.border
129
+ format_border_row(left_border: @style.border.left_joined_border,
130
+ column_border: @style.border.horizontal_separator,
131
+ joined_border: @style.border.inner_joined_border,
132
+ right_border: @style.border.right_joined_border)
133
+ else
134
+ ''
135
+ end
136
+ end
137
+
138
+ #
139
+ # Builds the top border row.
140
+ #
141
+ # @return [String]
142
+ # The formatted bottom border row.
143
+ #
144
+ def format_bottom_border
145
+ format_border_row(left_border: @style.border.bottom_left_corner,
146
+ column_border: @style.border.bottom_border,
147
+ joined_border: @style.border.bottom_joined_border,
148
+ right_border: @style.border.bottom_right_corner)
149
+ end
150
+
151
+ #
152
+ # Formats a cell value.
153
+ #
154
+ # @param [String] value
155
+ # The value from the cell.
156
+ #
157
+ # @param [Integer] width
158
+ # The desired width of the cell.
159
+ #
160
+ # @param [:left, :right, :center] justify
161
+ # How to justify the cell's value.
162
+ #
163
+ # @return [String]
164
+ # The formatted cell.
165
+ #
166
+ def format_cell(value, width: , justify: @style.justify)
167
+ justified_value = case justify
168
+ when :center then value.center(width)
169
+ when :left then value.ljust(width)
170
+ when :right then value.rjust(width)
171
+ else
172
+ raise(ArgumentError,"invalid justify value (#{justify.inspect}), must be :left, :right, or :center")
173
+ end
174
+
175
+ return "#{@padding}#{justified_value}#{@padding}"
176
+ end
177
+
178
+ #
179
+ # Formats a specific line within a row.
180
+ #
181
+ # @param [RowBuilder] row
182
+ # The table row to format.
183
+ #
184
+ # @param [Integer] line_index
185
+ # The line index within the row to specifically format.
186
+ #
187
+ # @param [:left, :right, :center] justify
188
+ # How to justify each cell within the row.
189
+ #
190
+ # @return [String]
191
+ # The formatted row line.
192
+ #
193
+ def format_row_line(row,line_index, justify: @style.justify)
194
+ line = String.new
195
+ line << @style.border.left_border if @style.border
196
+
197
+ @table.max_columns.times do |column_index|
198
+ column_width = @table.column_widths[column_index]
199
+ cell_line = row[column_index][line_index]
200
+
201
+ line << format_cell(cell_line, width: column_width,
202
+ justify: justify)
203
+
204
+ if (@style.border && (column_index < @last_column))
205
+ line << @style.border.vertical_separator
206
+ end
207
+ end
208
+
209
+ line << @style.border.right_border if @style.border
210
+ return line
211
+ end
212
+
213
+ #
214
+ # Formats a row.
215
+ #
216
+ # @param [RowBuilder] row
217
+ # The table row to format.
218
+ #
219
+ # @param [:left, :right, :center] justify
220
+ # How to justify each cell within the row.
221
+ #
222
+ # @yield [line]
223
+ # The given block will be passed each formatted line within the row.
224
+ #
225
+ # @yieldparam [String] line
226
+ # A formatted line of the row.
227
+ #
228
+ def format_row(row, justify: @style.justify)
229
+ row.height.times do |line_index|
230
+ yield format_row_line(row,line_index, justify: justify)
231
+ end
232
+ end
233
+
234
+ #
235
+ # Formats a header row.
236
+ #
237
+ # @param [RowBuilder] row
238
+ # The header row to format.
239
+ #
240
+ # @yield [line]
241
+ # The given block will be passed each formatted line within the header
242
+ # row.
243
+ #
244
+ # @yieldparam [String] line
245
+ # A formatted line of the header row.
246
+ #
247
+ def format_header_row(row,&block)
248
+ format_row(row, justify: @style.justify_header, &block)
249
+ end
250
+
251
+ end
252
+ end
253
+ end
254
+ end