natty-ui 0.30.0 → 0.31.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.
@@ -11,342 +11,386 @@ module NattyUI
11
11
  class << self
12
12
  # Currently used theme
13
13
  #
14
- # @return [Compiled]
14
+ # @return [Theme]
15
15
  attr_reader :current
16
16
 
17
- # @attribute [w] current
18
- def current=(value)
19
- case value
20
- when Theme::Compiled
21
- @current = value
22
- when Theme
23
- @current = value.compiled
24
- else
25
- raise(TypeError, 'Theme expected')
26
- end
27
- end
28
-
29
- # Create a theme.
17
+ # Name of currently used theme
30
18
  #
31
- # @return [Theme] new theme
32
- def create
33
- theme = new
34
- yield(theme) if block_given?
35
- theme
36
- end
19
+ # @return [Symbol]
20
+ attr_reader :current_name
37
21
 
38
- # Default theme.
22
+ # Names of all registered themes
39
23
  #
40
- # @attribute [r] default
41
- # @return [Theme] default theme
42
- def default
43
- create do |theme|
44
- theme.heading_sytle = :bright_blue
45
- theme.task_style = %i[bright_green b]
46
- # theme.choice_style =
47
- theme.choice_current_style = %i[bright_white on_blue b]
48
- theme.define_marker(
49
- bullet: '[bright_white]•[/fg]',
50
- checkmark: '[bright_green]✓[/fg]',
51
- quote: '[bright_blue]▍[/fg]',
52
- information: '[bright_yellow]𝒊[/fg]',
53
- warning: '[bright_yellow]![/fg]',
54
- error: '[red]𝙓[/fg]',
55
- failed: '[bright_red]𝑭[/fg]',
56
- current: '[bright_green]➔[/fg]',
57
- choice: '[bright_white]◦[/fg]',
58
- current_choice: '[bright_green]➔[/fg]'
59
- )
60
- theme.define_section(
61
- default: :bright_blue,
62
- message: :bright_blue,
63
- information: :bright_blue,
64
- warning: :bright_yellow,
65
- error: :red,
66
- failed: :bright_red
67
- )
68
- end
69
- end
24
+ # @attribute [r] names
25
+ # @return [Array<Symbol>]
26
+ def names = @ll.keys.sort!
70
27
 
71
- def emoji
72
- create do |theme|
73
- theme.heading_sytle = :bright_blue
74
- theme.task_style = %i[bright_green b]
75
- # theme.choice_style =
76
- theme.choice_current_style = %i[bright_white on_blue b]
77
- theme.define_marker(
78
- bullet: '▫️',
79
- checkmark: '✅',
80
- quote: '[bright_blue]▍[/fg]',
81
- information: '📌',
82
- warning: '⚠️',
83
- error: '❗️',
84
- failed: '‼️',
85
- current: '➡️',
86
- choice: '[bright_white]•[/fg]',
87
- current_choice: '[bright_green]●[/fg]'
88
- )
89
- theme.define_section(
90
- default: :bright_blue,
91
- message: :bright_blue,
92
- information: :bright_blue,
93
- warning: :bright_yellow,
94
- error: :red,
95
- failed: :bright_red
96
- )
28
+ # Use a theme
29
+ #
30
+ # @param name [Symbol]
31
+ # @return [Symbol] name of used theme
32
+ def use(name)
33
+ sel = find(name).last
34
+ if sel.is_a?(Proc)
35
+ sel[builder = Builder.new]
36
+ sel = @ll[name][-1] = builder.build.freeze
97
37
  end
38
+ @current = sel
39
+ @current_name = name
98
40
  end
99
- end
100
41
 
101
- attr_accessor :section_border
102
- attr_reader :mark,
103
- :border,
104
- :heading,
105
- :heading_sytle,
106
- :section_styles,
107
- :task_style,
108
- :choice_current_style,
109
- :choice_style
42
+ # Get the descrition of a theme.
43
+ #
44
+ # @param name [Symbol]
45
+ # @return [String] description of the theme
46
+ def description(name) = find(name).first
110
47
 
