natty-ui 0.5.3 → 0.7.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.
data/lib/natty-ui/ansi.rb CHANGED
@@ -60,17 +60,23 @@ module NattyUI
60
60
  def cursor_left(columns = nil) = "\e[#{columns}D"
61
61
 
62
62
  # @param lines [Integer] number of lines
63
- # @return [String] ANSI code to move the cursor to beginning of the line some lines down
63
+ # @return [String] ANSI code to move the cursor to beginning of the line
64
+ # some lines down
64
65
  def cursor_line_down(lines = nil) = "\e[#{lines}E"
65
66
 
66
67
  # @param lines [Integer] number of lines
67
- # @return [String] ANSI code to move the cursor to beginning of the line some lines up
68
+ # @return [String] ANSI code to move the cursor to beginning of the line
69
+ # some lines up
68
70
  def cursor_line_up(lines = nil) = "\e[#{lines}F"
69
71
 
70
72
  # @param columns [Integer] number of columns
71
- # @return [String] ANSI code to move the cursor to giben column
73
+ # @return [String] ANSI code to move the cursor to given column
72
74
  def cursor_column(columns = nil) = "\e[#{columns}G"
73
75
 
76
+ # @return [String] ANSI code positioning the cursor on right hand side of
77
+ # the terminal
78
+ def cursor_right_aligned = "\e[9999G\e[D\e[C"
79
+
74
80
  # @return [String] ANSI code to hide the cursor
75
81
  def cursor_hide = "\e[?25l"
76
82
 
