output_mode 1.0.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 459750a9a32c10353ffca23bcf3f9aa1bdb7da651570a324e82ba224c260df87
4
- data.tar.gz: 5a1661e01600478c767d79ad27b2b499440f3d610d4255088c7c76fff0842e28
3
+ metadata.gz: 33db72e9e5872a974df80e11a0d4bae3735c13d49d3be4d4ab1366b7e461551f
4
+ data.tar.gz: f1ac7923d6cb9beda1f3af36ebaa41b84cdb6567d5fb71db4f2d1a2503ee731b
5
5
  SHA512:
6
- metadata.gz: 4bddfb1d70102c00543a73d6d2bdd56dfcd7f248fd0ea57b222dc428bb1cb6928168592663f07c7bd5d18e22a16ae70af59dc2fbf5281de19004971cb9b26737
7
- data.tar.gz: 94d113f12f084cf82a29b4a0f1e8528c3dbd8aa2138237d4dcb5ecbf8cc24352ab25e0528f6edb23a501dc2bdcec1e729698df7bb3f643d2666f8008ff0ece85
6
+ metadata.gz: f12f87485cdad45ceb4fba9b9b26c405366009b1ad39e87c223fc246424bd0501421a75570caf48c351523d55483f4ea1d705659a3e6185ae8213c6147d9ad96
7
+ data.tar.gz: cafd82a58648d5e4900c91d242a1128b2b1c5d1123716385160801a3d85959324df691ecd7c8443e038612e4c2b2afb221745da598483e239a15a7ef9a701c0a
data/Gemfile CHANGED
@@ -28,3 +28,10 @@ source "https://rubygems.org"
28
28
 
29
29
  # Specify your gem's dependencies in output_mode.gemspec
30
30
  gemspec
31
+
32
+ # NOTE: Checks out the openflight version of tty-table with the 'rotate' flag
33
+ # fix. This should eventually become part of the mainline version of TTY::Table
34
+ #
35
+ # This is intentionally not part of the gemspec so older versions of TTY::Table
36
+ # can still be used
37
+ gem 'tty-table', github: 'openflighthpc/tty-table', branch: '9b326fcbe04968463da58c000fbb1dd5ce178243'
data/LICENSE.txt CHANGED
@@ -1,7 +1,3 @@
1
- This project is partially dual licensed under BSD-2-Clause OR 0BSD. Source code originating from a BSD-2-Clause licensed file must comply with terms contained within the license header block. Source code originating from files without a license header block maybe redistributed under either license.
2
-
3
- The following is a copy of the BSD-2-Clause license:
4
-
5
1
  Copyright 2020 William McCumstie
6
2
 
7
3
  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
@@ -11,8 +7,3 @@ Redistribution and use in source and binary forms, with or without modification,
11
7
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12
8
 
13
9
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
-
15
- The following is a copy of the 0BSD license:
16
-
17
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18
-
data/README.md CHANGED
@@ -63,5 +63,5 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Willia
63
63
 
64
64
  ## Copyright and License
65
65
 
66
- See [LICENSE](LICENSE.txt) for dual licensing details.
66
+ See [LICENSE](LICENSE.txt) for licensing details.
67
67
 
data/bin/demo CHANGED
@@ -1,22 +1,57 @@
1
1
  #!/usr/bin/env ruby
2
2
  #==============================================================================
3
- # Refer to LICENSE.txt for licensing terms
3
+ # Copyright 2020 William McCumstie
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
4
26
  #==============================================================================
5
27
 
6
-
7
28
  require "bundler/setup"
8
29
  require "output_mode"
30
+ require 'erb'
9
31
 
10
32
  module DemoIndex
11
33
  extend OutputMode::TLDR::Index
12
34
 
13
- register_callable(header: 'Integer') { |i| i }
14
- register_callable(header: 'Standard') { 'always visible' }
35
+ register_callable(header: 'Integer', row_color: [:yellow, :bold]) { |i| i }
36
+ register_callable(header: 'Standard', header_color: [:strikethrough] ) { 'always visible' }
15
37
  register_callable(header: 'Verbose', verbose: true) { 'verbose visible' }
16
38
  register_callable(header: 'Simplified', verbose: false) { 'simplified visible' }
39
+ register_callable(header: 'Interactive', interactive: true) { 'interactive visible' }
40
+ register_callable(header: 'Non Interactive', interactive: false) { 'non-interactive visible' }
17
41
  register_callable(header: 'Yes/True') { true }
18
- register_callable(header: 'No/False') { false }
42
+ register_callable(header: 'No/False', row_color: [:clear]) { false }
19
43
  register_callable(header: 'Missing') { nil }
44
+ register_callable(header: 'Inline') do |interactive:, verbose:|
45
+ if interactive && verbose
46
+ 'interactive-verbose'
47
+ elsif interactive
48
+ 'interactive-simplified'
49
+ elsif verbose
50
+ 'non-interactive-verbose'
51
+ else
52
+ 'non-interactive-simplified'
53
+ end
54
+ end
20
55
  end
