natty-ui 0.5.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.
@@ -0,0 +1,430 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ #
5
+ # Helper module for ANSI escape codes.
6
+ #
7
+ module Ansi
8
+ class << self
9
+ # @return [String] ANSI code to reset all attributes
10
+ def reset = "\e[0m"
11
+
12
+ # @return [String] ANSI code to save current screen state
13
+ def screen_save = "\e[?47h"
14
+
15
+ # @return [String] ANSI code to restore screen state
16
+ def screen_restore = "\e[?47l"
17
+
18
+ # @return [String] ANSI code to alternate screen
19
+ def screen_alternative = "\e[?1049h"
20
+
21
+ # @return [String] ANSI code to set alternate screen off
22
+ def screen_alternative_off = "\e[?1049l"
23
+
24
+ # @return [String] ANSI code to erase screen
25
+ def screen_erase = "\e[2J"
26
+
27
+ # @return [String] ANSI code to erase screen below current cursor position
28
+ def screen_erase_below = "\e[0J"
29
+
30
+ # @return [String] ANSI code to erase screen above current cursor position
31
+ def screen_erase_above = "\e[1J"
32
+
33
+ # @return [String] ANSI code to erase current line
34
+ def line_erase = "\e[2K"
35
+
36
+ # @return [String] ANSI code to erase to end of current line
37
+ def line_erase_to_end = "\e[0K"
38
+
39
+ # @return [String] ANSI code to erase to begin of current line
40
+ def line_erase_to_begin = "\e[1K"
41
+
42
+ # @return [String] ANSI code to erase current line and position to first
43
+ # column
44
+ def line_clear = "\e[1K\e[0G"
45
+
46
+ # @param lines [Integer] number of lines
47
+ # @return [String] ANSI code to move the cursor up
48
+ def cursor_up(lines = nil) = "\e[#{lines}A"
49
+
50
+ # @param lines [Integer] number of lines
51
+ # @return [String] ANSI code to move the cursor down
52
+ def cursor_down(lines = nil) = "\e[#{lines}B"
53
+
54
+ # @param columns [Integer] number of columns
55
+ # @return [String] ANSI code to move the cursor right
56
+ def cursor_right(columns = nil) = "\e[#{columns}C"
57
+
58
+ # @param columns [Integer] number of columns
59
+ # @return [String] ANSI code to move the cursor left
60
+ def cursor_left(columns = nil) = "\e[#{columns}D"
61
+
62
+ # @param lines [Integer] number of lines
63
+ # @return [String] ANSI code to move the cursor to beginning of the line some lines down
64
+ def cursor_line_down(lines = nil) = "\e[#{lines}E"
65
+
66
+ # @param lines [Integer] number of lines
67
+ # @return [String] ANSI code to move the cursor to beginning of the line some lines up
68
+ def cursor_line_up(lines = nil) = "\e[#{lines}F"
69
+
70
+ # @param columns [Integer] number of columns
71
+ # @return [String] ANSI code to move the cursor to giben column
72
+ def cursor_column(columns = nil) = "\e[#{columns}G"
73
+
74
+ # @return [String] ANSI code to hide the cursor
75
+ def cursor_hide = "\e[?25l"
76
+
77
+ # @return [String] ANSI code to show the cursor (again)
78
+ def cursor_show = "\e[?25h"
79
+
80
+ # @return [String] ANSI code to save current cursor position
81
+ def cursor_save_pos = "\e[s"
82
+
83
+ # @return [String] ANSI code to restore saved cursor position
84
+ def cursor_restore_pos = "\e[u"
85
+
86
+ # @return [String] ANSI code to set cursor position on upper left corner
87
+ def cursor_home = "\e[H"
88
+
89
+ # @param row [Integer] row to set cursor
90
+ # @param column [Integer] column to set cursor
91
+ # @return [String] ANSI code to set cursor position
92
+ def cursor_pos(row, column = nil)
93
+ return column ? "\e[#{row};#{column}H" : "\e[#{row}H" if row
94
+ column ? "\e[;#{column}H" : "\e[H"
95
+ end
96
+
97
+ # Decorate given `obj` with ANSI `attributes`.
98
+ #
99
+ # @see []
100
+ #
101
+ # @param obj [#to_s] object to be decorated
102
+ # @param attributes [Symbol, String] attribute names to be used
103
+ # @param reset [Boolean] whether to include reset code for ANSI attributes
104
+ # @return [String] `obj` converted and decorated with the ANSI `attributes`
105
+ def embellish(obj, *attributes, reset: true)
106
+ attributes = self[*attributes]
107
+ attributes.empty? ? "#{obj}" : "#{attributes}#{obj}#{"\e[0m" if reset}"
108
+ end
109
+
110
+ # Combine given ANSI `attributes`.
111
+ #
112
+ # ANSI attribute names are:
113
+ #
114
+ # `reset`, `bold`, `faint`, `italic`, `underline`, `slow_blink`, `blink`,
115
+ # `rapid_blink`, `invert`, `reverse`, `conceal`, `hide`, `strike`,
116
+ # `primary_font`, `default_font`, `font1`, `font2`, `font3`, `font4`,
117
+ # `font5`, `font6`, `font7`, `font8`, `font9`, `fraktur`,
118
+ # `double_underline`, `doubly`, `bold_off`, `normal`, `italic_off`,
119
+ # `fraktur_off`, `underline_off`, `blink_off`, `proportional`, `spacing`,
120
+ # `invert_off`, `reverse_off`, `reveal`, `strike_off`, `proportional_off`,
121
+ # `spacing_off`, `framed`, `encircled`, `overlined`, `framed_off`,
122
+ # `encircled_off`, `overlined_off`
123
+ #
124
+ # Colors can specified by their name for ANSI 4-bit colors:
125
+ # `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`
128
+ #
129
+ # For 8-bit ANSI colors you can use a prefixed integer number:
130
+ # `i0`...`i255`.
131
+ #
132
+ # To use RGB ANSI colors just specify the hexadecimal code like `#XXXXXX`
133
+ # or the short form `#XXX`.
134
+ #
135
+ # To use a color as background color prefix the color attribute with `bg_`
136
+ # or `on_`.
137
+ #
138
+ # To use a color as underline color prefix the color attribute with `ul_`.
139
+ #
140
+ # To make it more clear a color attribute should be used as fereground
141
+ # color the code can be prefixed with `fg_`.
142
+ #
143
+ # @example Valid Foreground Color Attributes
144
+ # Ansi[:yellow]
145
+ # Ansi["#fab"]
146
+ # Ansi["#00aa00"]
147
+ # Ansi[:fg_fab]
148
+ # Ansi[:fg_00aa00]
149
+ # Ansi[:i196]
150
+ # Ansi[:fg_i196]
151
+ #
152
+ # @example Valid Background Color Attributes
153
+ # Ansi[:bg_yellow]
154
+ # Ansi[:bg_fab]
155
+ # Ansi[:bg_00aa00]
156
+ # Ansi['bg#00aa00']
157
+ # Ansi[:bg_i196]
158
+ #
159
+ # Ansi[:on_yellow]
160
+ # Ansi[:on_fab]
161
+ # Ansi[:on_00aa00]
162
+ # Ansi['on#00aa00']
163
+ # Ansi[:on_i196]
164
+ #
165
+ # @example Valid Underline Color Attributes
166
+ # Ansi[:underline, :yellow]
167
+ # Ansi[:underline, :ul_fab]
168
+ # Ansi[:underline, :ul_00aa00]
169
+ # Ansi[:underline, 'ul#00aa00']
170
+ # Ansi[:underline, :ul_i196]
171
+ # Ansi[:underline, :ul_bright_yellow]
172
+ #
173
+ # @example Combined attributes:
174
+ # Ansi[:bold, :italic, :bright_white, :on_0000cc]
175
+ #
176
+ # @param attributes [Array<Symbol, String>] attribute names to be used
177
+ # @return [String] combined ANSI attributes
178
+ def [](*attributes)
179
+ return '' if attributes.empty?
180
+ "\e[#{
181
+ attributes
182
+ .map do |arg|
183
+ case arg
184
+ when Symbol, String
185
+ ATTRIBUTES[arg] || named_color(arg) || invalid_argument(arg)
186
+ when (0..255)
187
+ "38;5;#{arg}"
188
+ when (256..512)
189
+ "48;5;#{arg}"
190
+ else
191
+ invalid_argument(arg)
192
+ end
193
+ end
194
+ .join(';')
195
+ }m"
196
+ end
197
+
198
+ # Try to combine given ANSI `attributes`. The `attributes` have to be a
199
+ # string containing attributes separated by space char (" ").
200
+ #
201
+ # @example Valid Attribute String
202
+ # Ansi.try_convert('bold italic blink red on#00ff00')
203
+ # # => ANSI attribute string for bold, italic text which blinks red on
204
+ # # green background
205
+ #
206
+ # @example Invalid Attribute String
207
+ # Ansi.try_convert('cool bold on green')
208
+ # # => nil
209
+ #
210
+ # @param attributes [#to_s] attributes separated by space char (" ")
211
+ # @return [String] combined ANSI attributes
212
+ # @return [nil] when string does not contain valid attributes
213
+ def try_convert(attributes)
214
+ attributes = attributes.to_s.split
215
+ return if attributes.empty?
216
+ "\e[#{
217
+ attributes
218
+ .map { |arg| ATTRIBUTES[arg] || named_color(arg) || return }
219
+ .join(';')
220
+ }m"
221
+ end
222
+
223
+ private
224
+
225
+ def invalid_argument(name)
226
+ raise(
227
+ ArgumentError,
228
+ "unknown Ansi attribute name - '#{name}'",
229
+ caller(1)
230
+ )
231
+ end
232
+
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))
253
+ end
254
+ 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
+ end
269
+
270
+ ATTRIBUTES =
271
+ {
272
+ reset: 0,
273
+ # ---
274
+ bold: 1,
275
+ faint: 2,
276
+ italic: 3,
277
+ underline: 4,
278
+ # ---
279
+ slow_blink: 5,
280
+ blink: 5,
281
+ # ---
282
+ rapid_blink: 6,
283
+ # ---
284
+ invert: 7,
285
+ reverse: 7,
286
+ # ---
287
+ conceal: 8,
288
+ hide: 8,
289
+ # ---
290
+ strike: 9,
291
+ # ---
292
+ primary_font: 10,
293
+ default_font: 10,
294
+ # ---
295
+ font1: 11,
296
+ font2: 12,
297
+ font3: 13,
298
+ font4: 14,
299
+ font5: 15,
300
+ font6: 16,
301
+ font7: 17,
302
+ font8: 18,
303
+ font9: 19,
304
+ fraktur: 20,
305
+ # ---
306
+ double_underline: 21,
307
+ doubly: 21,
308
+ bold_off: 21,
309
+ # ---
310
+ normal: 22,
311
+ # ---
312
+ italic_off: 23,
313
+ fraktur_off: 23,
314
+ # ---
315
+ underline_off: 24,
316
+ blink_off: 25,
317
+ # ---
318
+ proportional: 26,
319
+ spacing: 26,
320
+ # ---
321
+ invert_off: 27,
322
+ reverse_off: 27,
323
+ # ---
324
+ reveal: 28,
325
+ # ---
326
+ strike_off: 29,
327
+ # ---
328
+ proportional_off: 50,
329
+ spacing_off: 50,
330
+ # ---
331
+ framed: 51,
332
+ encircled: 52,
333
+ overlined: 53,
334
+ framed_off: 54,
335
+ encircled_off: 54,
336
+ overlined_off: 55,
337
+ # foreground colors
338
+ black: 30,
339
+ red: 31,
340
+ green: 32,
341
+ yellow: 33,
342
+ blue: 34,
343
+ magenta: 35,
344
+ cyan: 36,
345
+ white: 37,
346
+ default: 39,
347
+ bright_black: 90,
348
+ bright_red: 91,
349
+ bright_green: 92,
350
+ bright_yellow: 93,
351
+ bright_blue: 94,
352
+ bright_magenta: 95,
353
+ bright_cyan: 96,
354
+ bright_white: 97,
355
+ # background colors
356
+ on_black: 40,
357
+ on_red: 41,
358
+ on_green: 42,
359
+ on_yellow: 43,
360
+ on_blue: 44,
361
+ on_magenta: 45,
362
+ on_cyan: 46,
363
+ on_white: 47,
364
+ on_default: 49,
365
+ on_bright_black: 100,
366
+ on_bright_red: 101,
367
+ on_bright_green: 102,
368
+ on_bright_yellow: 103,
369
+ on_bright_blue: 104,
370
+ on_bright_magenta: 105,
371
+ on_bright_cyan: 106,
372
+ on_bright_white: 107,
373
+ # foreground colors
374
+ fg_black: 30,
375
+ fg_red: 31,
376
+ fg_green: 32,
377
+ fg_yellow: 33,
378
+ fg_blue: 34,
379
+ fg_magenta: 35,
380
+ fg_cyan: 36,
381
+ fg_white: 37,
382
+ fg_default: 39,
383
+ fg_bright_black: 90,
384
+ fg_bright_red: 91,
385
+ fg_bright_green: 92,
386
+ fg_bright_yellow: 93,
387
+ fg_bright_blue: 94,
388
+ fg_bright_magenta: 95,
389
+ fg_bright_cyan: 96,
390
+ fg_bright_white: 97,
391
+ # background colors
392
+ bg_black: 40,
393
+ bg_red: 41,
394
+ bg_green: 42,
395
+ bg_yellow: 43,
396
+ bg_blue: 44,
397
+ bg_magenta: 45,
398
+ bg_cyan: 46,
399
+ bg_white: 47,
400
+ bg_default: 49,
401
+ bg_bright_black: 100,
402
+ bg_bright_red: 101,
403
+ bg_bright_green: 102,
404
+ bg_bright_yellow: 103,
405
+ bg_bright_blue: 104,
406
+ bg_bright_magenta: 105,
407
+ bg_bright_cyan: 106,
408
+ bg_bright_white: 107,
409
+ # underline colors
410
+ ul_black: '58;2;0;0;0',
411
+ ul_red: '58;2;128;0;0',
412
+ ul_green: '58;2;0;128;0',
413
+ ul_yellow: '58;2;128;128;0',
414
+ ul_blue: '58;2;0;0;128',
415
+ ul_magenta: '58;2;128;0;128',
416
+ ul_cyan: '58;2;0;128;128',
417
+ ul_white: '58;2;128;128;128',
418
+ ul_default: '59',
419
+ ul_bright_black: '58;2;64;64;64',
420
+ ul_bright_red: '58;2;255;0;0',
421
+ ul_bright_green: '58;2;0;255;0',
422
+ ul_bright_yellow: '58;2;255;255;0',
423
+ ul_bright_blue: '58;2;0;0;255',
424
+ ul_bright_magenta: '58;2;255;0;255',
425
+ ul_bright_cyan: '58;2;0;255;255',
426
+ ul_bright_white: '58;2;255;255;255'
427
+ }.tap { |ret| ret.merge!(ret.transform_keys(&:to_s)).freeze }
428
+ private_constant :ATTRIBUTES
429
+ end
430
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require_relative 'wrapper'
5
+ require_relative 'ansi'
6
+
7
+ module NattyUI
8
+ class AnsiWrapper < Wrapper
9
+ def ansi? = true
10
+
11
+ def page
12
+ unless block_given?
13
+ @stream.flush
14
+ return self
15
+ end
16
+ (@stream << PAGE_BEGIN).flush
17
+ begin
18
+ yield(self)
19
+ ensure
20
+ (@stream << PAGE_END).flush
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ def embellish(obj)
27
+ obj = NattyUI.embellish(obj)
28
+ obj.empty? ? nil : obj
29
+ end
30
+
31
+ def temp_func
32
+ count = @lines_written
33
+ lambda do
34
+ count = @lines_written - count
35
+ if count.nonzero?
36
+ @stream << Ansi.cursor_line_up(count) << Ansi.screen_erase_below
37
+ @lines_written -= count
38
+ end
39
+ @stream.flush
40
+ self
41
+ end
42
+ end
43
+
44
+ class Message < Message
45
+ protected
46
+
47
+ def title_attr(str, symbol)
48
+ color = COLORS[symbol]
49
+ if color
50
+ {
51
+ prefix:
52
+ "#{Ansi[:bold, :italic, color]}#{str}" \
53
+ "#{Ansi[:reset, :bold, color]} ",
54
+ suffix: Ansi.reset
55
+ }
56
+ else
57
+ { prefix: "#{Ansi[:bold, 231]}#{str} ", suffix: Ansi.reset }
58
+ end
59
+ end
60
+
61
+ COLORS = {
62
+ default: 231,
63
+ information: 117,
64
+ warning: 220,
65
+ error: 196,
66
+ completed: 46,
67
+ failed: 198,
68
+ query: 220,
69
+ task: 117
70
+ }.compare_by_identity.freeze
71
+ end
72
+
73
+ class Section < Section
74
+ def temporary
75
+ stream = wrapper.stream
76
+ unless block_given?
77
+ stream.flush
78
+ return self
79
+ end
80
+ count = wrapper.lines_written
81
+ begin
82
+ yield(self)
83
+ ensure
84
+ count = wrapper.lines_written - count
85
+ if count.nonzero?
86
+ stream << Ansi.cursor_line_up(count) << Ansi.screen_erase_below
87
+ end
88
+ stream.flush
89
+ end
90
+ end
91
+
92
+ protected
93
+
94
+ def initialize(parent, prefix_attr: nil, **opts)
95
+ super
96
+ return unless @prefix && prefix_attr
97
+ @prefix = Ansi.embellish(@prefix, *prefix_attr)
98
+ end
99
+ end
100
+
101
+ class Heading < Heading
102
+ protected
103
+
104
+ def enclose(weight)
105
+ prefix, suffix = super
106
+ ["#{PREFIX}#{prefix}#{MSG}", "#{PREFIX}#{suffix}#{Ansi.reset}"]
107
+ end
108
+
109
+ PREFIX = Ansi[39].freeze
110
+ MSG = Ansi[:bold, 231].freeze
111
+ end
112
+
113
+ class Framed < Framed
114
+ protected
115
+
116
+ 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
+ ]
124
+ end
125
+ end
126
+
127
+ class Ask < Ask
128
+ protected
129
+
130
+ def query(question)
131
+ (wrapper.stream << "#{prefix}#{PREFIX} #{question}#{Ansi.reset} ").flush
132
+ end
133
+
134
+ def finish = (wrapper.stream << Ansi.line_clear).flush
135
+
136
+ PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
137
+ end
138
+
139
+ class Query < Query
140
+ protected
141
+
142
+ def read(choices, result_typye)
143
+ wrapper.stream << "#{prefix}#{PROMPT} "
144
+ super
145
+ end
146
+
147
+ PROMPT = Ansi.embellish(':', :bold, 220).freeze
148
+ end
149
+
150
+ class Task < Message
151
+ include ProgressAttributes
152
+ include TaskMethods
153
+ end
154
+
155
+ class Progress < Progress
156
+ protected
157
+
158
+ def draw_title(title)
159
+ @prefix = "#{prefix}#{TITLE_PREFIX}#{title}#{Ansi.reset} "
160
+ (wrapper.stream << @prefix).flush
161
+ @prefix = "#{Ansi.line_clear}#{@prefix}"
162
+ if @max_value
163
+ @prefix << BAR_COLOR
164
+ else
165
+ @prefix << INDICATOR_ATTRIBUTE
166
+ @indicator = 0
167
+ end
168
+ end
169
+
170
+ TITLE_PREFIX = "#{Ansi[:bold, :italic, 117]}➔#{Ansi[:reset, 117]} ".freeze
171
+ INDICATOR_ATTRIBUTE = Ansi[:bold, 220].freeze
172
+ BAR_COLOR = Ansi[39, 295].freeze
173
+ BAR_BACK = Ansi[236, 492].freeze
174
+ BAR_INK = Ansi[:bold, 255, :on_default].freeze
175
+
176
+ def draw_final = (wrapper.stream << Ansi.line_clear).flush
177
+
178
+ def redraw
179
+ (wrapper.stream << @prefix << (@max_value ? fullbar : indicator)).flush
180
+ end
181
+
182
+ def indicator = '─╲│╱'[(@indicator += 1) % 4]
183
+
184
+ def fullbar
185
+ percent = @value / @max_value
186
+ count = (30 * percent).to_i
187
+ "#{'█' * count}#{BAR_BACK}#{'▁' * (30 - count)}" \
188
+ "#{BAR_INK} #{
189
+ format(
190
+ '%<value>.0f/%<max_value>.0f (%<percent>.2f%%)',
191
+ value: @value,
192
+ max_value: @max_value,
193
+ percent: percent * 100
194
+ )
195
+ }"
196
+ end
197
+ end
198
+
199
+ PAGE_BEGIN =
200
+ "#{Ansi.reset}#{Ansi.cursor_save_pos}#{Ansi.screen_save}" \
201
+ "#{Ansi.cursor_home}#{Ansi.screen_erase}".freeze
202
+ PAGE_END =
203
+ "#{Ansi.screen_restore}#{Ansi.cursor_restore_pos}#{Ansi.reset}".freeze
204
+ end
205
+
206
+ private_constant :AnsiWrapper
207
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ # @return [String] the version number of the gem
5
+ VERSION = '0.5.0'
6
+ end
@@ -0,0 +1,76 @@
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 sec.ask('Do you like the NattyUI gem?')
17
+ # when true
18
+ # sec.info('Yeah!!')
19
+ # when false
20
+ # sec.write("That's pitty!")
21
+ # else
22
+ # sec.failed('You should have an opinion!')
23
+ # end
24
+ #
25
+ # @param question [#to_s] Question to display
26
+ # @param yes [#to_s] chars which will be used to answer 'Yes'
27
+ # @param no [#to_s] chars which will be used to answer 'No'
28
+ # @return [Boolean] whether the answer is yes or no
29
+ # @return [nil] when input was aborted with `ESC`, `^C` or `^D`
30
+ def ask(question, yes: "jotsyd\r\n", no: 'n')
31
+ _element(:Ask, question, yes, no)
32
+ end
33
+ end
34
+
35
+ class Wrapper
36
+ #
37
+ # An {Element} to ask user input for yes/no queries.
38
+ #
39
+ # @see Features#ask
40
+ class Ask < Element
41
+ protected
42
+
43
+ def _call(question, yes, no)
44
+ yes, no = grab(yes, no)
45
+ query(question)
46
+ read(yes, no)
47
+ ensure
48
+ finish
49
+ end
50
+
51
+ def query(question)
52
+ (wrapper.stream << prefix << "▶︎ #{question} ").flush
53
+ end
54
+
55
+ def finish = (wrapper.stream << "\n").flush
56
+
57
+ def read(yes, no)
58
+ while true
59
+ char = NattyUI.in_stream.getch
60
+ return if "\u0003\u0004\e".include?(char)
61
+ return true if yes.include?(char)
62
+ return false if no.include?(char)
63
+ end
64
+ end
65
+
66
+ def grab(yes, no)
67
+ yes = yes.to_s.chars.uniq
68
+ no = no.to_s.chars.uniq
69
+ raise(ArgumentError, ':yes can not be emoty') if yes.empty?
70
+ raise(ArgumentError, ':no can not be emoty') if no.empty?
71
+ return yes, no if (yes & no).empty?
72
+ raise(ArgumentError, 'chars in :yes and :no can not be intersect')
73
+ end
74
+ end
75
+ end
76
+ end