111
- def heading_sytle=(value)
112
- @heading_sytle = Utils.style(value)
113
- end
48
+ # Register a theme
49
+ #
50
+ # @param name [Symbol] theme name
51
+ # @param description [#to_s] theme description
52
+ # @yieldparam builder [Builder] theme build helper
53
+ # @return [Theme] itself
54
+ def register(name, description = nil, &block)
55
+ raise(ArgumentError, 'block missing') unless block
56
+ @ll[name.to_sym] = [description&.to_s || name.to_s.capitalize, block]
57
+ self
58
+ end
114
59
 
115
- def task_style=(value)
116
- @task_style = Utils.style(value)
117
- end
60
+ private
118
61
 
119
- def choice_current_style=(value)
120
- @choice_current_style = Utils.style(value)
62
+ def find(name)
63
+ @ll.fetch(name) { raise(ArgumentError, "no such theme - #{name}") }
64
+ end
121
65
  end
122
66
 
123
- def choice_style=(value)
124
- @choice_style = Utils.style(value)
125
- end
67
+ # @todo This chapter needs more documentation.
68
+ #
69
+ # Helper class to define a {Theme}.
70
+ #
71
+ class Builder
72
+ attr_accessor :section_border
73
+ attr_reader :mark,
74
+ :border,
75
+ :heading,
76
+ :heading_sytle,
77
+ :section_styles,
78
+ :task_style,
79
+ :choice_current_style,
80
+ :choice_style,
81
+ :sh_out_style,
82
+ :sh_err_style
126
83
 
127
- def compiled = Compiled.new(self).freeze
84
+ def heading_sytle=(value)
85
+ @heading_sytle = Utils.style(value)
86
+ end
128
87
 
129
- def define_marker(**defs)
130
- @mark.merge!(defs)
131
- self
132
- end
88
+ def task_style=(value)
89
+ @task_style = Utils.style(value)
90
+ end
133
91
 
134
- def define_border(**defs)
135
- defs.each_pair do |name, str|
136
- s = str.to_s
137
- case Text.width(s, bbcode: false)
138
- when 1
139
- @border[name.to_sym] = "#{s * 11}  "
140
- when 11
141
- @border[name.to_sym] = "#{s}  "
142
- when 13
143
- @border[name.to_sym] = s
144
- else
145
- raise(
146
- TypeError,
147
- "invalid boder definition for #{name} - #{str.inspect}"
148
- )
149
- end
92
+ def choice_current_style=(value)
93
+ @choice_current_style = Utils.style(value)
150
94
  end
151
- self
152
- end
153
95
 
154
- def define_heading(*defs)
155
- @heading = defs.flatten.take(6)
156
- @heading += Array.new(6 - @heading.size, @heading[-1])
157
- self
158
- end
96
+ def choice_style=(value)
97
+ @choice_style = Utils.style(value)
98
+ end
159
99
 
160
- def define_section(**defs)
161
- defs.each_pair do |name, style|
162
- style = Utils.style(style)
163
- @section_styles[name.to_sym] = style if style
100
+ def sh_out_style=(value)
101
+ @sh_out_style = Utils.style(value)
164
102
  end
165
- self
166
- end
167
103
 
168
- class Compiled
169
- attr_reader :task_style,
170
- :choice_current_style,
171
- :choice_style,
172
- :option_states
104
+ def sh_err_style=(value)
105
+ @sh_err_style = Utils.style(value)
106
+ end
173
107
 
174
- def defined_marks = @mark.keys.sort!
175
- def defined_borders = @border.keys.sort!
176
- def heading(index) = @heading[index.to_i.clamp(1, 6) - 1]
108
+ # @return [Theme] new theme
109
+ def build = Theme.new(self)
177
110
 
178
- def mark(value)
179
- return @mark[value] if value.is_a?(Symbol)
180
- (element = Str.new(value, true)).empty? ? @mark[:default] : element
111
+ def define_marker(**defs)
112
+ @mark.merge!(defs)
113
+ self
181
114
  end
182
115
 