21
56
 
22
57
  module DemoShow
@@ -26,14 +61,42 @@ module DemoShow
26
61
  register_callable(header: 'Standard') { 'always visible' }
27
62
  register_callable(header: 'Verbose', verbose: true) { 'verbose visible' }
28
63
  register_callable(header: 'Simplified', verbose: false) { 'simplified visible' }
29
- register_callable(header: 'Yes/True') { true }
30
- register_callable(header: 'No/False') { false }
64
+ register_callable(header: 'Interactive', interactive: true) { 'interactive visible' }
65
+ register_callable(header: 'Non Interactive', interactive: false) { 'non-interactive visible' }
66
+ register_callable(header: 'Yes/True', section: :boolean) { true }
67
+ register_callable(header: 'No/False', section: :boolean) { false }
31
68
  register_callable(header: 'Missing') { nil }
69
+ register_callable(header: 'Tab') { "tab1\ttab2" }
70
+ register_callable(header: 'New line') { "line1\nline2" }
32
71
  end
33
72
 
34
73
  data = [1, 2, 3]
35
74
 
75
+ other_template = ERB.new(<<~TEMPLATE, nil, '-')
76
+ # Non boolean values
77
+ <% each(:other) do |value, field:, padding:, **_| -%>
78
+ <%= padding -%><%= pastel.blue.bold field -%><%= pastel.bold ':' -%> <%= pastel.green value %>
79
+ <% end -%>
80
+
81
+ # Boolean Values
82
+ <% each(:boolean) do |value, field:, padding:, **_| -%>
83
+ <%= padding -%><%= pastel.blue.bold field -%><%= pastel.bold ':' -%> <%= pastel.green value %>
84
+ <% end -%>
85
+ TEMPLATE
86
+
36
87
  puts <<~EOF
88
+ #==============================================================================
89
+ #==============================================================================
90
+ # INDEX OUTPUTS
91
+ #==============================================================================
92
+ #==============================================================================
93
+
94
+ #==============================================================================
95
+ # Default Demo Index
96
+ # Simplified in interactive shells, verbose in non-interactive
97
+ #==============================================================================
98
+ #{DemoIndex.build_output.render(*data)}
99
+
37
100
  #==============================================================================
38
101
  # Demo Verbose Index
39
102
  #==============================================================================
@@ -41,10 +104,38 @@ puts <<~EOF
41
104
 
42
105
  #==============================================================================
43
106
  # Demo "Simplified" Index
44
- # NOTE: Disabled for non-interactive shell, shows the verbose output instead
45
107
  #==============================================================================
46
108
  #{DemoIndex.build_output(verbose: false).render(*data)}
47
109
 
110
+ #==============================================================================
111
+ # Force Interactive
112
+ # Always print as if the shell is interactive
113
+ #==============================================================================
114
+ #{DemoIndex.build_output(interactive: true).render(*data)}
115
+
116
+ #==============================================================================
117
+ # Force Non-Interactive
118
+ # Always print as if the shell is non-interactive
119
+ #==============================================================================
120
+ #{DemoIndex.build_output(interactive: false).render(*data)}
121
+
122
+ #==============================================================================
123
+ # Demo ASCII Index
124
+ #==============================================================================
125
+ #{DemoIndex.build_output(ascii: true).render(*data)}
126
+
127
+ #==============================================================================
128
+ #==============================================================================
129
+ # SHOW OUTPUTS
130
+ #==============================================================================
131
+ #==============================================================================
132
+
133
+ #==============================================================================
134
+ # Default Settings
135
+ # Simplified in interactive shells, verbose in non-interactive
136
+ #==============================================================================
137
+ #{DemoShow.build_output.render(*data)}
138
+
48
139
  #==============================================================================
49
140
  # Demo Verbose Show
50
141
  #==============================================================================
@@ -52,9 +143,30 @@ puts <<~EOF
52
143
 
53
144
  #==============================================================================
54
145
  # Demo "Simplified" Show
55
- # NOTE: Disabled for non-interactive shell, shows the verbose output instead
56
146
  #==============================================================================
57
147
  #{DemoShow.build_output(verbose: false).render(*data)}
58
- EOF
59
148
 
149
+ #==============================================================================
150
+ # Force Interactive
151
+ # Always print as if the shell is interactive
152
+ #==============================================================================
153
+ #{DemoShow.build_output(interactive: true).render(*data)}
154
+
155
+ #==============================================================================
156
+ # Force Non-Interactive
157
+ # Always print as if the shell is non-interactive
158
+ #==============================================================================
159
+ #{DemoShow.build_output(interactive: false).render(*data)}
160
+
161
+ #==============================================================================
162
+ # Demo ASCII Index
163
+ #==============================================================================
164
+ #{DemoShow.build_output(ascii: true).render(*data)}
60
165
 