@@ -107,6 +113,14 @@ module NattyUI
107
113
  attributes.empty? ? "#{obj}" : "#{attributes}#{obj}#{"\e[0m" if reset}"
108
114
  end
109
115
 
116
+ # Remove ANSI attribtes from given string.
117
+ #
118
+ # @see embellish
119
+ #
120
+ # @param str [#to_s] string to be modified
121
+ # @return [String] string without ANSI attributes
122
+ def blemish(str) = str.to_s.gsub(/(\x1b\[(?~[a-zA-Z])[a-zA-Z])/, '')
123
+
110
124
  # Combine given ANSI `attributes`.
111
125
  #
112
126
  # ANSI attribute names are:
@@ -119,55 +133,57 @@ module NattyUI
119
133
  # `fraktur_off`, `underline_off`, `blink_off`, `proportional`, `spacing`,
120
134
  # `invert_off`, `reverse_off`, `reveal`, `strike_off`, `proportional_off`,
121
135
  # `spacing_off`, `framed`, `encircled`, `overlined`, `framed_off`,
122
- # `encircled_off`, `overlined_off`
136
+ # `encircled_off`, `overlined_off`.
123
137
  #
124
- # Colors can specified by their name for ANSI 4-bit colors:
138
+ # Colors can specified by their name for ANSI 3-bit and 4-bit colors:
125
139
  # `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`,
126
- # `default`, `bright_black`, `bright_red`, `bright_green`, `bright_yellow`,
127
- # `bright_blue`, `bright_magenta`, `bright_cyan`, `bright_white`
140
+ # `default`, `bright_black`, `bright_red`, `bright_green`,
141
+ # `bright_yellow`, `bright_blue`, `bright_magenta`, `bright_cyan`,
142
+ # `bright_white`.
128
143
  #
129
- # For 8-bit ANSI colors you can use a prefixed integer number:
130
- # `i0`...`i255`.
144
+ # For 8-bit ANSI colors use 2-digit hexadecimal values `00`...`ff`.
131
145
  #
132
- # To use RGB ANSI colors just specify the hexadecimal code like `#XXXXXX`
133
- # or the short form `#XXX`.
146
+ # To use RGB ANSI colors (24-bit colors) specify 3-digit or 6-digit
147
+ # hexadecimal values `000`...`fff` or `000000`...`ffffff`.
148
+ # This represent the `RRGGBB` values (or `RGB` for short version) like you
149
+ # may known from CSS color notation.
134
150
  #
135
151
  # To use a color as background color prefix the color attribute with `bg_`
136
152
  # or `on_`.
137
153
  #
138
154
  # To use a color as underline color prefix the color attribute with `ul_`.
139
155
  #
140
- # To make it more clear a color attribute should be used as fereground
141
- # color the code can be prefixed with `fg_`.
156
+ # To make it more clear a color attribute have to be used as foreground
157
+ # color the color value can be prefixed with `fg_`.
142
158
  #
143
159
  # @example Valid Foreground Color Attributes
144
160
  # Ansi[:yellow]
145
- # Ansi["#fab"]
146
- # Ansi["#00aa00"]
161
+ # Ansi['#fab']
162
+ # Ansi['#00aa00']
147
163
  # Ansi[:fg_fab]
148
164
  # Ansi[:fg_00aa00]
149
- # Ansi[:i196]
150
- # Ansi[:fg_i196]
165
+ # Ansi[:af]
166
+ # Ansi[:fg_af]
151
167
  #
152
168
  # @example Valid Background Color Attributes
153
169
  # Ansi[:bg_yellow]
154
170
  # Ansi[:bg_fab]
155
171
  # Ansi[:bg_00aa00]
156
172
  # Ansi['bg#00aa00']
157
- # Ansi[:bg_i196]
173
+ # Ansi[:bg_af]
158
174
  #
159
175
  # Ansi[:on_yellow]
160
176
  # Ansi[:on_fab]
161
177
  # Ansi[:on_00aa00]
162
178
  # Ansi['on#00aa00']
163
- # Ansi[:on_i196]
179
+ # Ansi[:on_af]
164
180
  #
165
181
  # @example Valid Underline Color Attributes
166
- # Ansi[:underline, :yellow]
182
+ # Ansi[:underline, :ul_yellow]
167
183
  # Ansi[:underline, :ul_fab]
168
184
  # Ansi[:underline, :ul_00aa00]
169
185
  # Ansi[:underline, 'ul#00aa00']
170
- # Ansi[:underline, :ul_i196]
186
+ # Ansi[:underline, :ul_fa]
171
187
  # Ansi[:underline, :ul_bright_yellow]
172
188
  #
173
189
  # @example Combined attributes:
@@ -182,7 +198,7 @@ module NattyUI
182
198
  .map do |arg|
183
199
  case arg
184
200
  when Symbol, String
185
- ATTRIBUTES[arg] || named_color(arg) || invalid_argument(arg)
201
+ ATTRIBUTES[arg] || color(arg) || invalid_argument(arg)
186
202
  when (0..255)
187
203
  "38;5;#{arg}"
188
204
  when (256..512)
@@ -211,11 +227,10 @@ module NattyUI
211
227
  # @return [String] combined ANSI attributes
212
228
  # @return [nil] when string does not contain valid attributes
213
229
  def try_convert(attributes)
214
- attributes = attributes.to_s.split
215
- return if attributes.empty?
230
+ return if (attributes = attributes.to_s.split).empty?
216
231
  "\e[#{
217
232
  attributes
218
- .map { |arg| ATTRIBUTES[arg] || named_color(arg) || return }
233
+ .map { |arg| ATTRIBUTES[arg] || color(arg) || return }
219
234
  .join(';')
220
235
  }m"
221
236
  end
@@ -230,41 +245,37 @@ module NattyUI
230
245
  )
231
246
  end
232
247
 