183
- def border(value)
184
- return @border[value] if value.is_a?(Symbol)
185
- case Text.width(value = value.to_s, bbcode: false)
186
- when 1
187
- "#{value * 11}  "
188
- when 11
189
- "#{value}  "
190
- when 13
191
- value
192
- else
193
- @border[:default]
116
+ def define_border(**defs)
117
+ defs.each_pair do |name, str|
118
+ s = str.to_s
119
+ case Text.width(s, bbcode: false)
120
+ when 1
121
+ @border[name.to_sym] = "#{s * 11}  "
122
+ when 11
123
+ @border[name.to_sym] = "#{s}  "
124
+ when 13
125
+ @border[name.to_sym] = s
126
+ else
127
+ raise(
128
+ TypeError,
129
+ "invalid boder definition for #{name} - #{str.inspect}"
130
+ )
131
+ end
194
132
  end
133
+ self
195
134
  end
196
135
 
197
- def section_border(kind)
198
- kind.is_a?(Symbol) ? @sections[kind] : @sections[:default]
136
+ def define_heading(*defs)
137
+ @heading = defs.flatten.take(6)
138
+ @heading += Array.new(6 - @heading.size, @heading[-1])
139
+ self
199
140
  end
200
141
 
201
- def initialize(theme)
202
- @heading = create_heading(theme.heading, theme.heading_sytle).freeze
203
- @border = create_border(theme.border).freeze
204
- @mark = create_mark(theme.mark).freeze
205
- @task_style = as_style(theme.task_style)
206
- @choice_current_style = as_style(theme.choice_current_style)
207
- @choice_style = as_style(theme.choice_style)
208
- @sections =
209
- create_sections(
210
- SectionBorder.create(border(theme.section_border)),
211
- theme.section_styles.dup.compare_by_identity
212
- )
213
- @option_states = create_option_states
142
+ def define_section(**defs)
143
+ defs.each_pair do |name, style|
144
+ style = Utils.style(style)
145
+ @section_styles[name.to_sym] = style if style
146
+ end
147
+ self
214
148
  end
215
149
 
216
150
  private
217
151
 
218
- def as_style(value) = (Ansi[*value].freeze if value)
219
-
220
- def create_option_states
221
- # [current?][selected?]
222
- c = @mark[:current_choice]
223
- n = @mark[:none]
224
- sel = @mark[:checkmark]
225
- uns = @mark[:choice]
226
- {
227
- false => { false => n + uns, true => n + sel }.compare_by_identity,
228
- true => { false => c + uns, true => c + sel }.compare_by_identity
229
- }.compare_by_identity.freeze
152
+ def initialize
153
+ define_heading(%w[╸╸╺╸╺━━━ ╴╶╴╶─═══ ╴╶╴╶─── ════ ━━━━ ────])
154
+ @mark = {
155
+ default: '•',
156
+ bullet: '•',
157
+ checkmark: '✓',
158
+ quote: '▍',
159
+ information: '𝒊',
160
+ warning: '!',
161
+ error: '𝙓',
162
+ failed: '𝑭',
163
+ current: '➔',
164
+ choice: '◦',
165
+ current_choice: '◉',
166
+ sh_out: ':',
167
+ sh_err: '𝙓'
168
+ }
169
+ @border = {
170
+ ######### 0123456789012
171
+ default: '┌┬┐├┼┤└┴┘│─╶╴',
172
+ defaulth: '───────── ─╶╴',
173
+ defaultv: '││││││││││ ',
174
+ double: '╔╦╗╠╬╣╚╩╝║═',
175
+ doubleh: '═════════ ═',
176
+ doublev: '║║║║║║║║║║ ',
177
+ heavy: '┏┳┓┣╋┫┗┻┛┃━╺╸',
178
+ heavyh: '━━━━━━━━━ ━╺╸',
179
+ heavyv: '┃┃┃┃┃┃┃┃┃┃ ',
180
+ rounded: '╭┬╮├┼┤╰┴╯│─╶╴'
181
+ }
182
+ @section_border = :rounded
183
+ @section_styles = {}
230
184
  end
