natty-ui 0.12.1 → 0.26.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +22 -26
  4. data/examples/24bit-colors.rb +4 -7
  5. data/examples/3bit-colors.rb +28 -6
  6. data/examples/8bit-colors.rb +18 -21
  7. data/examples/attributes.rb +30 -22
  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/key-codes.rb +28 -0
  13. data/examples/ls.rb +32 -16
  14. data/examples/named-colors.rb +23 -0
  15. data/examples/sections.rb +26 -0
  16. data/examples/tables.rb +62 -0
  17. data/examples/tasks.rb +52 -0
  18. data/lib/natty-ui/attributes.rb +604 -0
  19. data/lib/natty-ui/choice.rb +56 -0
  20. data/lib/natty-ui/dumb_choice.rb +45 -0
  21. data/lib/natty-ui/element.rb +75 -0
  22. data/lib/natty-ui/features.rb +798 -0
  23. data/lib/natty-ui/framed.rb +51 -0
  24. data/lib/natty-ui/ls_renderer.rb +97 -0
  25. data/lib/natty-ui/progress.rb +179 -0
  26. data/lib/natty-ui/section.rb +66 -0
  27. data/lib/natty-ui/table.rb +241 -0
  28. data/lib/natty-ui/table_renderer.rb +155 -0
  29. data/lib/natty-ui/task.rb +47 -0
  30. data/lib/natty-ui/temporary.rb +36 -0
  31. data/lib/natty-ui/theme.rb +303 -0
  32. data/lib/natty-ui/utils.rb +79 -0
  33. data/lib/natty-ui/version.rb +1 -1
  34. data/lib/natty-ui/width_finder.rb +125 -0
  35. data/lib/natty-ui.rb +96 -148
  36. metadata +45 -53
  37. data/examples/animate.rb +0 -42
  38. data/examples/attributes_list.rb +0 -12
  39. data/examples/demo.rb +0 -51
  40. data/examples/message.rb +0 -30
  41. data/examples/progress.rb +0 -66
  42. data/examples/query.rb +0 -39
  43. data/examples/read_key.rb +0 -13
  44. data/examples/table.rb +0 -39
  45. data/lib/natty-ui/animation/binary.rb +0 -36
  46. data/lib/natty-ui/animation/default.rb +0 -38
  47. data/lib/natty-ui/animation/matrix.rb +0 -51
  48. data/lib/natty-ui/animation/rainbow.rb +0 -28
  49. data/lib/natty-ui/animation/type_writer.rb +0 -44
  50. data/lib/natty-ui/animation.rb +0 -69
  51. data/lib/natty-ui/ansi/constants.rb +0 -75
  52. data/lib/natty-ui/ansi.rb +0 -530
  53. data/lib/natty-ui/ansi_wrapper.rb +0 -232
  54. data/lib/natty-ui/frame.rb +0 -53
  55. data/lib/natty-ui/glyph.rb +0 -64
  56. data/lib/natty-ui/key_map.rb +0 -142
  57. data/lib/natty-ui/preload.rb +0 -12
  58. data/lib/natty-ui/spinner.rb +0 -120
  59. data/lib/natty-ui/text/east_asian_width.rb +0 -2529
  60. data/lib/natty-ui/text.rb +0 -203
  61. data/lib/natty-ui/wrapper/animate.rb +0 -17
  62. data/lib/natty-ui/wrapper/ask.rb +0 -78
  63. data/lib/natty-ui/wrapper/element.rb +0 -79
  64. data/lib/natty-ui/wrapper/features.rb +0 -21
  65. data/lib/natty-ui/wrapper/framed.rb +0 -45
  66. data/lib/natty-ui/wrapper/heading.rb +0 -64
  67. data/lib/natty-ui/wrapper/horizontal_rule.rb +0 -37
  68. data/lib/natty-ui/wrapper/list_in_columns.rb +0 -138
  69. data/lib/natty-ui/wrapper/message.rb +0 -109
  70. data/lib/natty-ui/wrapper/mixins.rb +0 -75
  71. data/lib/natty-ui/wrapper/progress.rb +0 -63
  72. data/lib/natty-ui/wrapper/query.rb +0 -89
  73. data/lib/natty-ui/wrapper/quote.rb +0 -25
  74. data/lib/natty-ui/wrapper/request.rb +0 -54
  75. data/lib/natty-ui/wrapper/section.rb +0 -118
  76. data/lib/natty-ui/wrapper/table.rb +0 -550
  77. data/lib/natty-ui/wrapper/task.rb +0 -55
  78. data/lib/natty-ui/wrapper.rb +0 -239
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ # {Element} with a frame around the content used by {Features.framed}.
7
+ #
8
+ class Framed < Element
9
+ # @!visibility private
10
+ def closed? = @bottom ? false : true
11
+
12
+ # @!visibility private
13
+ def puts(*objects, **options)
14
+ return self if closed?
15
+ options[:align] = @align
16
+ options[:expand] = true
17
+ super
18
+ end
19
+
20
+ # @!visibility private
21
+ def done
22
+ return if closed?
23
+ @parent.puts(@bottom)
24
+ @bottom = nil
25
+ end
26
+
27
+ # @!visibility private
28
+ def inspect = "#{_to_s.chop} align=#{@align.inspect} closed?=#{closed?}>"
29
+
30
+ private
31
+
32
+ def initialize(parent, align, chars, style, msg)
33
+ super(parent)
34
+ @align = align
35
+ if style
36
+ style = Ansi[*style]
37
+ @prefix = "#{style}#{chars[9]}[/] "
38
+ @suffix = " #{style}#{chars[9]}[/]"
39
+ else
40
+ @prefix = "#{chars[9]} "
41
+ @suffix = " #{chars[9]}"
42
+ end
43
+ @prefix_width = 2
44
+ @suffix_width = 2
45
+ line = chars[10] * (parent.columns - 2)
46
+ parent.puts("#{style}#{chars[0]}#{line}#{chars[2]}")
47
+ @bottom = "#{style}#{chars[6]}#{line}#{chars[8]}"
48
+ puts(*msg) unless msg.empty?
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ class LSRenderer
5
+ class << self
6
+ def lines(items, glyph, max_width)
7
+ items = as_items(items, glyph)
8
+ lines = []
9
+ width = items.max_by(&:width).width + 3
10
+ return lines if (sl_size = max_width / width).zero?
11
+ items.each_slice(sl_size) do |slice|
12
+ lines << slice.map { _1.to_s(width) }.join
13
+ end
14
+ lines
15
+ end
16
+
17
+ private
18
+
19
+ def as_items(items, glyph)
20
+ items.flatten!
21
+ glyph = as_glyph(glyph, items.size)
22
+ items.map! { Item.new(glyph[_1]) }
23
+ end
24
+
25
+ def as_glyph(glyph, size)
26
+ case glyph
27
+ when nil, false
28
+ lambda(&:itself)
29
+ when :hex
30
+ pad = [2, size.to_s(16).size].max
31
+ glyph = 0
32
+ ->(s) { "#{(glyph += 1).to_s(16).rjust(pad, '0')} #{s}" }
33
+ when /\A0x(\h+)\z/
34
+ glyph = Regexp.last_match(1).hex - 1
35
+ pad = [2, (glyph + size).to_s(16).size].max
36
+ ->(s) { "0x#{(glyph += 1).to_s(16).rjust(pad, '0')} #{s}" }
37
+ when Integer
38
+ pad = (glyph + size).to_s.size
39
+ glyph -= 1
40
+ ->(s) { "#{(glyph += 1).to_s.rjust(pad, ' ')} #{s}" }
41
+ when Symbol
42
+ ->(s) { "#{[glyph, glyph = glyph.succ][0]} #{s}" }
43
+ else
44
+ ->(s) { "#{glyph} #{s}" }
45
+ end
46
+ end
47
+
48
+ class Item
49
+ attr_reader :width
50
+
51
+ def to_s(in_width) = "#{@str}#{' ' * (in_width - @width)}"
52
+
53
+ def initialize(str)
54
+ @str = str
55
+ @width = Text.width(str)
56
+ end
57
+ end
58
+ private_constant :Item
59
+ end
60
+ end
61
+
62
+ class CompactLSRenderer < LSRenderer
63
+ class << self
64
+ def lines(items, glyph, max_width)
65
+ items = as_items(items, glyph)
66
+ return [] if items.empty?
67
+ found, widths = find_columns(items, max_width)
68
+ fill(found[-1], found[0].size)
69
+ found.transpose.map! do |row|
70
+ row.each_with_index.map { |item, col| item&.to_s(widths[col]) }.join
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def find_columns(items, max_width)
77
+ found = [items]
78
+ widths = [items.max_by(&:width).width]
79
+ 1.upto(items.size - 1) do |slice_size|
80
+ candidate = items.each_slice(items.size / slice_size).to_a
81
+ cwidths = candidate.map { _1.max_by(&:width).width + 3 }
82
+ cwidths[-1] -= 3
83
+ break if cwidths.sum > max_width
84
+ found = candidate
85
+ widths = cwidths
86
+ end
87
+ [found, widths]
88
+ end
89
+
90
+ def fill(ary, size)
91
+ (diff = size - ary.size).positive? && ary.fill(nil, ary.size, diff)
92
+ end
93
+ end
94
+ end
95
+
96
+ private_constant :LSRenderer, :CompactLSRenderer
97
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ # @todo This chapter needs more documentation.
7
+ #
8
+ # Progress indictaor helper used by {Features.progress}.
9
+ #
10
+ module ProgressHelper
11
+ include WithStatus
12
+
13
+ # @return [String]
14
+ attr_reader :title
15
+
16
+ # @attribute [w] title
17
+ def title=(value)
18
+ @title = value
19
+ redraw
20
+ end
21
+
22
+ # @return [Float]
23
+ attr_reader :value
24
+
25
+ # @attribute [w] value
26
+ def value=(val)
27
+ @value = val.to_f
28
+ @value = 0.0 if @value < 0
29
+ @max = @value if @max&.<(@value)
30
+ redraw
31
+ end
32
+
33
+ # @return [Float]
34
+ attr_reader :max
35
+
36
+ # @attribute [w] max
37
+ def max=(val)
38
+ @max = val.to_f
39
+ if @max <= 0
40
+ @max = nil
41
+ elsif @max < @value
42
+ @max = @value
43
+ end
44
+ redraw
45
+ end
46
+
47
+ def step(count: 1, title: nil)
48
+ @title = title if title
49
+ self.value += count
50
+ self
51
+ end
52
+
53
+ alias _to_s to_s
54
+ private :_to_s
55
+
56
+ # @!visibility private
57
+ def to_s
58
+ return "#{title}: #{format('%5.2f', @value)}" unless @max
59
+ "#{@title}: #{
60
+ format(
61
+ '%5.2f of %5.2f - %5.2f%%',
62
+ @value,
63
+ @max,
64
+ 100.0 * (@value / @max)
65
+ )
66
+ }"
67
+ end
68
+
69
+ # @!visibility private
70
+ def inspect = "#{_to_s.chop} #{self}>"
71
+ end
72
+
73
+ class Progress
74
+ include ProgressHelper
75
+
76
+ private
77
+
78
+ def _done(text)
79
+ NattyUI.back_to_line(@pin_line)
80
+ @pin_line = nil
81
+ cm = Theme.current.mark(:checkmark)
82
+ @parent.puts(
83
+ *text,
84
+ pin: @pin,
85
+ first_line_prefix: cm,
86
+ first_line_prefix_width: cm.width
87
+ )
88
+ end
89
+
90
+ def _failed
91
+ NattyUI.back_to_line(NattyUI.lines_written - 1) if @last&.size == 2
92
+ @pin_line = nil
93
+ end
94
+
95
+ def initialize(parent, title, max, pin)
96
+ @parent = parent
97
+ @value = 0
98
+ @title = title
99
+ @pin = pin
100
+ @pin_line = NattyUI.lines_written
101
+ @style = Theme.current.task_style
102
+ max ? self.max = max : redraw
103
+ end
104
+
105
+ def redraw
106
+ return if @status
107
+ bar = @max ? bar(@value / @max) : moving_bar
108
+ curr = bar ? [@title, bar] : [@title]
109
+ return if @last == curr
110
+ @pin_line = NattyUI.back_to_line(@pin_line) if @last
111
+ @parent.puts(
112
+ *curr,
113
+ first_line_prefix: "#{@style}➔ ",
114
+ first_line_prefix_width: 2
115
+ )
116
+ @last = curr
117
+ end
118
+
119
+ def moving_bar
120
+ "#{@style}#{'•' * @value}" if @value >= 1
121
+ end
122
+
123
+ def bar(diff)
124
+ size = [@parent.columns, 72].min - 11
125
+ percent = format('%5.2f', 100.0 * diff)
126
+ return percent if size < 10
127
+ return if percent == '100.00'
128
+ fill = '█' * (size * diff)
129
+ "#{percent}% [bright_black]┃#{@style}#{fill}[bright_black]#{
130
+ '░' * (size - fill.size)
131
+ }┃[/]"
132
+ end
133
+ end
134
+
135
+ class DumbProgress
136
+ include ProgressHelper
137
+
138
+ private
139
+
140
+ def _done(text)
141
+ cm = Theme.current.mark(:checkmark)
142
+ @parent.puts(
143
+ *text,
144
+ pin: @pin,
145
+ first_line_prefix: cm,
146
+ first_line_prefix_width: cm.width
147
+ )
148
+ end
149
+
150
+ def _failed
151
+ # nop
152
+ end
153
+
154
+ def initialize(parent, title, max)
155
+ @parent = parent
156
+ @value = @last_value = 0.0
157
+ @title = title
158
+ max ? self.max = max : redraw
159
+ end
160
+
161
+ def redraw
162
+ return if @status
163
+ if @last_title != @title
164
+ @parent.puts(
165
+ @last_title = @title,
166
+ first_line_prefix: '➔ ',
167
+ first_line_prefix_width: 2
168
+ )
169
+ end
170
+ return if @max.nil? || @value < 1
171
+ percent = format('%3.0f %%', 100.0 * (@value / @max))
172
+ return if @last_percent == percent
173
+ @parent.puts(percent, first_line_prefix: '  ', first_line_prefix_width: 2)
174
+ @last_percent = percent
175
+ end
176
+ end
177
+
178
+ private_constant :Progress, :DumbProgress
179
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ # Display section used by
7
+ #
8
+ # - {Features.section}
9
+ # - {Features.message}
10
+ # - {Features.information}
11
+ # - {Features.warning}
12
+ # - {Features.error}
13
+ # - {Features.failed}
14
+ #
15
+ class Section < Element
16
+ include WithStatus
17
+
18
+ # @!visibility private
19
+ def puts(*objects, **options) = @state ? self : super
20
+
21
+ private
22
+
23
+ def _done(text)
24
+ puts(*text) unless text.empty?
25
+ @parent.puts(@border.bottom)
26
+ end
27
+
28
+ def _failed
29
+ @parent.puts(@border.bottom)
30
+ end
31
+
32
+ def show_title(title)
33
+ return @parent.puts(@border.top) unless title
34
+ prefix = @border.top_left
35
+ suffix = @border.top_right
36
+ @parent.puts(
37
+ title,
38
+ prefix: prefix,
39
+ prefix_width: prefix.width,
40
+ suffix: suffix,
41
+ suffix_width: suffix.width
42
+ )
43
+ end
44
+
45
+ def initialize(parent, title, msg, kind)
46
+ super(parent)
47
+ title, rest = split(title) if title && !title.empty?
48
+ @border = Theme.current.section_border(kind)
49
+ show_title(title)
50
+ @prefix = @border.prefix
51
+ @prefix_width = @prefix.size
52
+ puts(*rest) if rest && !rest.empty?
53
+ puts(*msg) unless msg.empty?
54
+ end
55
+
56
+ def split(title)
57
+ rest =
58
+ Text.each_line(
59
+ title,
60
+ limit: @parent.columns - 9,
61
+ ansi: Terminal.ansi?
62
+ ).to_a
63
+ [rest.shift, rest]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attributes'
4
+ require_relative 'table_renderer'
5
+
6
+ module NattyUI
7
+ # @todo This chapter needs more documentation.
8
+ #
9
+ # Collection of rows and columns used by {Features.table}.
10
+ #
11
+ class Table
12
+ class Column < NattyUI::Attributes::Base
13
+ include NattyUI::Attributes::Width
14
+
15
+ # @return [Integer] column index
16
+ attr_reader :index
17
+
18
+ def width
19
+ @width ||= find_width
20
+ end
21
+
22
+ def to_s = "#{super.chop} @index:#{@index} @width:#{width.inspect}>"
23
+ alias inspect to_s
24
+
25
+ private
26
+
27
+ def find_width
28
+ min = max = nil
29
+ @parent.each_cell_of(@index) do |cell|
30
+ next unless cell
31
+ m = cell.attributes.min_width
32
+ min = m if m && (min.nil? || m < min)
33
+ m = cell.attributes.max_width
34
+ max = m if m && (max.nil? || max < m)
35
+ end
36
+ wh_from(min.to_i, max.to_i)
37
+ end
38
+
39
+ def respond_to_missing?(name, _)
40
+ return suoer unless name.end_with?('=')
41
+ Cell::Attributes.public_method_defined?(name) || super
42
+ end
43
+
44
+ def method_missing(name, *args, **kwargs)
45
+ return super unless name.end_with?('=')
46
+ return super unless Cell::Attributes.public_method_defined?(name)
47
+ @parent.each_cell_of(@index) { _1.attributes.__send__(name, *args) }
48
+ args[0]
49
+ end
50
+
51
+ def initialize(parent, index, **attributes)
52
+ super(**attributes)
53
+ @parent = parent
54
+ @index = index
55
+ end
56
+ end
57
+
58
+ class ColumnsCollection
59
+ include Enumerable
60
+
61
+ # @return [Integer] count of columns
62
+ def count = columns.size
63
+
64
+ def empty? = columns.empty?
65
+
66
+ # @return [Column, nil] column at given index
67
+ def [](index) = columns[index]
68
+
69
+ def each(&block) = columns.each(&block)
70
+
71
+ def to_s = "#{super.chop} @columns:#{columns.inspect}>"
72
+ alias inspect to_s
73
+
74
+ private
75
+
76
+ def columns
77
+ cc = @parent.column_count
78
+ case cc <=> @columns.size
79
+ when -1
80
+ @columns = @columns.take(cc)
81
+ when 1
82
+ bi = @columns.size
83
+ @columns +=
84
+ Array.new(cc - @columns.size) { Column.new(@parent, bi + _1) }
85
+ else
86
+ @columns
87
+ end
88
+ end
89
+
90
+ def initialize(parent)
91
+ @parent = parent
92
+ @columns = []
93
+ end
94
+
95
+ def initialize_copy(*_)
96
+ super
97
+ @columns = []
98
+ end
99
+ end
100
+
101
+ class Row
102
+ include Enumerable
103
+
104
+ def empty? = @cells.empty?
105
+
106
+ # @return [Integer] count of cells
107
+ def count = @cells.size
108
+
109
+ def each(&block) = @cells.each(&block)
110
+
111
+ # @return [Cell, nil] cell at given index
112
+ def [](index) = @cells[index]
113
+
114
+ # @return [Cell] created cell
115
+ def []=(index, *args)
116
+ @cells[index] = create_cell(args)
117
+ @cells.map! { _1 || Cell.new }
118
+ end
119
+
120
+ # Add a new cell to the row with given `text` and `attributes`.
121
+ # @return [Cell] created cell
122
+ def add(*text, **attributes)
123
+ nc = Cell.new(*text, **attributes)
124
+ @cells << nc
125
+ block_given? ? yield(nc) : nc
126
+ end
127
+
128
+ def delete(cell)
129
+ cell.is_a?(Cell) ? @cells.delete(cell) : @cells.delete_at(cell)
130
+ self
131
+ end
132
+
133
+ # Add a new cell to the row with given `text`.
134
+ # @return [Row] itself
135
+ def <<(text)
136
+ add(text)
137
+ self
138
+ end
139
+
140
+ private
141
+
142
+ def respond_to_missing?(name, _)
143
+ return super unless name.end_with?('=')
144
+ Cell::Attributes.public_method_defined?(name) || super
145
+ end
146
+
147
+ def method_missing(name, *args, **_)
148
+ return super unless name.end_with?('=')
149
+ return super unless Cell::Attributes.public_method_defined?(name)
150
+ @cells.each { _1.attributes.__send__(name, *args) }
151
+ args[0]
152
+ end
153
+
154
+ def initialize
155
+ @cells = []
156
+ end
157
+
158
+ def initialize_copy(*_)
159
+ super
160
+ @cells = @cells.map(&:dup)
161
+ end
162
+
163
+ def create_cell(args)
164
+ args.flatten!(1)
165
+ Cell.new(*args)
166
+ end
167
+ end
168
+
169
+ class Cell
170
+ include TextWithAttributes
171
+
172
+ class Attributes < NattyUI::Attributes::Base
173
+ prepend NattyUI::Attributes::Width
174
+ prepend NattyUI::Attributes::Padding
175
+ prepend NattyUI::Attributes::Align
176
+ prepend NattyUI::Attributes::Vertical
177
+ prepend NattyUI::Attributes::Style
178
+ end
179
+ end
180
+
181
+ class Attributes < NattyUI::Attributes::Base
182
+ prepend NattyUI::Attributes::Border
183
+ prepend NattyUI::Attributes::BorderStyle
184
+ prepend NattyUI::Attributes::BorderAround
185
+ end
186
+
187
+ include Enumerable
188
+ include WithAttributes
189
+
190
+ # @return [ColumnsCollection] table columns
191
+ attr_reader :columns
192
+
193
+ def empty? = @rows.empty?
194
+
195
+ # @return [Integer] count of rows
196
+ def count = @rows.size
197
+
198
+ # @return [Integer] count of columns
199
+ def column_count = @rows.empty? ? 0 : @rows.max_by(&:count).count
200
+
201
+ def each(&block) = @rows.each(&block)
202
+
203
+ def [](row_index, column_index = nil)
204
+ row = @rows[row_index] or return
205
+ column_index ? row[column_index] : row
206
+ end
207
+
208
+ # @return [Row] created row
209
+ def add(*text, **attributes)
210
+ nr = Row.new
211
+ @rows << nr
212
+ text.each { nr.add(_1, **attributes) }
213
+ block_given? ? yield(nr) : nr
214
+ end
215
+
216
+ def delete(row)
217
+ row.is_a?(Row) ? @rows.delete(row) : @rows.delete_at(row)
218
+ end
219
+
220
+ def each_cell_of(column_index)
221
+ return to_enum(__method__, column_index) unless block_given?
222
+ return if (column_index = column_index.to_i) >= column_count
223
+ @rows.each { yield(_1[column_index]) }
224
+ nil
225
+ end
226
+
227
+ private
228
+
229
+ def initialize(**attributes)
230
+ super
231
+ @rows = []
232
+ @columns = ColumnsCollection.new(self)
233
+ end
234
+
235
+ def initialize_copy(*_)
236
+ super
237
+ @columns = ColumnsCollection.new(self)
238
+ @rows = @rows.map(&:dup)
239
+ end
240
+ end
241
+ end