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.
- checksums.yaml +7 -0
- data/LICENSE +28 -0
- data/README.md +122 -0
- data/examples/basic.rb +61 -0
- data/examples/colors.rb +5 -0
- data/examples/illustration.svg +1 -0
- data/examples/progress.rb +84 -0
- data/examples/query.rb +32 -0
- data/lib/natty-ui/ansi.rb +430 -0
- data/lib/natty-ui/ansi_wrapper.rb +207 -0
- data/lib/natty-ui/version.rb +6 -0
- data/lib/natty-ui/wrapper/ask.rb +76 -0
- data/lib/natty-ui/wrapper/element.rb +77 -0
- data/lib/natty-ui/wrapper/features.rb +24 -0
- data/lib/natty-ui/wrapper/framed.rb +54 -0
- data/lib/natty-ui/wrapper/heading.rb +87 -0
- data/lib/natty-ui/wrapper/message.rb +116 -0
- data/lib/natty-ui/wrapper/mixins.rb +67 -0
- data/lib/natty-ui/wrapper/progress.rb +60 -0
- data/lib/natty-ui/wrapper/query.rb +85 -0
- data/lib/natty-ui/wrapper/section.rb +102 -0
- data/lib/natty-ui/wrapper/task.rb +58 -0
- data/lib/natty-ui/wrapper.rb +168 -0
- data/lib/natty-ui.rb +159 -0
- metadata +91 -0
@@ -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,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
|