185
+ end
231
186
 
232
- def create_sections(template, styles)
233
- Hash
234
- .new do |h, kind|
235
- h[kind] = SectionBorder.new(*template.parts(styles[kind])).freeze
236
- end
237
- .compare_by_identity
238
- end
187
+ attr_reader :task_style,
188
+ :choice_current_style,
189
+ :choice_style,
190
+ :sh_out_style,
191
+ :sh_err_style,
192
+ :option_states
193
+
194
+ def defined_marks = @mark.keys.sort!
195
+ def defined_borders = @border.keys.sort!
196
+ def heading(index) = @heading[index.to_i.clamp(1, 6) - 1]
197
+
198
+ def mark(value)
199
+ return @mark[value] if value.is_a?(Symbol)
200
+ (element = Str.new(value, true)).empty? ? @mark[:default] : element
201
+ end
239
202
 
240
- def create_mark(mark)
241
- return {} if mark.empty?
242
- mark = mark.to_h { |n, e| [n.to_sym, Str.new("#{e} ")] }
243
- mark[:none] ||= Str.new('  ', 2)
244
- with_default(mark)
203
+ def border(value)
204
+ return @border[value] if value.is_a?(Symbol)
205
+ case Text.width(value = value.to_s, bbcode: false)
206
+ when 1
207
+ "#{value * 11}  "
208
+ when 11
209
+ "#{value}  "
210
+ when 13
211
+ value
212
+ else
213
+ @border[:default]
245
214
  end
215
+ end
246
216
 
247
- def create_border(border)
248
- return {} if border.empty?
249
- with_default(border.transform_values { _1.dup.freeze })
250
- end
217
+ def section_border(kind)
218
+ kind.is_a?(Symbol) ? @sections[kind] : @sections[:default]
219
+ end
251
220
 
252
- def create_heading(heading, style)
253
- return create_styled_heading(heading, style) if style
254
- heading.map do |left|
255
- right = " #{left.reverse}"
256
- [left = Str.new("#{left} ", true), Str.new(right, left.width)]
257
- end
258
- end
221
+ def initialize(theme)
222
+ @heading = create_heading(theme.heading, theme.heading_sytle).freeze
223
+ @border = create_border(theme.border).freeze
224
+ @mark = create_mark(theme.mark).freeze
225
+ @task_style = as_style(theme.task_style)
226
+ @choice_current_style = as_style(theme.choice_current_style)
227
+ @choice_style = as_style(theme.choice_style)
228
+ @sh_out_style = as_style(theme.sh_out_style)
229
+ @sh_err_style = as_style(theme.sh_err_style)
230
+ @sections =
231
+ create_sections(
232
+ SectionBorder.create(border(theme.section_border)),
233
+ theme.section_styles.dup.compare_by_identity
234
+ )
235
+ @option_states = create_option_states
236
+ end
259
237
 
260
- def create_styled_heading(heading, style)
261
- heading.map do |left|
262
- right = Ansi.decorate(left.reverse, *style)
263
- [
264
- left = Str.new("#{Ansi.decorate(left, *style)} ", true),
265
- Str.new(" #{right}", left.width)
266
- ]
238
+ private
239
+
240
+ def as_style(value) = (Ansi[*value].freeze if value)
241
+
242
+ def create_option_states
243
+ # [current?][selected?]
244
+ c = @mark[:current_choice]
245
+ n = @mark[:none]
246
+ sel = @mark[:checkmark]
247
+ uns = @mark[:choice]
248
+ {
249
+ false => { false => n + uns, true => n + sel }.compare_by_identity,
250
+ true => { false => c + uns, true => c + sel }.compare_by_identity
251
+ }.compare_by_identity.freeze
252
+ end
253
+
254
+ def create_sections(template, styles)
255
+ Hash
256
+ .new do |h, kind|
257
+ h[kind] = SectionBorder.new(*template.parts(styles[kind])).freeze
267
258
  end