233
- def named_color(value)
234
- case value
235
- when /\A(fg_|fg:|fg)?#?([[:xdigit:]]{3})\z/
236
- hex_rgb_short(38, Regexp.last_match(2))
237
- when /\A(fg_|fg:|fg)?#?([[:xdigit:]]{6})\z/
238
- hex_rgb(38, Regexp.last_match(2))
239
- when /\A(bg_|bg:|bg|on_|on:|on)#?([[:xdigit:]]{3})\z/
240
- hex_rgb_short(48, Regexp.last_match(2))
241
- when /\A(bg_|bg:|bg|on_|on:|on)#?([[:xdigit:]]{6})\z/
242
- hex_rgb(48, Regexp.last_match(2))
243
- when /\A(ul_|ul:|ul)#?([[:xdigit:]]{3})\z/
244
- hex_rgb_short(58, Regexp.last_match(2))
245
- when /\A(ul_|ul:|ul)#?([[:xdigit:]]{6})\z/
246
- hex_rgb(58, Regexp.last_match(2))
247
- when /\A(fg_|fg:|fg)?i([[:digit:]]{1,3})\z/
248
- number(38, Regexp.last_match(2))
249
- when /\A(bg_|bg:|bg|on_|on:|on)i([[:digit:]]{1,3})\z/
250
- number(48, Regexp.last_match(2))
251
- when /\A(ul_|ul:|ul)i([[:digit:]]{1,3})\z/
252
- number(58, Regexp.last_match(2))
248
+ def color(val)
249
+ val = val.to_s.downcase
250
+ base =
251
+ if val.delete_prefix!('fg')
252
+ val.delete_prefix!(':') || val.delete_prefix!('_')
253
+ '38;'
254
+ elsif val.delete_prefix!('ul')
255
+ val.delete_prefix!(':') || val.delete_prefix!('_')
256
+ '58;'
257
+ elsif val.delete_prefix!('bg') || val.delete_prefix!('on')
258
+ val.delete_prefix!(':') || val.delete_prefix!('_')
259
+ '48;'
260
+ else
261
+ '38;'
262
+ end
263
+ val.delete_prefix!('#')
264
+ case val.size
265
+ when 2
266
+ "#{base}5;#{val.hex}" if /\A[[:xdigit:]]+\z/.match?(val)
267
+ when 3
268
+ if /\A[[:xdigit:]]+\z/.match?(val)
269
+ "#{base}2;#{(val[0] * 2).hex};#{(val[1] * 2).hex};#{
270
+ (val[2] * 2).hex
271
+ }"
272
+ end
273
+ when 6
274
+ if /\A[[:xdigit:]]+\z/.match?(val)
275
+ "#{base}2;#{val[0, 2].hex};#{val[2, 2].hex};#{val[4, 2].hex}"
276
+ end
253
277
  end
254
278
  end
255
-
256
- def number(base, str)
257
- index = str.to_i
258
- "#{base};5;#{index}" if index >= 0 && index <= 255
259
- end
260
-
261
- def hex_rgb_short(base, str)
262
- "#{base};2;#{(str[0] * 2).hex};#{(str[1] * 2).hex};#{(str[2] * 2).hex}"
263
- end
264
-
265
- def hex_rgb(base, str)
266
- "#{base};2;#{str[0, 2].hex};#{str[2, 2].hex};#{str[4, 2].hex}"
267
- end
268
279
  end
269
280
 
270
281
  ATTRIBUTES =
@@ -282,7 +293,6 @@ module NattyUI
282
293
  rapid_blink: 6,
283
294
  # ---
284
295
  invert: 7,
285
- reverse: 7,
286
296
  # ---
287
297
  conceal: 8,
288
298
  hide: 8,
@@ -319,7 +329,6 @@ module NattyUI
319
329
  spacing: 26,
320
330
  # ---
321
331
  invert_off: 27,
322
- reverse_off: 27,
323
332
  # ---
324
333
  reveal: 28,
325
334
  # ---
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'io/console'
4
- require_relative 'wrapper'
5
3
  require_relative 'ansi'
4
+ require_relative 'wrapper'
6
5
 
7
6
  module NattyUI
8
7
  class AnsiWrapper < Wrapper
@@ -23,10 +22,7 @@ module NattyUI
23
22
 
24
23
  protected
25
24
 
26
- def embellish(obj)
27
- obj = NattyUI.embellish(obj)
28
- obj.empty? ? nil : obj
29
- end
25
+ def embellish(obj) = (obj = NattyUI.embellish(obj)).empty? ? nil : obj
30
26
 
31
27
  def temp_func
32
28
  count = @lines_written
