natty-ui 0.12.0 → 0.25.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +23 -24
  4. data/examples/24bit-colors.rb +4 -9
  5. data/examples/3bit-colors.rb +28 -8
  6. data/examples/8bit-colors.rb +18 -23
  7. data/examples/attributes.rb +30 -25
  8. data/examples/cols.rb +40 -0
  9. data/examples/elements.rb +31 -0
  10. data/examples/examples.rb +45 -0
  11. data/examples/illustration.rb +56 -54
  12. data/examples/ls.rb +16 -18
  13. data/examples/named-colors.rb +23 -0
  14. data/examples/sections.rb +29 -0
  15. data/examples/tables.rb +62 -0
  16. data/examples/tasks.rb +52 -0
  17. data/lib/natty-ui/attributes.rb +604 -0
  18. data/lib/natty-ui/choice.rb +56 -0
  19. data/lib/natty-ui/dumb_choice.rb +45 -0
  20. data/lib/natty-ui/element.rb +78 -0
  21. data/lib/natty-ui/features.rb +798 -0
  22. data/lib/natty-ui/framed.rb +51 -0
  23. data/lib/natty-ui/ls_renderer.rb +93 -0
  24. data/lib/natty-ui/progress.rb +187 -0
  25. data/lib/natty-ui/section.rb +69 -0
  26. data/lib/natty-ui/table.rb +241 -0
  27. data/lib/natty-ui/table_renderer.rb +147 -0
  28. data/lib/natty-ui/task.rb +44 -0
  29. data/lib/natty-ui/temporary.rb +38 -0
  30. data/lib/natty-ui/theme.rb +303 -0
  31. data/lib/natty-ui/utils.rb +79 -0
  32. data/lib/natty-ui/version.rb +1 -1
  33. data/lib/natty-ui/width_finder.rb +125 -0
  34. data/lib/natty-ui.rb +89 -147
  35. metadata +47 -56
  36. data/examples/animate.rb +0 -44
  37. data/examples/attributes_list.rb +0 -14
  38. data/examples/demo.rb +0 -53
  39. data/examples/message.rb +0 -32
  40. data/examples/progress.rb +0 -68
  41. data/examples/query.rb +0 -41
  42. data/examples/read_key.rb +0 -13
  43. data/examples/table.rb +0 -41
  44. data/lib/natty-ui/animation/binary.rb +0 -36
  45. data/lib/natty-ui/animation/default.rb +0 -38
  46. data/lib/natty-ui/animation/matrix.rb +0 -51
  47. data/lib/natty-ui/animation/rainbow.rb +0 -28
  48. data/lib/natty-ui/animation/type_writer.rb +0 -44
  49. data/lib/natty-ui/animation.rb +0 -69
  50. data/lib/natty-ui/ansi/constants.rb +0 -75
  51. data/lib/natty-ui/ansi.rb +0 -521
  52. data/lib/natty-ui/ansi_wrapper.rb +0 -199
  53. data/lib/natty-ui/frame.rb +0 -53
  54. data/lib/natty-ui/glyph.rb +0 -64
  55. data/lib/natty-ui/key_map.rb +0 -142
  56. data/lib/natty-ui/preload.rb +0 -12
  57. data/lib/natty-ui/spinner.rb +0 -120
  58. data/lib/natty-ui/text/east_asian_width.rb +0 -2529
  59. data/lib/natty-ui/text.rb +0 -203
  60. data/lib/natty-ui/wrapper/animate.rb +0 -17
  61. data/lib/natty-ui/wrapper/ask.rb +0 -78
  62. data/lib/natty-ui/wrapper/element.rb +0 -79
  63. data/lib/natty-ui/wrapper/features.rb +0 -21
  64. data/lib/natty-ui/wrapper/framed.rb +0 -45
  65. data/lib/natty-ui/wrapper/heading.rb +0 -60
  66. data/lib/natty-ui/wrapper/horizontal_rule.rb +0 -37
  67. data/lib/natty-ui/wrapper/list_in_columns.rb +0 -138
  68. data/lib/natty-ui/wrapper/message.rb +0 -109
  69. data/lib/natty-ui/wrapper/mixins.rb +0 -67
  70. data/lib/natty-ui/wrapper/progress.rb +0 -74
  71. data/lib/natty-ui/wrapper/query.rb +0 -89
  72. data/lib/natty-ui/wrapper/quote.rb +0 -25
  73. data/lib/natty-ui/wrapper/request.rb +0 -54
  74. data/lib/natty-ui/wrapper/section.rb +0 -118
  75. data/lib/natty-ui/wrapper/table.rb +0 -551
  76. data/lib/natty-ui/wrapper/task.rb +0 -55
  77. data/lib/natty-ui/wrapper.rb +0 -230