259
+ .compare_by_identity
260
+ end
261
+
262
+ def create_mark(mark)
263
+ return {} if mark.empty?
264
+ mark = mark.to_h { |n, e| [n.to_sym, Str.new("#{e} ")] }
265
+ mark[:none] ||= Str.new('  ', 2)
266
+ with_default(mark)
267
+ end
268
+
269
+ def create_border(border)
270
+ return {} if border.empty?
271
+ with_default(border.transform_values { _1.dup.freeze })
272
+ end
273
+
274
+ def create_heading(heading, style)
275
+ return create_styled_heading(heading, style) if style
276
+ heading.map do |left|
277
+ right = " #{left.reverse}"
278
+ [left = Str.new("#{left} ", true), Str.new(right, left.width)]
268
279
  end
280
+ end
269
281
 
270
- def with_default(map)
271
- map.default = (map[:default] ||= map[map.first.first])
272
- map.compare_by_identity
282
+ def create_styled_heading(heading, style)
283
+ heading.map do |left|
284
+ right = Ansi.decorate(left.reverse, *style)
285
+ [
286
+ left = Str.new("#{Ansi.decorate(left, *style)} ", true),
287
+ Str.new(" #{right}", left.width)
288
+ ]
273
289
  end
290
+ end
274
291
 
275
- SectionBorder =
276
- Struct.new(:top, :top_left, :top_right, :bottom, :prefix) do
277
- def self.create(border)
278
- mid = border[10] * 2
279
- mid2 = mid * 2
280
- right = "#{border[11]}#{border[12] * 2}"
281
- new(
282
- border[0] + mid2 + right,
283
- border[0] + mid,
284
- mid + right,
285
- border[6] + mid2 + right,
286
- border[9]
287
- )
288
- end
292
+ def with_default(map)
293
+ map.default = (map[:default] ||= map[map.first.first])
294
+ map.compare_by_identity
295
+ end
289
296
 
290
- def parts(style)
291
- unless style
292
- return [
293
- Str.new(top, 6),
294
- Str.new("#{top_left} ", 4),
295
- Str.new(" #{top_right}", 6),
296
- Str.new(bottom, 6),
297
- Str.new("#{prefix} ", 2)
298
- ]
299
- end
297
+ SectionBorder =
298
+ Struct.new(:top, :top_left, :top_right, :bottom, :prefix) do
299
+ def self.create(border)
300
+ mid = border[10] * 2
301
+ mid2 = mid * 2
302
+ right = "#{border[11]}#{border[12] * 2}"
303
+ new(
304
+ border[0] + mid2 + right,
305
+ border[0] + mid,
306
+ mid + right,
307
+ border[6] + mid2 + right,
308
+ border[9]
309
+ )
310
+ end
311
+
312
+ def parts(style)
313
+ if style
300
314
  style = Ansi[*style]
301
- [
302
- # TODO: use rather [/fg]
303
- Str.new("#{style}#{top}#{Ansi::RESET}", 6),
304
- Str.new("#{style}#{top_left}#{Ansi::RESET} ", 4),
305
- Str.new(" #{style}#{top_right}#{Ansi::RESET}", 6),
306
- Str.new("#{style}#{bottom}#{Ansi::RESET}", 6),
307
- Str.new("#{style}#{prefix}#{Ansi::RESET} ", 2)
308
- ]
315
+ reset = Ansi::RESET
309
316
  end
317
+ [
318
+ Str.new("#{style}#{top}#{reset}", 6),
319
+ Str.new("#{style}#{top_left}#{reset} ", 4),
320
+ Str.new(" #{style}#{top_right}#{reset}", 6),
321
+ Str.new("#{style}#{bottom}#{reset}", 6),
322
+ Str.new("#{style}#{prefix}#{reset} ", 2)
323
+ ]
310
324
  end