@@ -69,6 +65,7 @@ module NattyUI
69
65
  task: 117
70
66
  }.compare_by_identity.freeze
71
67
  end
68
+ private_constant :Message
72
69
 
73
70
  class Section < Section
74
71
  def temporary
@@ -97,6 +94,7 @@ module NattyUI
97
94
  @prefix = Ansi.embellish(@prefix, *prefix_attr)
98
95
  end
99
96
  end
97
+ private_constant :Section
100
98
 
101
99
  class Heading < Heading
102
100
  protected
@@ -109,20 +107,52 @@ module NattyUI
109
107
  PREFIX = Ansi[39].freeze
110
108
  MSG = Ansi[:bold, 231].freeze
111
109
  end
110
+ private_constant :Heading
112
111
 
113
- class Framed < Framed
112
+ class Framed < Section
114
113
  protected
115
114
 
115
+ def initialize(parent, title:, type:, **opts)
116
+ @parent = parent
117
+ title = title.to_s.tr("\r\n\t", '')
118
+ topl, topr, botl, botr, hor, vert = *components(type)
119
+ width = available_width
120
+ rcount = [width - _plain_width(title) - 6, 0].max
121
+ parent.puts(
122
+ "#{COLOR}#{topl}#{hor}#{hor}#{Ansi.reset} " \
123
+ "#{TITLE_ATTR}#{title}#{Ansi.reset} " \
124
+ "#{COLOR}#{hor * rcount}#{topr}#{Ansi.reset}"
125
+ )
126
+ @bottom = "#{COLOR}#{botl}#{hor * (width - 2)}#{botr}#{Ansi.reset}"
127
+ vert = "#{COLOR}#{vert}#{Ansi.reset}"
128
+ super(
129
+ parent,
130
+ prefix: "#{vert} ",
131
+ suffix:
132
+ "#{Ansi.cursor_right_aligned}" \
133
+ "#{Ansi.cursor_left(suffix_width)}#{vert}",
134
+ **opts
135
+ )
136
+ end
137
+
138
+ def suffix = "#{super} "
139
+ def finish = parent.puts(@bottom)
140
+
116
141
  def components(type)
117
- top_start, top_suffix, left, bottom = super
118
- [
119
- "#{Ansi[39]}#{top_start}#{Ansi[:bold, 231]}",
120
- "#{Ansi[:reset, 39]}#{top_suffix}#{Ansi.reset}",
121
- Ansi.embellish(left, 39),
122
- Ansi.embellish(bottom, 39)
123
- ]
142
+ COMPONENTS[type] || raise(ArgumentError, "invalid frame type - #{type}")
124
143
  end
144
+
145
+ COLOR = Ansi[39].freeze
146
+ TITLE_ATTR = Ansi[:bold, 231].freeze
147
+ COMPONENTS = {
148
+ rounded: %w[╭ ╮ ╰ ╯ ─ │],
149
+ simple: %w[┌ ┐ └ ┘ ─ │],
150
+ heavy: %w[┏ ┓ ┗ ┛ ━ ┃],
151
+ semi: %w[┍ ┑ ┕ ┙ ━ │],
152
+ double: %w[╔ ╗ ╚ ╝ ═ ║]
153
+ }.compare_by_identity.freeze
125
154
  end
155
+ private_constant :Framed
126
156
 
127
157
  class Ask < Ask
128
158
  protected
@@ -135,6 +165,16 @@ module NattyUI
135
165
 
136
166
  PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
137
167
  end
168
+ private_constant :Ask
169
+
170
+ class Request < Request
171
+ def prompt(question) = "#{prefix}#{PREFIX} #{question}#{Ansi.reset} "
172
+ def finish = (wrapper.stream << FINISH).flush
173
+
174
+ PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
175
+ FINISH = (Ansi.cursor_line_up + Ansi.line_erase_to_end).freeze
176
+ end
177
+ private_constant :Request
138
178
 
139
179
  class Query < Query
140
180
  protected
@@ -146,47 +186,41 @@ module NattyUI
146
186
 
