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
@@ -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,93 @@
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| Item.new(item = glyph[item], Text.width(item)) }
23
+ end
24
+
25
+ def as_glyph(glyph, size)
26
+ case glyph
27
+ when nil, false
28
+ lambda(&:itself)
29
+ when :hex
30
+ pad = size.to_s(16).size
31
+ glyph = 0
32
+ ->(s) { "#{(glyph += 1).to_s(16).rjust(pad, '0')} #{s}" }
33
+ when Integer
34
+ pad = (glyph + size).to_s.size
35
+ glyph -= 1
36
+ ->(s) { "#{(glyph += 1).to_s.rjust(pad)} #{s}" }
37
+ when Symbol
38
+ lambda do |s|
39
+ "#{
40
+ t = glyph
41
+ glyph = glyph.succ
42
+ t
43
+ } #{s}[/]"
44
+ end
45
+ else
46
+ ->(s) { "#{glyph} #{s}" }
47
+ end
48
+ end
49
+
50
+ Item =
51
+ Struct.new(:str, :width) do
52
+ def to_s(in_width) = "#{str}#{' ' * (in_width - width)}"
53
+ end
54
+ private_constant :Item
55
+ end
56
+ end
57
+
58
+ class CompactLSRenderer < LSRenderer
59
+ class << self
60
+ def lines(items, glyph, max_width)
61
+ items = as_items(items, glyph)
62
+ return [] if items.empty?
63
+ found, widths = find_columns(items, max_width)
64
+ fill(found[-1], found[0].size)
65
+ found.transpose.map! do |row|
66
+ row.each_with_index.map { |item, col| item&.to_s(widths[col]) }.join
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def find_columns(items, max_width)
73
+ found = [items]
74
+ widths = [items.max_by(&:width).width]
75
+ 1.upto(items.size - 1) do |slice_size|
76
+ candidate = items.each_slice(items.size / slice_size).to_a
77
+ cwidths = candidate.map { _1.max_by(&:width).width + 3 }
78
+ cwidths[-1] -= 3
79
+ break if cwidths.sum > max_width
80
+ found = candidate
81
+ widths = cwidths
82
+ end
83
+ [found, widths]
84
+ end
85
+
86
+ def fill(ary, size)
87
+ (diff = size - ary.size).positive? && ary.fill(nil, ary.size, diff)
88
+ end
89
+ end
90
+ end
91
+
92
+ private_constant :LSRenderer, :CompactLSRenderer
93
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ # @todo This chapter needs more documentation.
5
+ #
6
+ # Progress indictaor helper used by {Features.progress}.
7
+ #
8
+ module ProgressHelper
9
+ # @return [String]
10
+ attr_reader :title
11
+
12
+ # @attribute [w] title
13
+ def title=(value)
14
+ @title = value
15
+ redraw
16
+ end
17
+
18
+ # @return [Float]
19
+ attr_reader :value
20
+
21
+ # @attribute [w] value
22
+ def value=(val)
23
+ @value = val.to_f
24
+ @value = 0.0 if @value < 0
25
+ @max = @value if @max&.<(@value)
26
+ redraw
27
+ end
28
+
29
+ # @return [Float]
30
+ attr_reader :max
31
+
32
+ # @attribute [w] max
33
+ def max=(val)
34
+ @max = val.to_f
35
+ if @max <= 0
36
+ @max = nil
37
+ elsif @max < @value
38
+ @max = @value
39
+ end
40
+ redraw
41
+ end
42
+
43
+ def active? = @status.nil?
44
+ def ok? = @status == :ok
45
+ def failed? = @status == :failed
46
+
47
+ def step(count: 1, title: nil)
48
+ @title = title if title
49
+ self.value += count
50
+ self
51
+ end
52
+
53
+ def done(title = nil) = finish(:ok, title)
54
+ alias ok done
55
+
56
+ def failed(title = nil) = finish(:failed, title)
57
+
58
+ alias _to_s to_s
59
+ private :_to_s
60
+
61
+ # @!visibility private
62
+ def to_s
63
+ return "#{title}: #{format('%5.2f', @value)}" unless @max
64
+ "#{@title}: #{
65
+ format(
66
+ '%5.2f of %5.2f - %5.2f%%',
67
+ @value,
68
+ @max,
69
+ 100.0 * (@value / @max)
70
+ )
71
+ }"
72
+ end
73
+
74
+ # @!visibility private
75
+ def inspect = "#{_to_s.chop} #{self}>"
76
+ end
77
+
78
+ class Progress
79
+ include ProgressHelper
80
+
81
+ private
82
+
83
+ def finish(status, title)
84
+ return if @status
85
+ @status = status
86
+ NattyUI.back_to_line(@pin_line)
87
+ if status == :failed
88
+ @parent.failed(title || @title)
89
+ else
90
+ cm = Theme.current.mark(:checkmark)
91
+ @parent.puts(
92
+ title || @title,
93
+ pin: @pin,
94
+ first_line_prefix: cm,
95
+ first_line_prefix_width: cm.width
96
+ )
97
+ end
98
+ self
99
+ end
100
+
101
+ def initialize(parent, title, max, pin)
102
+ @parent = parent
103
+ @value = 0.0
104
+ @title = title
105
+ @pin = pin
106
+ @pin_line = NattyUI.lines_written
107
+ @style = Theme.current.task_style
108
+ max ? self.max = max : redraw
109
+ end
110
+
111
+ def redraw
112
+ return if @status
113
+ bar = @max ? bar(@value / @max) : moving_bar
114
+ curr = bar ? [@title, bar] : [@title]
115
+ return if @last == curr
116
+ @pin_line = NattyUI.back_to_line(@pin_line) if @last
117
+ @parent.puts(
118
+ *curr,
119
+ first_line_prefix: "#{@style}➔ ",
120
+ first_line_prefix_width: 2
121
+ )
122
+ @last = curr
123
+ end
124
+
125
+ def moving_bar
126
+ "#{@style}#{'·' * @value}" if @value >= 1
127
+ end
128
+
129
+ def bar(diff)
130
+ size = [@parent.columns, 72].min - 11
131
+ percent = format('%5.2f', 100.0 * diff)
132
+ return percent if size < 10
133
+ return if percent == '100.00'
134
+ fill = '█' * (size * diff)
135
+ "#{percent}% [bright_black]┃#{@style}#{fill}[bright_black]#{
136
+ '░' * (size - fill.size)
137
+ }┃[/]"
138
+ end
139
+ end
140
+
141
+ class DumbProgress
142
+ include ProgressHelper
143
+
144
+ private
145
+
146
+ def finish(status, title)
147
+ return if @status
148
+ @status = status
149
+ if status == :failed
150
+ @parent.failed(title || @title)
151
+ else
152
+ cm = Theme.current.mark(:checkmark)
153
+ @parent.puts(
154
+ title || @title,
155
+ first_line_prefix: cm,
156
+ first_line_prefix_width: cm.width
157
+ )
158
+ end
159
+ self
160
+ end
161
+
162
+ def initialize(parent, title, max)
163
+ @parent = parent
164
+ @value = @last_value = 0.0
165
+ @title = title
166
+ max ? self.max = max : redraw
167
+ end
168
+
169
+ def redraw
170
+ return if @status
171
+ if @last_title != @title
172
+ @parent.puts(
173
+ @last_title = @title,
174
+ first_line_prefix: '➔ ',
175
+ first_line_prefix_width: 2
176
+ )
177
+ end
178
+ return if @max.nil? || @value < 1
179
+ percent = format('%3.0f %%', 100.0 * (@value / @max))
180
+ return if @last_percent == percent
181
+ @parent.puts(percent, first_line_prefix: '  ', first_line_prefix_width: 2)
182
+ @last_percent = percent
183
+ end
184
+ end
185
+
186
+ private_constant :Progress, :DumbProgress
187
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ # {Element} implemting a 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 StateMixin
17
+
18
+ # @!visibility private
19
+ def puts(*objects, **options) = @state ? self : super
20
+
21
+ private
22
+
23
+ def finish_ok(text)
24
+ puts(*text) unless text.empty?
25
+ @state = :ok
26
+ @parent.puts(@border.bottom)
27
+ self
28
+ end
29
+
30
+ def finish_failed
31
+ @parent.puts(@border.bottom)
32
+ super
33
+ end
34
+
35
+ def show_title(title)
36
+ return @parent.puts(@border.top) unless title
37
+ prefix = @border.top_left
38
+ suffix = @border.top_right
39
+ @parent.puts(
40
+ title,
41
+ prefix: prefix,
42
+ prefix_width: prefix.width,
43
+ suffix: suffix,
44
+ suffix_width: suffix.width
45
+ )
46
+ end
47
+
48
+ def initialize(parent, title, msg, kind)
49
+ super(parent)
50
+ title, rest = split(title) if title && !title.empty?
51
+ @border = Theme.current.section_border(kind)
52
+ show_title(title)
53
+ @prefix = @border.prefix
54
+ @prefix_width = @prefix.size
55
+ puts(*rest) if rest && !rest.empty?
56
+ puts(*msg) unless msg.empty?
57
+ end
58
+
59
+ def split(title)
60
+ rest =
61
+ Text.each_line(
62
+ title,
63
+ limit: @parent.columns - 9,
64
+ ansi: Terminal.ansi?
65
+ ).to_a
66
+ [rest.shift, rest]
67
+ end
68
+ end
69
+ 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