166
+ #==============================================================================
167
+ # Group the boolean value separately
168
+ # NOTE: This only occurs in interactive mode
169
+ # Non-Interactive sessions have a fix order
170
+ #==============================================================================
171
+ #{DemoShow.build_output(template: other_template).render(*data)}
172
+ EOF
@@ -36,7 +36,9 @@ module OutputMode
36
36
  # @param config Directly provided to {OutputMode::Callable#initialize}
37
37
  # @yield Directly provided to {OutputMode::Callable#initialize}
38
38
  def register_callable(**config, &b)
39
- output_callables << Callable.new(**config, &b)
39
+ Callable.new(**config, &b).tap do |c|
40
+ output_callables << c
41
+ end
40
42
  end
41
43
 
42
44
  # Provides the base method signature
@@ -26,16 +26,17 @@
26
26
 
27
27
  module OutputMode
28
28
  # Internal array like object that will convert procs to Callable
29
- class Callables < Array
29
+ class Callables
30
+ include Enumerable
31
+
30
32
  # @api private
31
33
  def initialize(callables = nil)
34
+ @callables = []
32
35
  case callables
33
- when Array
34
- super().tap do |all|
35
- callables.each { |c| all << c }
36
- end
36
+ when Array, Callables
37
+ callables.each { |c| @callables << c }
37
38
  when nil
38
- super()
39
+ # NOOP
39
40
  else
40
41
  raise "Can not convert #{callables.class} into a #{self.class}"
41
42
  end
@@ -43,13 +44,45 @@ module OutputMode
43
44
 
44
45
  def <<(item)
45
46
  if item.is_a? Callable
46
- super
47
+ @callables << item
47
48
  elsif item.respond_to?(:call)
48
- super(Callable.new(&item))
49
+ @callables << Callable.new(&item)
49
50
  else
50
51
  raise Error, "#{item.class} is not callable"
51
52
  end
52
53
  end
54
+
55
+ def each(&block)
56
+ @callables.each(&block)
57
+ end
58
+
59
+ def pad_each(key = :header)
60
+ max_length = self.map { |c| c.config[key].to_s.length }
61
+ .max
62
+
63
+ pads = self.map do |callable|
64
+ length = max_length - callable.config[key].to_s.length
65
+ [callable, { padding: ' ' * length }]
66
+ end
67
+
68
+ if block_given?
69
+ pads.each { |*args| yield(*args) }
70
+ else
71
+ pads.each
72
+ end
73
+ end
74
+
75
+ def config_select(key, *values)
76
+ selected = self.select do |callable|
77
+ conf = callable.config[key]
78
+ if conf.is_a? Array
79
+ !(conf & values).empty?
80
+ else
81
+ values.include?(conf)
82
+ end
83
+ end
84
+ Callables.new(selected)
85
+ end
53
86
  end
54
87
 
55
88
  class Callable
@@ -83,28 +116,36 @@ module OutputMode
83
116
 
84
117
  # Handles the dynamic +<query>?+ and +<explicit-negation>!+ methods
85
118
  #
86
- # The +<query>?+ methods check if the mode has been set on the object. If
87
- # +query+ is a defined mode, then the value is directly pulled from #{modes}.
88
- # Undefined modes will return +false+.
89
- #
90
- # The +<explicit-negation>!+ methods are similar to queries, but undefined modes
91
- # will return +true+. This means +<explicit-negation>!+ methods only return +false+
92
- # if the +explicit-negation+ mode has been set to +false+ in {#modes}.
93
- #
94
119
  # @return [Boolean] The result of the query or explicit-negation
95
- # @raises [NoMethodError] All other method calls
96
- def method_missing(s, *a, &b)
120
+ # @raise [NoMethodError] All other method calls
121
+ def method_missing(s, *args, &b)
97
122
  mode = s[0..-2].to_sym
98
123
  case method_char(s)
99
124
  when '?'
100
- modes.fetch(mode, false)
125
+ ifnone = (args.length > 0 ? args.first : false)
126
+ modes.fetch(mode, ifnone)
101
127
  when '!'
102
- modes.fetch(mode, true)
128
+ send(:"#{mode}?", true)
103
129
  else
104
130
  super
105
131
  end
106
132
  end
107
133
 
134
+ # @!method mode?(ifnone = false)
135
+ # This is a dynamic method for check if an arbitrary +mode+ has been set. It will
136
+ # return the associated value if the +mode+ has been defined in {#modes}.
137
+ #
138
+ # Otherwise it will return the +ifnone+ value
139
+ # @return [Boolean] the associated value if +mode+ has been defined
140
+ # @return otherwise return the +ifnone+ value
141
+ #
142
+ # @!method mode!
143
+ # Older syntax that returns +true+ if the +mode+ has not been defined. Otherwise
144
+ # the same as {#mode?}
145
+ #
146
+ # @return [Boolean]
147
+ # @deprecated Please use the newer +mode?(true)+ syntax
148
+
108
149
  # Responds +true+ for valid dynamic methods
109
150
  # @param [Symbol] s The method to be tested
110
151
  # @return [Boolean] The truthiness of the underlining call to {#method_char}