147
187
  PROMPT = Ansi.embellish(':', :bold, 220).freeze
148
188
  end
189
+ private_constant :Query
149
190
 
150
191
  class Task < Message
151
192
  include ProgressAttributes
152
193
  include TaskMethods
153
194
  end
195
+ private_constant :Task
154
196
 
155
197
  class Progress < Progress
156
198
  protected
157
199
 
158
- def draw_title(title)
200
+ def draw(title)
159
201
  @prefix = "#{prefix}#{TITLE_PREFIX}#{title}#{Ansi.reset} "
160
- (wrapper.stream << @prefix).flush
202
+ (wrapper.stream << @prefix << Ansi.cursor_hide).flush
161
203
  @prefix = "#{Ansi.line_clear}#{@prefix}"
162
- if @max_value
163
- @prefix << BAR_COLOR
164
- else
165
- @prefix << INDICATOR_ATTRIBUTE
166
- @indicator = 0
167
- end
204
+ return @prefix << BAR_COLOR if @max_value
205
+ @prefix << INDICATOR_ATTRIBUTE
206
+ @indicator = 0
168
207
  end
169
208
 
170
- def draw_final = (wrapper.stream << Ansi.line_clear).flush
171
-
172
209
  def redraw
173
210
  (wrapper.stream << @prefix << (@max_value ? fullbar : indicator)).flush
174
211
  end
175
212
 
213
+ def end_draw = (wrapper.stream << ERASE).flush
176
214
  def indicator = '─╲│╱'[(@indicator += 1) % 4]
215
+ # def indicator = '⣷⣯⣟⡿⢿⣻⣽⣾'[(@indicator += 1) % 8]
177
216
 
178
217
  def fullbar
179
218
  percent = @value / @max_value
180
219
  count = (30 * percent).to_i
220
+ mv = max_value.to_i.to_s
181
221
  "#{'█' * count}#{BAR_BACK}#{'▁' * (30 - count)}" \
182
- "#{BAR_INK} #{
183
- format(
184
- '%<value>.0f/%<max_value>.0f (%<percent>.2f%%)',
185
- value: @value,
186
- max_value: @max_value,
187
- percent: percent * 100
188
- )
189
- }"
222
+ "#{BAR_INK} #{value.to_i.to_s.rjust(mv.size)}/#{mv} " \
223
+ "(#{(percent * 100).round(2).to_s.rjust(6)})"
190
224
  end
191
225
 
192
226
  TITLE_PREFIX = "#{Ansi[:bold, :italic, 117]}➔#{Ansi[:reset, 117]} ".freeze
@@ -194,7 +228,9 @@ module NattyUI
194
228
  BAR_COLOR = Ansi[39, 295].freeze
195
229
  BAR_BACK = Ansi[236, 492].freeze
196
230
  BAR_INK = Ansi[:bold, 255, :on_default].freeze
231
+ ERASE = (Ansi.line_clear + Ansi.cursor_show).freeze
197
232
  end
233
+ private_constant :Progress
198
234
 
199
235
  PAGE_BEGIN =
200
236
  "#{Ansi.reset}#{Ansi.cursor_save_pos}#{Ansi.screen_save}" \
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ #
5
+ # Features of {NattyUI} - methods to display natty elements.
6
+ #
7
+ module Features
8
+ # Print a horizontal rule
9
+ #
10
+ # @param [#to_s] symbol string to build the horizontal rule
11
+ # @return [Wrapper, Wrapper::Element] itself
12
+ def hr(symbol = '═')
13
+ symbol = symbol.to_s
14
+ size = _plain_width(symbol)
15
+ return self if size.zero?
16
+ msg = symbol * ((available_width - 1) / size)
17
+ return puts(msg, prefix: Ansi[39], suffix: Ansi.reset) if wrapper.ansi?
18
+ puts(msg)
19
+ end
20
+
21
+ protected
22
+
23
+ def _plain_width(str) = NattyUI.display_width(NattyUI.plain(str))
24
+ def _blemish_width(str) = NattyUI.display_width(Ansi.blemish(str))
25
+
26
+ def _element(type, ...)
27
+ wrapper.class.const_get(type).__send__(:new, self).__send__(:_call, ...)
28
+ end
29
+
30
+ def _section(owner, type, args, **opts, &block)
31
+ sec = wrapper.class.const_get(type).__send__(:new, owner, **opts)
32
+ sec.puts(*args) if args && !args.empty?
33
+ block ? sec.__send__(:_call, &block) : sec
34
+ end
35
+ end
36
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module NattyUI
4
4
  # @return [String] the version number of the gem