311
-
312
- private_constant :SectionBorder
325
+ end
326
+ private_constant :SectionBorder
327
+
328
+ @ll = {}
329
+
330
+ register(:mono, 'Monochrome – Non-ANSI fallback', &:itself)
331
+
332
+ register(:default, 'Default – uses default colors') do |theme|
333
+ theme.heading_sytle = :bright_blue
334
+ theme.task_style = %i[bright_green b]
335
+ # theme.choice_style =
336
+ theme.sh_out_style = :default
337
+ theme.sh_err_style = :bright_yellow
338
+ theme.choice_current_style = %i[bright_white on_blue b]
339
+ theme.define_marker(
340
+ bullet: '[bright_white]•[/fg]',
341
+ checkmark: '[bright_green]✓[/fg]',
342
+ quote: '[bright_blue]▍[/fg]',
343
+ information: '[bright_yellow]𝒊[/fg]',
344
+ warning: '[bright_yellow]![/fg]',
345
+ error: '[red]𝙓[/fg]',
346
+ failed: '[bright_red]𝑭[/fg]',
347
+ current: '[bright_green]➔[/fg]',
348
+ choice: '[bright_white]◦[/fg]',
349
+ current_choice: '[bright_green]➔[/fg]',
350
+ sh_out: '[bright_white]:[/fg]',
351
+ sh_err: '[red]𝙓[/fg]'
352
+ )
353
+ theme.define_section(
354
+ default: :bright_blue,
355
+ message: :bright_blue,
356
+ information: :bright_blue,
357
+ warning: :bright_yellow,
358
+ error: :red,
359
+ failed: :bright_red
360
+ )
313
361
  end
314
- # private_constant :Compiled
315
-
316
- private
317
362
 
318
- def initialize
319
- define_heading(%w[╸╸╺╸╺━━━ ╴╶╴╶─═══ ╴╶╴╶─── ════ ━━━━ ────])
320
- @mark = {
321
- default: '•',
322
- bullet: '•',
323
- checkmark: '✓',
324
- quote: '▍',
325
- information: '𝒊',
326
- warning: '!',
327
- error: '𝙓',
328
- failed: '𝑭',
329
- current: '',
330
- choice: '',
331
- current_choice: ''
332
- }
333
- @border = {
334
- ######### 0123456789012
335
- default: '┌┬┐├┼┤└┴┘│─╶╴',
336
- defaulth: '───────── ─╶╴',
337
- defaultv: '││││││││││ ',
338
- double: '╔╦╗╠╬╣╚╩╝║═',
339
- doubleh: '═════════ ═',
340
- doublev: '║║║║║║║║║║ ',
341
- heavy: '┏┳┓┣╋┫┗┻┛┃━╺╸',
342
- heavyh: '━━━━━━━━━ ━╺╸',
343
- heavyv: '┃┃┃┃┃┃┃┃┃┃ ',
344
- rounded: '╭┬╮├┼┤╰┴╯│─╶╴'
345
- }
346
- @section_border = :rounded
347
- @section_styles = {}
363
+ register(:emoji, 'Emoji – emoticons and default colors') do |theme|
364
+ theme.heading_sytle = :bright_blue
365
+ theme.task_style = %i[bright_green b]
366
+ # theme.choice_style =
367
+ theme.sh_out_style = :default
368
+ theme.sh_err_style = :bright_red
369
+ theme.choice_current_style = %i[bright_white on_blue b]
370
+ theme.define_marker(
371
+ bullet: '▫️',
372
+ checkmark: '',
373
+ quote: '[bright_blue]▍[/fg]',
374
+ information: '📌',
375
+ warning: '⚠️',
376
+ error: '❗️',
377
+ failed: '‼️',
378
+ current: '➡️',
379
+ choice: '[bright_white]•[/fg]',
380
+ current_choice: '[bright_green]●[/fg]',
381
+ sh_out: '[white b]:[/]',
382
+ sh_err: '❗️'
383
+ )
384
+ theme.define_section(
385
+ default: :bright_blue,
386
+ message: :bright_blue,
387
+ information: :bright_blue,
388
+ warning: :bright_yellow,
389
+ error: :red,
390
+ failed: :bright_red
391
+ )
348
392
  end
349
393
 
350
- self.current = Terminal.colors == 2 ? new : default
394
+ use(Terminal.colors == 2 ? :mono : :default)
351
395
  end
352
396
  end