@@ -133,6 +174,29 @@ module OutputMode
133
174
  def call(*a)
134
175
  callable.call(*a)
135
176
  end
177
+
178
+ def generator(output)
179
+ ->(*a) do
180
+ # Implicitly determine which parts of the context can be passed through
181
+ ctx = if callable.parameters.any? { |type, _| type == :keyrest }
182
+ output.context
183
+ else
184
+ keys = callable.parameters.select { |type, _| [:key, :keyreq].include?(type) }
185
+ .map { |_, k| k }
186
+ output.context.slice(*keys)
187
+ end
188
+ raw = call(*a, **ctx)
189
+ if raw == true
190
+ config[:yes] || output.yes
191
+ elsif raw == false
192
+ config[:no] || output.no
193
+ elsif [nil, ''].include?(raw)
194
+ config[:default] || output.default
195
+ else
196
+ raw
197
+ end
198
+ end
199
+ end
136
200
  end
137
201
  end
138
202
 
@@ -36,45 +36,41 @@ module OutputMode
36
36
  # @!attribute [r] config
37
37
  # @return [Hash] additional key-values to modify the render
38
38
  # @!attribute [r] default
39
- # @return either a static default or a column based array of defaults
39
+ # @return either a static default
40
40
  # @!attribute [r] yes
41
- # @return either a static yes value or a column based array of values
41
+ # @return either a static yes value
42
42
  # @!attribute [r] no
43
- # @return either a static no value or a column based array of values
44
- attr_reader :procs, :config, :yes, :no, :default
43
+ # @return either a static no value
44
+ # @!attribute [r] context
45
+ # @return a hash of keys to be provided to the callables
46
+ attr_reader :procs, :config, :yes, :no, :default, :context
45
47
 
46
48
  # Creates a new outputting instance from an array of procs
47
49
  #
48
50
  # @param *procs [Array<#call>] an array of procs (or callable objects)
49
51
  # @param default: [String] replaces _blanks_ with a static string
50
- # @param default: [Array] replace _blanks_ on a per column basis. The last value is repeated if the +procs+ are longer.
51
52
  # @param yes: [String] replaces +true+ with a static string
52
- # @param yes: [Array] replaces +true+ on a per column basis. The last value is repeated if the +procs+ are longer.
53
53
  # @param no: [String] replaces +false+ with a static string
54
- # @param no: [Array] replaces +false+ on a per column basis. The last value is repeated if the +procs+ are longer.
54
+ # @param context: [Hash] of keys to be provided to the callables
55
55
  # @param **config [Hash] a hash of additional keys to be stored
56
- def initialize(*procs, default: nil, yes: 'true', no: 'false', **config)
57
- @procs = procs
56
+ def initialize(*procs, default: nil, yes: 'true', no: 'false', context: {}, **config)
57
+ @procs = Callables.new(procs)
58
58
  @config = config
59
59
  @yes = yes
60
60
  @no = no
61
61
  @default = default
62
+ @context = context
63
+ end
64
+
65
+ def callables
66
+ procs
62
67
  end
63
68
 
64
69
  # Returns the results of the +procs+ for a particular +object+. It will apply the
65
70
  # +default+, +yes+, and +no+ values.
66
71
  def generate(object)
67
- procs.each_with_index.map do |p, idx|
68
- raw = p.call(object)
69
- if raw == true
70
- index_selector(:yes, idx)
71
- elsif raw == false
72
- index_selector(:no, idx)
73
- elsif !default.nil? && (raw.nil? || raw == '')
74
- index_selector(:default, idx)
75
- else
76
- raw
77
- end
72
+ procs.map do |callable|
73
+ callable.generator(self).call(object)
78
74
  end
79
75
  end
80
76
 
@@ -91,37 +87,5 @@ module OutputMode
91
87
  def render(*data)
92
88
  raise NotImplementedError
93
89
  end
94
-
95
- # A helper method for selecting elements from a source array or return
96
- # a static value.
97
- #
98
- # @param [Symbol] method The source method on the +output+
99
- # @param [Integer] index The index to lookup
100
- #
101
- # @overload index_selector(array_method, valid_index)
102
- # @param array_method A method that returns an array
103
- # @param valid_index An index that is less than the array's length
104
- # @return the value at the index
105
- #
106
- # @overload index_selector(array_method, out_of_bounds)
107
- # @param array_method A method that returns an array
108
- # @param out_of_bounds An index greater than the maximum array length
109
- # @return the last element of the array
110
- #
111
- # @overload index_selector(non_array_method, _)
112
- # @param non_array_method A method that does not return an array
113
- # @param _ The index is ignored
114
- # @return the result of the non_array_method
115
- def index_selector(method, index)
116
- source = public_send(method)
117
- is_array = source.is_a? Array
118
- if is_array && source.length > index
119
- source[index]
120
- elsif is_array
121
- source.last
122
- else
123
- source
124
- end
125
- end
126
90
  end
127
91
  end
@@ -40,7 +40,12 @@ module OutputMode
40
40
  def render(*data)
