command_kit 0.3.0 → 0.4.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.
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