5
- VERSION = '0.5.3'
5
+ VERSION = '0.7.0'
6
6
  end
@@ -17,7 +17,7 @@ module NattyUI
17
17
  # when true
18
18
  # sec.info('Yeah!!')
19
19
  # when false
20
- # sec.write("That's pitty!")
20
+ # sec.write("That's pity!")
21
21
  # else
22
22
  # sec.failed('You should have an opinion!')
23
23
  # end
@@ -59,7 +59,7 @@ module NattyUI
59
59
  def read(yes, no)
60
60
  while true
61
61
  char = NattyUI.in_stream.getch
62
- return if "\u0003\u0004\e".include?(char)
62
+ return if "\3\4\e".include?(char)
63
63
  return true if yes.include?(char)
64
64
  return false if no.include?(char)
65
65
  end
@@ -67,9 +67,9 @@ module NattyUI
67
67
 
68
68
  def grab(yes, no)
69
69
  yes = yes.to_s.chars.uniq
70
+ raise(ArgumentError, ':yes can not be empty') if yes.empty?
70
71
  no = no.to_s.chars.uniq
71
- raise(ArgumentError, ':yes can not be emoty') if yes.empty?
72
- raise(ArgumentError, ':no can not be emoty') if no.empty?
72
+ raise(ArgumentError, ':no can not be empty') if no.empty?
73
73
  return yes, no if (yes & no).empty?
74
74
  raise(ArgumentError, 'chars in :yes and :no can not be intersect')
75
75
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'features'
3
+ require_relative '../features'
4
4
 
5
5
  module NattyUI
6
6
  class Wrapper
@@ -36,7 +36,10 @@ module NattyUI
36
36
  protected
37
37
 
38
38
  def prefix = "#{@parent.__send__(:prefix)}#{@prefix}"
39
- def suffix = "#{@parent.__send__(:suffix)}#{@suffix}"
39
+ def suffix = "#{@suffix}#{@parent.__send__(:suffix)}"
40
+ def prefix_width = _blemish_width(prefix)
41
+ def suffix_width = _blemish_width(suffix)
42
+ def available_width = wrapper.screen_columns - prefix_width - suffix_width
40
43
  def finish = nil
41
44
 
42
45
  def wrapper
@@ -46,7 +49,7 @@ module NattyUI
46
49
  @wrapper
47
50
  end
48
51
 
49
- def initialize(parent) = (@parent = parent)
52
+ def initialize(parent, **_) = (@parent = parent)
50
53
 
51
54
  def _close(state)