41
41
  io = StringIO.new
42
42
  csv = CSV.new(io, **config)
43
- data.each { |d| csv << generate(d) }
43
+ data.each do |datum|
44
+ csv << generate(datum).map do |value|
45
+ next nil if value.nil?
46
+ value.to_s.dump[1...-1]
47
+ end
48
+ end
44
49
  io.tap(&:rewind).read
45
50
  end
46
51
  end
@@ -29,33 +29,40 @@ require 'tty-table'
29
29
  module OutputMode
30
30
  module Outputs
31
31
  class Tabulated < Output
32
- attr_reader :renderer, :header, :default, :block, :yes, :no
33
-
34
32
  # @!attribute [r] renderer
35
33
  # @return [Symbol] the renderer type, see: https://github.com/piotrmurach/tty-table#32-renderer
36
- # @!attribute [r] header
37
- # @return [Array] An optional header row for the table
38
34
  # @!attribute [r] block
39
35
  # @return [#call] an optional block of code that configures the renderer
36
+ # @!attribute [r] colorize
37
+ # @return [Boolean] enable or disabled the colorization
38
+ # @!attribute [r] header_color
39
+ # @return An optional header color or array of colors
40
+ # @!attribute [r] row_color
41
+ # @return An optional data color or array of colors
42
+ attr_reader :renderer, :default, :block, :yes, :no,
43
+ :header_color, :row_color, :colorize
40
44
 
41
45
  # @return [Hash] additional options to +TTY::Table+ renderer
42
46
  # @see https://github.com/piotrmurach/tty-table#33-options
43
47
  def config; super; end
44
48
 
45
- # @overload initialize(*procs, renderer: nil, header: nil, **config)
49
+ # @overload initialize(*procs, renderer: nil, **config)
46
50
  # @param [Array] *procs see {OutputMode::Outputs::Base#initialize}
47
51
  # @param [Symbol] :renderer override the default renderer
48
- # @param [Array<String>] :header the header row of the table
49
52
  # @param [Hash] **config additional options to the renderer
50
53
  # @yieldparam tty_table_renderer [TTY::Table::Renderer::Base] optional access the underlining TTY::Table renderer
51
54
  def initialize(*procs,
52
55
  renderer: :unicode,
53
- header: nil,
56
+ colorize: false,
57
+ header_color: nil,
58
+ row_color: nil,
54
59
  **config,
55
60
  &block)
56
- @header = header
57
61
  @renderer = renderer
58
62
  @block = block
63
+ @header_color = header_color
64
+ @row_color = row_color
65
+ @colorize = colorize
59
66
  super(*procs, **config)
60
67
  end
61
68
 
@@ -63,10 +70,48 @@ module OutputMode
63
70
  # @see OutputMode::Outputs::Base#render
64
71
  # @see https://github.com/piotrmurach/tty-table
65
72
  def render(*data)
66
- table = TTY::Table.new header: header
67
- data.each { |d| table << generate(d) }
73
+ table = TTY::Table.new header: processed_header
74
+ data.each { |d| table << process_row(d) }
68
75
  table.render(renderer, **config, &block) || ''
69
76
  end
77
+
78
+ private
79
+
80
+ # Colorizes the header when requested
81
+ def processed_header
82
+ callables.map do |callable|
83
+ header = callable.config.fetch(:header, '')
84
+ color = callable.config.fetch(:header_color) || header_color
85
+ case color
86
+ when nil
87
+ header.to_s
88
+ when Array
89
+ pastel.decorate(header.to_s, *color)
90
+ else
91
+ pastel.decorate(header.to_s, color)
92
+ end
93
+ end
94
+ end
95
+
96
+ # Colorizes the row when requested
97
+ def process_row(model)
98
+ callables.map do |callable|
99
+ d = callable.generator(self).call(model)
100
+ color = callable.config[:row_color] || row_color
101
+ case color
102
+ when NilClass
103
+ d.to_s
104
+ when Array
105
+ pastel.decorate(d.to_s, *color)
106
+ else
107
+ pastel.decorate(d.to_s, color)
108
+ end
109
+ end
110
+ end
111
+
112
+ def pastel
113
+ @pastel ||= Pastel::Color.new(enabled: colorize)
114
+ end
70
115
  end
71
116
  end
72
117
  end
@@ -38,13 +38,25 @@ module OutputMode
38
38
  # @yieldparam field: An optional field header for the value
39
39
  # @yieldparam padding: A padding string which will right align the +field+
40
40
  # @yieldparam **config TBA
41
- def each
42
- max = output.max_field_length
43
- output.generate(model).each_with_index do |value, idx|
44
- field = output.index_selector(:fields, idx)
45
- padding = ' ' * (max - field.to_s.length)
46
- yield(value, field: field, padding: padding)
41
+ def each(section = nil, &block)
42
+ # Select the callable objects
43
+ callables = if section == nil
44
+ output.callables
45
+ elsif section == :default
46
+ output.callables.config_select(:section, :default, nil)
47
+ else
48
+ output.callables.config_select(:section, section)
49
+ end
50
+
51
+ # Yield each selected attribute
52
+ objs = callables.pad_each.map do |callable, padding:|
53
+ value = callable.generator(output).call(model)
54
+ field = callable.config[:header]
55
+ [value, {field: field, padding: padding }]
47
56
  end