data/lib/natty-ui/text.rb DELETED
@@ -1,203 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'ansi'
4
-
5
- module NattyUI
6
- module Text
7
- class << self
8
- def plain_but_ansi(str)
9
- (str = str.to_s).empty? and return str
10
- str.gsub(BBCODE) do
11
- match = Regexp.last_match[1]
12
- if match[0] == '/'
13
- next if match.size == 1
14
- next "[#{match[1..]}]" if match[1] == '/'
15
- end
16
- Ansi.try_convert(match) ? nil : "[#{match}]"
17
- end
18
- end
19
-
20
- def plain(str) = Ansi.blemish(plain_but_ansi(str))
21
-
22
- def embellish(str)
23
- (str = str.to_s).empty? and return str
24
- reset = false
25
- str =
26
- str.gsub(BBCODE) do
27
- match = Regexp.last_match[1]
28
- if match[0] == '/'
29
- if match.size == 1
30
- reset = false
31
- next Ansi::RESET
32
- end
33
- next "[#{match[1..]}]" if match[1] == '/'
34
- end
35
-
36
- ansi = Ansi.try_convert(match)
37
- ansi ? reset = ansi : "[#{match}]"
38
- end
39
- reset ? "#{str}#{Ansi::RESET}" : str
40
- end
41
-
42
- # works for UTF-8 chars only!
43
- def char_width(char)
44
- ord = char.ord
45
- return WIDTH_CONTROL_CHARS[ord] || 2 if ord < 0x20
46
- return 1 if ord < 0xa1
47
- size = EastAsianWidth[ord]
48
- return @ambiguous_char_width if size == -1
49
- if size == 1 && char.size >= 2
50
- sco = char[1].ord
51
- # Halfwidth Dakuten Handakuten
52
- return sco == 0xff9e || sco == 0xff9f ? 2 : 1
53
- end
54
- size
55
- end
56
-
57
- def width(str)
58
- return 0 if (str = plain_but_ansi(str)).empty?
59
- str = str.encode(UTF_8) if str.encoding != UTF_8
60
- width = 0
61
- in_zero_width = false
62
- str.scan(WIDTH_SCANNER) do |np_start, np_end, _csi, _osc, gc|
63
- if in_zero_width
64
- in_zero_width = false if np_end
65
- next
66
- end
67
- next in_zero_width = true if np_start
68
- width += char_width(gc) if gc
69
- end
70
- width
71
- end
72
-
73
- def each_line_plain(strs, max_width)
74
- return if (max_width = max_width.to_i) < 1
75
- strs.each do |str|
76
- plain_but_ansi(str).each_line(chomp: true) do |line|
77
- next yield(line, 0) if line.empty?
78
- empty = String.new(encoding: line.encoding)
79
- current = empty.dup
80
- width = 0
81
- in_zero_width = false
82
- line = line.encode(UTF_8) if line.encoding != UTF_8
83
- line.scan(WIDTH_SCANNER) do |np_start, np_end, csi, osc, gc|
84
- next in_zero_width = (current << "\1") if np_start
85
- next in_zero_width = !(current << "\2") if np_end
86
- next if osc || csi
87
- next current << gc if in_zero_width
88
- cw = char_width(gc)
89
- if (width += cw) > max_width
90
- yield(current, width - cw)
91
- width = cw
92
- current = empty.dup
93
- end
94
- current << gc
95
- end
96
- yield(current, width)
97
- end
98
- end
99
- nil
100
- end
101
-
102
- def as_lines_plain(strs, width, height = nil)
103
- ret = []
104
- each_line_plain(strs, width) do |*info|
105
- ret << info
106
- break if height == ret.size
107
- end
108
- ret
109
- end
110
-
111
- def each_line(strs, max_width)
112
- return if (max_width = max_width.to_i) < 1
113
- strs.each do |str|
114
- str
115
- .to_s
116
- .each_line(chomp: true) do |line|
117
- line = embellish(line)
118
- next yield(line, 0) if line.empty?
119
- current = String.new(encoding: line.encoding)
120
- seq = current.dup
121
- width = 0
122
- in_zero_width = false
123
- line = line.encode(UTF_8) if line.encoding != UTF_8
124
- line.scan(WIDTH_SCANNER) do |np_start, np_end, csi, osc, gc|
125
- next in_zero_width = (current << "\1") if np_start
126
- next in_zero_width = !(current << "\2") if np_end
127
- next (current << osc) && (seq << osc) if osc
128
- if csi
129
- current << csi
130
- next seq.clear if csi == "\e[m" || csi == "\e[0m"
131
- next if in_zero_width
132
- next seq << csi
133
- end
134
- next current << gc if in_zero_width
135
- cw = char_width(gc)
136
- if (width += cw) > max_width
137
- yield(current, width - cw)
138
- width = cw
139
- current = seq.dup
140
- end
141
- current << gc
142
- end
143
- yield(current, width)
144
- end
145
- end
146
- end
147
-
148
- def as_lines(strs, width, height = nil)
149
- ret = []
150
- each_line(strs, width) do |*info|
151
- ret << info
152
- break if height == ret.size
153
- end
154
- ret
155
- end
156
- end
157
-
158
- UTF_8 = Encoding::UTF_8
159
- BBCODE = /(?:\[((?~[\[\]]))\])/
160
- WIDTH_SCANNER = /\G(?:(\1)|(\2)|(#{Ansi::CSI})|(#{Ansi::OSC})|(\X))/
161
- WIDTH_CONTROL_CHARS = {
162
- 0x00 => 0,
163
- 0x01 => 1,
164
- 0x02 => 1,
165
- 0x03 => 1,
166
- 0x04 => 1,
167
- 0x05 => 0,
168
- 0x06 => 1,
169
- 0x07 => 0,
170
- 0x08 => 0,
171
- 0x09 => 8,
172
- 0x0a => 0,
173
- 0x0b => 0,
174
- 0x0c => 0,
175
- 0x0d => 0,
176
- 0x0e => 0,
177
- 0x0f => 0,
178
- 0x10 => 1,
179
- 0x11 => 1,
180
- 0x12 => 1,
181
- 0x13 => 1,
182
- 0x14 => 1,
183
- 0x15 => 1,
184
- 0x16 => 1,
185
- 0x17 => 1,
186
- 0x18 => 1,
187
- 0x19 => 1,
188
- 0x1a => 1,
189
- 0x1b => 1,
190
- 0x1c => 1,
191
- 0x1d => 1,
192
- 0x1e => 1,
193
- 0x1f => 1
194
- }.compare_by_identity.freeze
195
-
196
- autoload(:EastAsianWidth, File.join(__dir__, 'text', 'east_asian_width'))
197
- private_constant :EastAsianWidth
198
-
199
- @ambiguous_char_width = 1
200
- end
201
-
202
- private_constant :Text
203
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NattyUI
4
- module Features
5
- # Print given arguments line-wise with animation.
6
- #
7
- # @overload animate(..., animation: :default)
8
- # @param [#to_s] ... objects to print
9
- # @param [:binary, :default, :matrix, :rainbow, :type_writer]
10
- # animation type of animation
11
- # @return [Wrapper::Section, Wrapper] it's parent object
12
- def animate(*args, **kwargs)
13
- kwargs[:animation] ||= :default
14
- puts(*args, **kwargs)
15
- end
16
- end
17
- end
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'element'
4
-
5
- module NattyUI
6
- module Features
7
- # Ask a yes/no question from user.
8
- #
9
- # The defaults for `yes` and `no` will work for
10
- # Afrikaans, Dutch, English, French, German, Italian, Polish, Portuguese,
11
- # Romanian, Spanish and Swedish.
12
- #
13
- # The default for `yes` includes `ENTER` and `RETURN` key
14
- #
15
- # @example
16
- # case ui.ask('Do you like the NattyUI gem?')
17
- # when true
18
- # ui.info('Yeah!!')
19
- # when false
20
- # ui.write("That's pity!")
21
- # else
22
- # ui.failed('You should have an opinion!')
23
- # end
24
- #
25
- # @see NattyUI.in_stream
26
- #
27
- # @param question [#to_s] Question to display
28
- # @param yes [#to_s] chars which will be used to answer 'Yes'
29
- # @param no [#to_s] chars which will be used to answer 'No'
30
- # @return [Boolean] whether the answer is yes or no
31
- # @return [nil] when input was aborted with `^C` or `^D`
32
- def ask(question, yes: "jotsyd\r\n", no: 'n')
33
- _element(:Ask, question, yes, no)
34
- end
35
- end
36
-
37
- class Wrapper
38
- #
39
- # An {Element} to ask user input for yes/no queries.
40
- #
41
- # @see Features#ask
42
- class Ask < Element
43
- protected
44
-
45
- def call(question, yes, no)
46
- yes, no = grab(yes, no)
47
- draw(question)
48
- while true
49
- char = NattyUI.read_key(mode: :raw)
50
- return if "\3\4".include?(char)
51
- return true if yes.include?(char)
52
- return false if no.include?(char)
53
- end
54
- ensure
55
- wrapper.ansi? ? (wrapper.stream << Ansi::CLL).flush : @parent.puts
56
- end
57
-
58
- def draw(question)
59
- glyph = NattyUI::Glyph[:query]
60
- @parent.print(
61
- question,
62
- prefix: "#{glyph} #{Ansi[255]}",
63
- prefix_width: Text.width(glyph) + 1,
64
- suffix_width: 0
65
- )
66
- end
67
-
68
- def grab(yes, no)
69
- yes = yes.to_s.chars.uniq
70
- raise(ArgumentError, ':yes can not be empty') if yes.empty?
71
- no = no.to_s.chars.uniq
72
- raise(ArgumentError, ':no can not be empty') if no.empty?
73
- return yes, no if (yes & no).empty?
74
- raise(ArgumentError, 'chars in :yes and :no can not be intersect')
75
- end
76
- end
77
- end
78
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'features'
4
-
5
- module NattyUI
6
- class Wrapper
7
- #
8
- # Basic visual element.
9
- #
10
- class Element
11
- include Features
12
-
13
- # @attribute [r] available_width
14
- # @return [Integer] available columns count within the element
15
- def available_width = @parent.available_width
16
-
17
- # @attribute [r] closed?
18
- # @return [Boolean] whether its closed or not
19
- def closed? = (@status != nil)
20
-
21
- # @return [Section, Wrapper] parent element
22
- attr_reader :parent
23
-
24
- # @return [Symbol, nil] status when closed
25
- attr_reader :status
26
-
27
- # @attribute [r] wrapper
28
- # @return [Wrapper] assigned output stream wrapper
29
- def wrapper
30
- return @wrapper if @wrapper
31
- @wrapper = @parent
32
- @wrapper = @wrapper.parent until @wrapper.is_a?(Wrapper)
33
- @wrapper
34
- end
35
-
36
- # Close the element.
37
- #
38
- # @return [Element] itself
39
- def close = _close(:closed)
40
-
41
- alias _to_s to_s
42
- private :_to_s
43
- # @!visibility private
44
- def inspect = _to_s
45
-
46
- protected
47
-
48
- def _close(state)
49
- return self if @status
50
- @status = state
51
- finish
52
- @raise ? raise(BREAK) : self
53
- end
54
-
55
- def call
56
- NattyUI.instance_variable_set(:@element, self)
57
- @raise = true
58
- yield(self)
59
- closed? ? self : close
60
- rescue BREAK
61
- nil
62
- ensure
63
- NattyUI.instance_variable_set(:@element, @parent)
64
- end
65
-
66
- def finish = nil
67
- def prefix = "#{@parent.instance_variable_get(:@prefix)}#{@prefix}"
68
-
69
- def initialize(parent)
70
- @parent = parent
71
- end
72
-
73
- private_class_method :new
74
-
75
- BREAK = Class.new(StandardError)
76
- private_constant :BREAK
77
- end
78
- end
79
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NattyUI
4
- #
5
- # Features of {NattyUI} - methods to display natty elements.
6
- #
7
- module Features
8
- protected
9
-
10
- def _element(type, ...)
11
- wrapper.class.const_get(type).__send__(:new, self).__send__(:call, ...)
12
- end
13
-
14
- def _section(type, args = nil, owner: nil, **opts, &block)
15
- sec = wrapper.class.const_get(type).__send__(:new, owner || self, **opts)
16
- sec.puts(*args) if args && !args.empty?
17
- sec.__send__(:call, &block) if block
18
- sec
19
- end
20
- end
21
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'section'
4
-
5
- module NattyUI
6
- module Features
7
- # Creates frame-enclosed section with a highlighted `title` and
8
- # prints given additional arguments as lines into the section.
9
- #
10
- # When no block is given, the section must be closed, see
11
- # {Wrapper::Element#close}.
12
- #
13
- # @param [Array<#to_s>] args more objects to print
14
- # @param [Symbol, String] type frame type; see {NattyUI::Frame}
15
- # @yieldparam [Wrapper::Framed] framed the created section
16
- # @return [Object] the result of the code block
17
- # @return [Wrapper::Framed] itself, when no code block is given
18
- def framed(*args, type: :default, &block)
19
- _section(:Framed, args, type: NattyUI::Frame[type], &block)
20
- end
21
- end
22
-
23
- class Wrapper
24
- #
25
- # A frame-enclosed {Section} with a highlighted title.
26
- #
27
- # @see Features#framed
28
- class Framed < Section
29
- protected
30
-
31
- def initialize(parent, type:)
32
- super(parent, prefix: "#{type[4]} ", prefix_width: 2, suffix_width: 2)
33
- init(type)
34
- end
35
-
36
- def init(type)
37
- aw = @parent.available_width - 1
38
- parent.puts("#{type[0]}#{type[5] * aw}")
39
- @finish = "#{type[2]}#{type[5] * aw}"
40
- end
41
-
42
- def finish = @parent.puts(@finish)
43
- end
44
- end
45
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'element'
4
-
5
- module NattyUI
6
- module Features
7
- # Prints a H1 title.
8
- #
9
- # @param [#to_s] title text
10
- # @return [Wrapper::Section, Wrapper] it's parent object
11
- def h1(title) = _element(:Heading, title, '═══════')
12
-
13
- # Prints a H2 title.
14
- #
15
- # @param (see #h1)
16
- # @return (see #h1)
17
- def h2(title) = _element(:Heading, title, '━━━━━')
18
-
19
- # Prints a H3 title.
20
- #
21
- # @param (see #h1)
22
- # @return (see #h1)
23
- def h3(title) = _element(:Heading, title, '━━━')
24
-
25
- # Prints a H4 title.
26
- #
27
- # @param (see #h1)
28
- # @return (see #h1)
29
- def h4(title) = _element(:Heading, title, '───')
30
-
31
- # Prints a H5 title.
32
- #
33
- # @param (see #h1)
34
- # @return (see #h1)
35
- def h5(title) = _element(:Heading, title, '──')
36
- end
37
-
38
- class Wrapper
39
- #
40
- # A {Element} drawing a title.
41
- #
42
- # @see Features#h1
43
- # @see Features#h2
44
- # @see Features#h3
45
- # @see Features#h4
46
- # @see Features#h5
47
- class Heading < Element
48
- protected
49
-
50
- def call(title, enclose)
51
- @parent.puts(
52
- title,
53
- prefix: "#{Ansi[39]}#{enclose} #{Ansi[:bold, 255]}",
54
- suffix: " #{Ansi[:bold_off, 39]}#{enclose}#{Ansi::RESET}",
55
- max_width: available_width - 2 - (enclose.size * 2)
56
- )
57
- end
58
- end
59
- end
60
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'element'
4
-
5
- module NattyUI
6
- module Features
7
- # Print a horizontal rule
8
- #
9
- # @param [#to_s] symbol string to build the horizontal rule
10
- # @return [Wrapper::Section, Wrapper] it's parent object
11
- def hr(symbol = '─') = _element(:HorizontalRule, symbol)
12
- end
13
-
14
- class Wrapper
15
- #
16
- # A {Element} drawing a horizontal rule.
17
- #
18
- # @see Features#hr
19
- class HorizontalRule < Element
20
- protected
21
-
22
- def call(symbol)
23
- size = Text.width(symbol = symbol.to_s)
24
- return @parent.puts if size == 0
25
- max_width = available_width
26
- @parent.print(
27
- symbol * ((max_width / size)),
28
- max_width: max_width,
29
- prefix: Ansi[39],
30
- prefix_width: 0,
31
- suffix: Ansi::RESET,
32
- suffix_width: 0
33
- )
34
- end
35
- end
36
- end
37
- end
@@ -1,138 +0,0 @@
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
- # @example simple compact list
15
- # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry')
16
- # # => apple banana blueberry pineapple strawberry
17
- #
18
- # @example (unordered) list with red dot
19
- # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry', glyph: '[red]•[/]')
20
- # # => • apple • banana • blueberry • pineapple • strawberry
21
- #
22
- # @example ordered list
23
- # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry', glyph: 1)
24
- # # => 1 apple 2 banana 3 blueberry 4 pineapple 5 strawberry
25
- #
26
- # @example ordered list, start at 100
27
- # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry', glyph: 100)
28
- # # => 100 apple 101 banana 102 blueberry 103 pineapple 104 strawberry
29
- #
30
- # @example ordered list using, uppercase characters
31
- # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry', glyph: :A)
32
- # # => A apple B banana C blueberry D pineapple E strawberry
33
- #
34
- # @example ordered list, using lowercase characters
35
- # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry', glyph: :a)
36
- # # => a apple b banana c blueberry d pineapple e strawberry
37
- #
38
- # @param [Array<#to_s>] args items to print
39
- # @param [Boolean] compact whether to use compact format
40
- # @param [nil,#to_s,Integer,Symbol] glyph optional glyph used as element
41
- # prefix
42
- # @return [Wrapper, Wrapper::Element] itself
43
- def ls(*args, compact: true, glyph: nil)
44
- _element(:ListInColumns, args, compact, glyph)
45
- end
46
- end
47
-
48
- class Wrapper
49
- #
50
- # An {Element} to print items of a given list as columns.
51
- #
52
- # @see Features#ls
53
- class ListInColumns < Element
54
- protected
55
-
56
- def call(list, compact, glyph)
57
- return @parent if list.empty?
58
- list.flatten!
59
- cvt = cvt(glyph, list.size)
60
- list.map! { |item| Item.new(item = cvt[item], Text.width(item)) }
61
- if compact
62
- each_compacted(list, available_width - 1) { @parent.puts(_1) }
63
- else
64
- each(list, available_width - 1) { @parent.puts(_1) }
65
- end
66
- @parent
67
- end
68
-
69
- def cvt(glyph, size)
70
- case glyph
71
- when nil, false
72
- ->(s) { Text.embellish(s) }
73
- when :hex
74
- pad = size.to_s(16).size
75
- glyph = 0
76
- lambda do |s|
77
- "#{(glyph += 1).to_s(16).rjust(pad, '0')} #{Text.embellish(s)}"
78
- end
79
- when Integer
80
- pad = (glyph + size).to_s.size
81
- glyph -= 1
82
- ->(s) { "#{(glyph += 1).to_s.rjust(pad)} #{Text.embellish(s)}" }
83
- when Symbol
84
- lambda do |s|
85
- "#{
86
- t = glyph
87
- glyph = glyph.succ
88
- t
89
- } #{Text.embellish(s)}"
90
- end
91
- else
92
- ->(s) { "#{Text.embellish(glyph)} #{Text.embellish(s)}" }
93
- end
94
- end
95
-
96
- def each(list, max_width)
97
- width = list.max_by(&:width).width + 3
98
- list.each_slice(max_width / width) do |slice|
99
- yield(slice.map { _1.to_s(width) }.join)
100
- end
101
- end
102
-
103
- def each_compacted(list, max_width)
104
- found, widths = find_columns(list, max_width)
105
- fill(found[-1], found[0].size)
106
- found.transpose.each do |row|
107
- row = row.each_with_index.map { |item, col| item&.to_s(widths[col]) }
108
- yield(row.join)
109
- end
110
- end
111
-
112
- def find_columns(list, max_width)
113
- found = [list]
114
- widths = [list.max_by(&:width).width]
115
- 1.upto(list.size - 1) do |slice_size|
116
- candidate = list.each_slice(list.size / slice_size).to_a
117
- cwidths = candidate.map { _1.max_by(&:width).width + 3 }
118
- cwidths[-1] -= 3
119
- break if cwidths.sum > max_width
120
- found = candidate
121
- widths = cwidths
122
- end
123
- [found, widths]
124
- end
125
-
126
- def fill(ary, size)
127
- (diff = size - ary.size).positive? && ary.fill(nil, ary.size, diff)
128
- end
129
-
130
- Item =
131
- Struct.new(:str, :width) do
132
- def to_s(in_width) = "#{str}#{' ' * (in_width - width)}"
133
- end
134
-
135
- private_constant :Item
136
- end
137
- end
138
- end