52
55
  return self if @status
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ module Features
7
+ #
8
+ # Print items of a given list as columns.
9
+ # In the default compact format columns may have diffrent widths and the
10
+ # list items are ordered column-wise.
11
+ # The non-compact format prints all columns in same width and order the list
12
+ # items row-wise.
13
+ #
14
+ # @param [Array<#to_s>] args items to print
15
+ # @param [Boolean] compact whether to use compact format
16
+ # @return [Wrapper, Wrapper::Element] itself
17
+ def ls(*args, compact: true) = _element(:ListInColumns, args, compact)
18
+ end
19
+
20
+ class Wrapper
21
+ #
22
+ # An {Element} to print items of a given list as columns.
23
+ #
24
+ # @see Features#ls
25
+ class ListInColumns < Element
26
+ protected
27
+
28
+ def _call(list, compact)
29
+ list.flatten!
30
+ return parent if list.empty?
31
+ list.map! { |item| Item.new(item = item.to_s, _plain_width(item)) }
32
+ if compact
33
+ each_compacted(list, available_width) { |line| parent.puts(line) }
34
+ else
35
+ each(list, available_width) { |line| parent.puts(line) }
36
+ end
37
+ parent
38
+ end
39
+
40
+ def each(list, max_width)
41
+ width = list.max_by(&:width).width + 3
42
+ list.each_slice(max_width / width) do |slice|
43
+ yield(slice.map { |item| item.to_s(width) }.join)
44
+ end
45
+ end
46
+
47
+ def each_compacted(list, max_width)
48
+ found, widths = find_columns(list, max_width)
49
+ fill(found[-1], found[0].size)
50
+ found.transpose.each do |row|
51
+ row = row.each_with_index.map { |item, col| item&.to_s(widths[col]) }
52
+ yield(row.join)
53
+ end
54
+ end
55
+
56
+ def find_columns(list, max_width)
57
+ found = [list]
58
+ widths = [list.max_by(&:width).width]
59
+ 1.upto(list.size - 1) do |slice_size|
60
+ candidate = list.each_slice(list.size / slice_size).to_a
61
+ cwidths = candidate.map { |ary| ary.max_by(&:width).width + 3 }
62
+ cwidths[-1] -= 3
63
+ break if cwidths.sum > max_width
64
+ found = candidate
65
+ widths = cwidths
66
+ end
67
+ [found, widths]
68
+ end
69
+
70
+ def fill(ary, size)
71
+ diff = size - ary.size
72
+ ary.fill(nil, ary.size, diff) if diff.positive?
73
+ end
74
+
75
+ Item =
76
+ Data.define(:str, :width) do
77
+ def to_s(in_width) = "#{str}#{' ' * (in_width - width)}"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -12,7 +12,7 @@ module NattyUI
12
12
  #
13
13
  module ProgressAttributes
14
14
  # @attribute [r] completed?
15
- # @return [Boolean] whether the task completed sucessfully
15
+ # @return [Boolean] whether the task completed successfully
16
16
  def completed? = (@status == :completed)
17
17
 
18
18
  # @attribute [r] failed?
@@ -49,9 +49,9 @@ module NattyUI
49
49
  redraw
50
50
  end
51
51
 
52
- # Maximal value.
52
+ # Maximum value.
53
53
  #
54
- # @return [Float] maximal value
54
+ # @return [Float] maximum value
55
55
  # @return [nil] when no max_value was configured
56
56
  attr_reader :max_value
57
57
 
@@ -29,17 +29,17 @@ module NattyUI
29
29
 
30
30
  protected
31
31
 
32
- def initialize(parent, title:, max_value:, **_)
33
- super(parent)
32
+ def initialize(parent, title:, max_value:, **opts)
33
+ super(parent, **opts)
34
34
  @final_text = [title]
35
35
  @max_value = [0, max_value.to_f].max if max_value
36
36
  @value = 0
37
37
  @progress = 0
38
- draw_title(title)
38
+ draw(title)
39
39
  end
40
40
 
41
- def draw_title(title) = (wrapper.stream << prefix << "➔ #{title} ").flush
42
- def draw_final = (wrapper.stream << "\n")
41
+ def draw(title) = (wrapper.stream << prefix << "➔ #{title} ").flush
42
+ def end_draw = (wrapper.stream << "\n")
43
43
 
44
44
  def redraw
45
45
  return (wrapper.stream << '.').flush unless @max_value
@@ -50,10 +50,15 @@ module NattyUI
50
50
  end
51
51
 
52
52
  def finish
53
- draw_final
53
+ end_draw
54
54
  return @parent.failed(*@final_text) if failed?
55
- @status = :ok if @status == :closed
56
- @parent.completed(*@final_text)
55
+ _section(
56
+ @parent,
57
+ :Message,
58
+ @final_text,
59
+ title: @final_text.shift,
60
+ symbol: @status = :completed
61
+ )
57
62
  end
58
63
  end
59
64
  end