57
+
58
+ # Runs the provided block
59
+ objs.each(&block)
48
60
  end
49
61
 
50
62
  # Renders an ERB object within the entry's context. This provides access to the
@@ -61,6 +73,12 @@ module OutputMode
61
73
  def pastel
62
74
  @pastel ||= Pastel.new(enabled: colorize)
63
75
  end
76
+
77
+ private
78
+
79
+ def generated
80
+ @generated ||= output.generate(model)
81
+ end
64
82
  end
65
83
 
66
84
  # @!attribute [r] erb
@@ -68,7 +86,8 @@ module OutputMode
68
86
  # @!attribute [r] separator
69
87
  # @!attribute [r] fields
70
88
  # @!attribute [r] colorize
71
- attr_reader :erb, :fields, :separator, :colorize
89
+ # @!attribute [r] sections
90
+ attr_reader :erb, :fields, :separator, :colorize, :sections
72
91
 
73
92
  # Create a new +output+ which will render using +ERB+. The provided +template+ should
74
93
  # only render the +output+ for a single +entry+ (aka model, record, data object, etc).
@@ -84,23 +103,32 @@ module OutputMode
84
103
  #
85
104
  # @overload initialize(*procs, template: nil, fields: nil, seperator: "\n", yes: 'true', no: 'false', **config)
86
105
  # @param [Array] *procs see {OutputMode::Output#initialize}
87
- # @param [String] template: A string to be converted into +ERB+
88
106
  # @param [ERB] template: The +template+ object used by the renderer
89
107
  # @param [Array] fields: An optional array of field headers that map to the procs, repeating the last value if required
90
108
  # @param fields: A static value to use as all field headers
91
109
  # @param separator: The character(s) used to join the "entries" together
92
110
  # @param colorize: Flags if the caller wants the colorized version, this maybe ignored by +template+
111
+ # @param sections: An optional array that groups the procs into sections. This is ignored by default
93
112
  # @param [Hash] **config see {OutputMode::Output#initialize}
94
113
  def initialize(*procs,
95
114
  template: nil,
96
115
  fields: nil,
97
116
  separator: "\n",
98
117
  colorize: false,
118
+ sections: nil,
99
119
  **config)
100
- @erb = DEFAULT_ERB
120
+ @erb = case template
121
+ when String
122
+ ERB.new(template, nil, '-')
123
+ when ERB
124
+ template
125
+ else
126
+ DEFAULT_ERB
127
+ end
101
128
  @fields = fields
102
129
  @separator = separator
103
130
  @colorize = colorize
131
+ @sections = sections
104
132
  super(*procs, **config)
105
133
  end
106
134
 
@@ -1,7 +1,31 @@
1
1
  #==============================================================================
2
- # Refer to LICENSE.txt for licensing terms
2
+ # Copyright 2020 William McCumstie
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # 1. Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24
+ # POSSIBILITY OF SUCH DAMAGE.
3
25
  #==============================================================================
4
26
 
27
+ require 'tty-color'
28
+
5
29
  module OutputMode
6
30
  module TLDR
7
31
  module Index
@@ -11,15 +35,31 @@ module OutputMode
11
35
  # @overload register_callable(header:, verbose: true)
12
36
  # @param header: The column's header field when displaying to humans
13
37
  # @param verbose: Whether the column will be shown in the verbose output
38
+ # @param interactive: Whether the field will be show in the interactive output
39
+ # @param header_color: Override the default color for the header
40
+ # @param row_color: Override the default color for the row
41
+ # @param modes: Additional modes flags for the callable
14
42
  # @yieldparam model The subject the column is describing, some sort of data model
15
- def register_callable(header:, verbose: nil, &b)
16
- super(modes: { verbose: verbose }, header: header, &b)
43
+ def register_callable(modes: {}, header:, verbose: nil, interactive: nil, header_color: nil, row_color: nil, &b)
44
+ modes = modes.map { |m| [m, true] }.to_h if modes.is_a? Array
45
+ super(modes: modes.merge(verbose: verbose, interactive: interactive),
46
+ header: header,
47
+ header_color: header_color,
48
+ row_color: row_color,
49
+ &b)
17
50
  end
51
+ alias_method :register_column, :register_callable
18
52
 
19
53
  # Creates an new +output+ from the verbosity flag. This method only uses
20
54
  # +$stdout+ as part of it's output class discovery logic. It does not
21
55
  # print to the output directly
22
56
  #
57
+ # The +ascii+ flag disables the unicode formatting in interactive shells.
58
+ # Non interactive shells use ASCII by default.
59
+ #
60
+ # The +verbose+ flag toggles the simplified and verbose outputs in the
61
+ # interactive output. Non-interactive outputs are always verbose
62
+ #
23
63
  # If +$stdout+ is an interactive shell (aka a TTY), then it will display using
24
64
  # {OutputMode::Outputs::Tabulated}. This is intended for human consumption
25
65
  # and will obey the provided +verbose+ flag.
@@ -28,26 +68,57 @@ module OutputMode
28
68
  # {OutputMode::Outputs::Delimited} using tab delimiters. This is intended
29
69
  # for consumption by machines. This output ignores the provided +verbose+
30
70
  # flag as it is always verbose.
31
- def build_output(verbose: false)
32
- callables = if verbose || !$stdout.tty?
71
+ #
72
+ # An interative/ non-interactive output can be forced by setting the
73
+ # +interactive+ flag to +true+/+false+ respectively
74
+ def build_output(verbose: nil, ascii: nil, interactive: nil, header_color: [:blue, :bold], row_color: :green, context: {})
75
+ # Set the interactive and verbose flags if not provided
76
+ interactive = $stdout.tty? if interactive.nil?
77
+ verbose = !interactive if verbose.nil?
78
+ ascii = !interactive if ascii.nil?
79
+
80
+ # Update the rendering context with the verbosity/interactive settings
81
+ context = context.merge(interactive: interactive, verbose: verbose, ascii: ascii)
82
+
83
+ callables = if verbose
33
84
  # Filter out columns that are explicitly not verbose
34
- output_callables.select(&:verbose!)
85
+ output_callables.select { |o| o.verbose?(true) }
35
86
  else
36
87
  # Filter out columns that are explicitly verbose
37
88
  output_callables.reject(&:verbose?)
38
89
  end
39
90
 
40
- if $stdout.tty?
91
+ callables = if interactive
92
+ # Filter out columns that are explicitly not interactive
93
+ callables.select { |o| o.interactive?(true) }
94
+ else
95
+ # Filter out columns that are explicitly interactive
96
+ callables.reject { |o| o.interactive? }
97
+ end
98
+
99
+ if interactive
41
100
  # Creates the human readable output
101
+ opts = if ascii
102
+ { yes: 'yes', no: 'no', renderer: :ascii }
103
+ else
104
+ {
105
+ yes: '✓', no: '✕', renderer: :unicode, colorize: TTY::Color.color?,
106
+ header_color: header_color,
107
+ row_color: row_color
108
+ }
109
+ end
110
+
42
111
  Outputs::Tabulated.new(*callables,
43
- header: callables.map { |c| c.config.fetch(:header, 'missing') },
44
- renderer: :unicode,
112
+ rotate: false,
45
113
  padding: [0,1],
46
114
  default: '(none)',
47
- yes: '✓', no: '✕')
115
+ context: context,
116
+ **opts
117
+ )
48
118
  else
49
119
  # Creates the machine readable output
50
- Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'y', no: 'n', default: '')
120
+ Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'yes', no: 'no', default: nil,
121
+ context: context)
51
122
  end
52
123
  end
53
124
  end
@@ -1,5 +1,27 @@
1
1
  #==============================================================================
2
- # Refer to LICENSE.txt for licensing terms
2
+ # Copyright 2020 William McCumstie
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # 1. Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24
+ # POSSIBILITY OF SUCH DAMAGE.
3
25
  #==============================================================================
4
26
 
5
27
  require 'tty-color'
@@ -13,15 +35,29 @@ module OutputMode
13
35
  # @overload register_callable(header:, verbose: true)
14
36
  # @param header: The human readable key to the field, uses the term 'header' for consistency
15
37
  # @param verbose: Whether the field will be shown in the verbose output
38
+ # @param interactive: Whether the field will be show in the interactive output
39
+ # @param section: Define the grouping a callable belongs to. Ignored by default
40
+ # @param modes: Additional modes flags for the callable
16
41
  # @yieldparam model The subject the column is describing, some sort of data model
17
- def register_callable(header:, verbose: nil, &b)
18
- super(modes: { verbose: verbose }, header: header, &b)
42
+ def register_callable(modes: {}, header:, verbose: nil, interactive: nil, section: :other, &b)
43
+ modes = modes.map { |m| [m, true] }.to_h if modes.is_a? Array
44
+ super(modes: modes.merge(verbose: verbose, interactive: interactive),
45
+ header: header,
46
+ section: section,
47
+ &b)
19
48
  end
49
+ alias_method :register_attribute, :register_callable
20
50
 
21
51
  # Creates an new +output+ from the verbosity flag. This method only uses
22
52
  # +$stdout+ as part of it's output class discovery logic. It does not
23
53
  # print to the io directly
24
54
  #
55
+ # The +ascii+ flag disables the unicode formatting in interactive shells.
56
+ # Non interactive shells use ASCII by default.
57
+ #
58
+ # The +verbose+ flag toggles the simplified and verbose outputs in the
59
+ # interactive output. Non-interactive outputs are always verbose
60
+ #
25
61
  # If +$stdout+ is an interactive shell (aka a TTY), then it will display using
26
62
  # {OutputMode::Outputs::Templated}. This is intended for human consumption
27
63
  # and will obey the provided +verbose+ flag.
@@ -30,25 +66,57 @@ module OutputMode
30
66
  # {OutputMode::Outputs::Delimited} using tab delimiters. This is intended
31
67
  # for consumption by machines. This output ignores the provided +verbose+
32
68
  # flag as it is always verbose.
33
- def build_output(verbose: false)
34
- callables = if verbose || !$stdout.tty?
69
+ #
70
+ # The +template+ overrides the default erb template for the output
71
+ #
72
+ # An interative/ non-interactive output can be forced by setting the
73
+ # +interactive+ flag to +true+/+false+ respectively
74
+ def build_output(verbose: nil, ascii: nil, interactive: nil, template: nil, context: {})
75
+ # Set the interactive and verbose flags if not provided
76
+ interactive = $stdout.tty? if interactive.nil?
77
+ verbose = !interactive if verbose.nil?
78
+ ascii = !interactive if ascii.nil?
79
+
80
+ # Update the rendering context with the verbosity/interactive settings
81
+ context = context.merge(interactive: interactive, verbose: verbose, ascii: ascii)
82
+
83
+ callables = if verbose
35
84
  # Filter out columns that are explicitly not verbose
36
- output_callables.select(&:verbose!)
85
+ output_callables.select { |o| o.verbose?(true) }
37
86
  else
38
87
  # Filter out columns that are explicitly verbose
39
88
  output_callables.reject(&:verbose?)
40
89
  end
41
90
 
42
- if $stdout.tty?
91
+ callables = if interactive
92
+ # Filter out columns that are explicitly not interactive
93
+ callables.select { |o| o.interactive?(true) }
94
+ else
95
+ # Filter out columns that are explicitly interactive
96
+ callables.reject { |o| o.interactive? }
97
+ end
98
+
99
+ if interactive
43
100
  # Creates the human readable output
101
+ opts = if ascii
102
+ { yes: 'yes', no: 'no', colorize: false }
103
+ else
104
+ { yes: '✓', no: '✕', colorize: TTY::Color.color? }
105
+ end
106
+
107
+ sections = callables.map { |o| o.config[:section] }
108
+
44
109
  Outputs::Templated.new(*callables,
45
110
  fields: callables.map { |c| c.config.fetch(:header, 'missing') },
46
- colorize: TTY::Color.color?,
47
111
  default: '(none)',
48
- yes: '✓', no: '✕')
112
+ sections: sections,
113
+ template: template,
114
+ context: context,
115
+ **opts)
49
116
  else
50
117
  # Creates the machine readable output
51
- Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'y', no: 'n', default: '')
118
+ Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'yes', no: 'no', default: nil,
119
+ context: context)
52
120
  end
53
121
  end
54
122
  end
@@ -25,5 +25,5 @@
25
25
  #==============================================================================
26
26
 
27
27
  module OutputMode
28
- VERSION = "1.0.0"
28
+ VERSION = "1.5.0"
29
29
  end
data/output_mode.gemspec CHANGED
@@ -31,6 +31,7 @@ require "output_mode/version"
31
31
  Gem::Specification.new do |spec|
32
32
  spec.name = "output_mode"
33
33
  spec.version = OutputMode::VERSION
34
+ spec.licenses = ['BSD-2-Clause']
34
35
  spec.authors = ["William McCumsite"]
35
36
  spec.email = ["openlicense.williams@gmail.com"]
36
37
 
@@ -48,9 +49,9 @@ Gem::Specification.new do |spec|
48
49
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
49
50
  spec.require_paths = ["lib"]
50
51
 
51
- spec.add_runtime_dependency 'tty-table', '~> 0.11'
52
- spec.add_runtime_dependency 'pastel', '~> 0.7'
53
- spec.add_runtime_dependency 'tty-color', '~> 0.5'
52
+ spec.add_runtime_dependency 'tty-table', '>= 0.11'
53
+ spec.add_runtime_dependency 'pastel', '>= 0.7'
54
+ spec.add_runtime_dependency 'tty-color', '>= 0.5'
54
55
 
55
56
  spec.add_development_dependency "bundler", "~> 2.0"
56
57
  spec.add_development_dependency "rake", ">= 12.3.3"
metadata CHANGED
@@ -1,55 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: output_mode
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William McCumsite
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-30 00:00:00.000000000 Z
11
+ date: 2021-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-table
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0.11'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.11'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pastel
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0.7'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.7'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: tty-color
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0.5'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.5'
55
55
  - !ruby/object:Gem::Dependency
@@ -141,7 +141,8 @@ files:
141
141
  - lib/output_mode/version.rb
142
142
  - output_mode.gemspec
143
143
  homepage: https://github.com/WilliamMcCumstie/output_mode
144
- licenses: []
144
+ licenses:
145
+ - BSD-2-Clause
145
146
  metadata:
146
147
  homepage_uri: https://github.com/WilliamMcCumstie/output_mode
147
148
  post_install_message: