dev-ui 0.1.0 → 0.1.1
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 +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +10 -2
- data/README.md +132 -2
- data/dev.yml +0 -1
- data/lib/dev/ui.rb +82 -4
- data/lib/dev/ui/ansi.rb +67 -6
- data/lib/dev/ui/color.rb +21 -0
- data/lib/dev/ui/formatter.rb +23 -2
- data/lib/dev/ui/frame.rb +128 -10
- data/lib/dev/ui/glyph.rb +43 -18
- data/lib/dev/ui/interactive_prompt.rb +81 -38
- data/lib/dev/ui/progress.rb +42 -17
- data/lib/dev/ui/prompt.rb +54 -1
- data/lib/dev/ui/spinner.rb +28 -148
- data/lib/dev/ui/spinner/async.rb +40 -0
- data/lib/dev/ui/spinner/spin_group.rb +223 -0
- data/lib/dev/ui/stdout_router.rb +3 -2
- data/lib/dev/ui/terminal.rb +3 -0
- data/lib/dev/ui/version.rb +1 -1
- metadata +5 -3
data/lib/dev/ui/color.rb
CHANGED
@@ -4,6 +4,16 @@ module Dev
|
|
4
4
|
module UI
|
5
5
|
class Color
|
6
6
|
attr_reader :sgr, :name, :code
|
7
|
+
|
8
|
+
# Creates a new color mapping
|
9
|
+
# Signatures can be found here:
|
10
|
+
# https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
11
|
+
#
|
12
|
+
# ==== Attributes
|
13
|
+
#
|
14
|
+
# * +sgr+ - The color signature
|
15
|
+
# * +name+ - The name of the color
|
16
|
+
#
|
7
17
|
def initialize(sgr, name)
|
8
18
|
@sgr = sgr
|
9
19
|
@code = Dev::UI::ANSI.sgr(sgr)
|
@@ -44,12 +54,23 @@ module Dev
|
|
44
54
|
end
|
45
55
|
end
|
46
56
|
|
57
|
+
# Looks up a color code by name
|
58
|
+
#
|
59
|
+
# ==== Raises
|
60
|
+
# Raises a InvalidColorName if the color is not available
|
61
|
+
# You likely need to add it to the +MAP+ or you made a typo
|
62
|
+
#
|
63
|
+
# ==== Returns
|
64
|
+
# Returns a color code
|
65
|
+
#
|
47
66
|
def self.lookup(name)
|
48
67
|
MAP.fetch(name)
|
49
68
|
rescue KeyError
|
50
69
|
raise InvalidColorName, name
|
51
70
|
end
|
52
71
|
|
72
|
+
# All available colors by name
|
73
|
+
#
|
53
74
|
def self.available
|
54
75
|
MAP.keys
|
55
76
|
end
|
data/lib/dev/ui/formatter.rb
CHANGED
@@ -6,6 +6,11 @@ require 'strscan'
|
|
6
6
|
module Dev
|
7
7
|
module UI
|
8
8
|
class Formatter
|
9
|
+
# Available mappings of formattings
|
10
|
+
# To use any of them, you can use {{<key>:<string>}}
|
11
|
+
# There are presentational (colours and formatters)
|
12
|
+
# and semantic (error, info, command) formatters available
|
13
|
+
#
|
9
14
|
SGR_MAP = {
|
10
15
|
# presentational
|
11
16
|
'red' => '31',
|
@@ -32,14 +37,14 @@ module Dev
|
|
32
37
|
|
33
38
|
SCAN_FUNCNAME = /\w+:/
|
34
39
|
SCAN_GLYPH = /.}}/
|
35
|
-
SCAN_BODY =
|
40
|
+
SCAN_BODY = %r{
|
36
41
|
.*?
|
37
42
|
(
|
38
43
|
#{BEGIN_EXPR} |
|
39
44
|
#{END_EXPR} |
|
40
45
|
\z
|
41
46
|
)
|
42
|
-
|
47
|
+
}mx
|
43
48
|
|
44
49
|
DISCARD_BRACES = 0..-3
|
45
50
|
|
@@ -55,10 +60,26 @@ module Dev
|
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
63
|
+
# Initialize a formatter with text.
|
64
|
+
#
|
65
|
+
# ===== Attributes
|
66
|
+
#
|
67
|
+
# * +text+ - the text to format
|
68
|
+
#
|
58
69
|
def initialize(text)
|
59
70
|
@text = text
|
60
71
|
end
|
61
72
|
|
73
|
+
# Format the text using a map.
|
74
|
+
#
|
75
|
+
# ===== Attributes
|
76
|
+
#
|
77
|
+
# * +sgr_map+ - the mapping of the formattings. Defaults to +SGR_MAP+
|
78
|
+
#
|
79
|
+
# ===== Options
|
80
|
+
#
|
81
|
+
# * +:enable_color+ - enable color output? Default is true
|
82
|
+
#
|
62
83
|
def format(sgr_map = SGR_MAP, enable_color: true)
|
63
84
|
@nodes = []
|
64
85
|
stack = parse_body(StringScanner.new(@text))
|
data/lib/dev/ui/frame.rb
CHANGED
@@ -3,15 +3,50 @@ require 'dev/ui'
|
|
3
3
|
module Dev
|
4
4
|
module UI
|
5
5
|
module Frame
|
6
|
+
class UnnestedFrameException < StandardError; end
|
6
7
|
class << self
|
7
8
|
DEFAULT_FRAME_COLOR = Dev::UI.resolve_color(:cyan)
|
8
9
|
|
10
|
+
# Opens a new frame. Can be nested
|
9
11
|
# Can be invoked in two ways: block and blockless
|
10
|
-
# In block form, the frame is closed automatically when the block returns
|
11
|
-
# In blockless form, caller MUST call Frame.close when the frame is
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
12
|
+
# * In block form, the frame is closed automatically when the block returns
|
13
|
+
# * In blockless form, caller MUST call +Frame.close+ when the frame is logically done
|
14
|
+
# * Blockless form is strongly discouraged in cases where block form can be made to work
|
15
|
+
#
|
16
|
+
# https://user-images.githubusercontent.com/3074765/33799861-cb5dcb5c-dd01-11e7-977e-6fad38cee08c.png
|
17
|
+
#
|
18
|
+
# The return value of the block determines if the block is a "success" or a "failure"
|
19
|
+
#
|
20
|
+
# ==== Attributes
|
21
|
+
#
|
22
|
+
# * +text+ - (required) the text/title to output in the frame
|
23
|
+
#
|
24
|
+
# ==== Options
|
25
|
+
#
|
26
|
+
# * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
|
27
|
+
# * +:failure_text+ - If the block failed, what do we output? Defaults to nil
|
28
|
+
# * +:success_text+ - If the block succeeds, what do we output? Defaults to nil
|
29
|
+
# * +:timing+ - How long did the frame content take? Invalid for blockless. Defaults to true for the block form
|
30
|
+
#
|
31
|
+
# ==== Example
|
32
|
+
#
|
33
|
+
# ===== Block Form (Assumes +Dev::UI::StdoutRouter.enable+ has been called)
|
34
|
+
#
|
35
|
+
# Dev::UI::Frame.open('Open') { puts 'hi' }
|
36
|
+
#
|
37
|
+
# Output:
|
38
|
+
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
39
|
+
# ┃ hi
|
40
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (0.0s) ━━
|
41
|
+
#
|
42
|
+
# ===== Blockless Form
|
43
|
+
#
|
44
|
+
# Dev::UI::Frame.open('Open')
|
45
|
+
#
|
46
|
+
# Output:
|
47
|
+
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
48
|
+
#
|
49
|
+
#
|
15
50
|
def open(
|
16
51
|
text,
|
17
52
|
color: DEFAULT_FRAME_COLOR,
|
@@ -64,6 +99,26 @@ module Dev
|
|
64
99
|
end
|
65
100
|
end
|
66
101
|
|
102
|
+
# Closes a frame
|
103
|
+
# Automatically called for a block-form +open+
|
104
|
+
#
|
105
|
+
# ==== Attributes
|
106
|
+
#
|
107
|
+
# * +text+ - (required) the text/title to output in the frame
|
108
|
+
#
|
109
|
+
# ==== Options
|
110
|
+
#
|
111
|
+
# * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
|
112
|
+
# * +:elapsed+ - How long did the frame take? Defaults to nil
|
113
|
+
#
|
114
|
+
# ==== Example
|
115
|
+
#
|
116
|
+
# Dev::UI::Frame.close('Close')
|
117
|
+
#
|
118
|
+
# Output:
|
119
|
+
# ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
120
|
+
#
|
121
|
+
#
|
67
122
|
def close(text, color: DEFAULT_FRAME_COLOR, elapsed: nil)
|
68
123
|
color = Dev::UI.resolve_color(color)
|
69
124
|
|
@@ -77,9 +132,35 @@ module Dev
|
|
77
132
|
end
|
78
133
|
end
|
79
134
|
|
135
|
+
# Adds a divider in a frame
|
136
|
+
# Used to separate information within a single frame
|
137
|
+
#
|
138
|
+
# ==== Attributes
|
139
|
+
#
|
140
|
+
# * +text+ - (required) the text/title to output in the frame
|
141
|
+
#
|
142
|
+
# ==== Options
|
143
|
+
#
|
144
|
+
# * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
|
145
|
+
#
|
146
|
+
# ==== Example
|
147
|
+
#
|
148
|
+
# Dev::UI::Frame.open('Open') { Dev::UI::Frame.divider('Divider') }
|
149
|
+
#
|
150
|
+
# Output:
|
151
|
+
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
152
|
+
# ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
153
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
154
|
+
#
|
155
|
+
# ==== Raises
|
156
|
+
#
|
157
|
+
# MUST be inside an open frame or it raises a +UnnestedFrameException+
|
158
|
+
#
|
80
159
|
def divider(text, color: nil)
|
160
|
+
fs_item = FrameStack.pop
|
161
|
+
raise UnnestedFrameException, "no frame nesting to unnest" unless fs_item
|
81
162
|
color = Dev::UI.resolve_color(color)
|
82
|
-
item = Dev::UI.resolve_color(
|
163
|
+
item = Dev::UI.resolve_color(fs_item)
|
83
164
|
|
84
165
|
Dev::UI.raw do
|
85
166
|
puts edge(text, color: (color || item), first: Dev::UI::Box::Heavy::DIV)
|
@@ -87,6 +168,12 @@ module Dev
|
|
87
168
|
FrameStack.push(item)
|
88
169
|
end
|
89
170
|
|
171
|
+
# Determines the prefix of a frame entry taking multi-nested frames into account
|
172
|
+
#
|
173
|
+
# ==== Options
|
174
|
+
#
|
175
|
+
# * +:color+ - The color of the prefix. Defaults to +Thread.current[:devui_frame_color_override]+ or nil
|
176
|
+
#
|
90
177
|
def prefix(color: nil)
|
91
178
|
pfx = String.new
|
92
179
|
items = FrameStack.items
|
@@ -101,6 +188,12 @@ module Dev
|
|
101
188
|
pfx
|
102
189
|
end
|
103
190
|
|
191
|
+
# Override a color for a given thread.
|
192
|
+
#
|
193
|
+
# ==== Attributes
|
194
|
+
#
|
195
|
+
# * +color+ - The color to override to
|
196
|
+
#
|
104
197
|
def with_frame_color_override(color)
|
105
198
|
prev = Thread.current[:devui_frame_color_override]
|
106
199
|
Thread.current[:devui_frame_color_override] = color
|
@@ -109,6 +202,8 @@ module Dev
|
|
109
202
|
Thread.current[:devui_frame_color_override] = prev
|
110
203
|
end
|
111
204
|
|
205
|
+
# The width of a prefix given the number of Frames in the stack
|
206
|
+
#
|
112
207
|
def prefix_width
|
113
208
|
w = FrameStack.items.size
|
114
209
|
w.zero? ? 0 : w + 1
|
@@ -154,18 +249,41 @@ module Dev
|
|
154
249
|
|
155
250
|
o = String.new
|
156
251
|
|
252
|
+
is_ci = ![0, '', nil].include?(ENV['CI'])
|
253
|
+
|
254
|
+
# Jumping around the line can cause some unwanted flashes
|
255
|
+
o << Dev::UI::ANSI.hide_cursor
|
256
|
+
|
257
|
+
if is_ci
|
258
|
+
# In CI, we can't use absolute horizontal positions because of timestamps.
|
259
|
+
# So we move around the line by offset from this cursor position.
|
260
|
+
o << Dev::UI::ANSI.cursor_save
|
261
|
+
else
|
262
|
+
# Outside of CI, we reset to column 1 so that things like ^C don't
|
263
|
+
# cause output misformatting.
|
264
|
+
o << "\r"
|
265
|
+
end
|
266
|
+
|
157
267
|
o << color.code
|
158
268
|
o << Dev::UI::Box::Heavy::HORZ * termwidth # draw a full line
|
159
|
-
o <<
|
160
|
-
o <<
|
161
|
-
o <<
|
162
|
-
o << suffix
|
269
|
+
o << print_at_x(prefix_start, prefix, is_ci)
|
270
|
+
o << color.code
|
271
|
+
o << print_at_x(suffix_start, suffix, is_ci)
|
163
272
|
o << Dev::UI::Color::RESET.code
|
273
|
+
o << Dev::UI::ANSI.show_cursor
|
164
274
|
o << "\n"
|
165
275
|
|
166
276
|
o
|
167
277
|
end
|
168
278
|
|
279
|
+
def print_at_x(x, str, is_ci)
|
280
|
+
if is_ci
|
281
|
+
Dev::UI::ANSI.cursor_restore + Dev::UI::ANSI.cursor_forward(x) + str
|
282
|
+
else
|
283
|
+
Dev::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
169
287
|
module FrameStack
|
170
288
|
ENVVAR = 'DEV_FRAME_STACK'
|
171
289
|
|
data/lib/dev/ui/glyph.rb
CHANGED
@@ -3,9 +3,28 @@ require 'dev/ui'
|
|
3
3
|
module Dev
|
4
4
|
module UI
|
5
5
|
class Glyph
|
6
|
-
|
6
|
+
class InvalidGlyphHandle < ArgumentError
|
7
|
+
def initialize(handle)
|
8
|
+
@handle = handle
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
keys = Glyph.available.join(',')
|
13
|
+
"invalid glyph handle: #{@handle} " \
|
14
|
+
"-- must be one of Dev::UI::Glyph.available (#{keys})"
|
15
|
+
end
|
16
|
+
end
|
7
17
|
|
8
18
|
attr_reader :handle, :codepoint, :color, :char, :to_s, :fmt
|
19
|
+
|
20
|
+
# Creates a new glyph
|
21
|
+
#
|
22
|
+
# ==== Attributes
|
23
|
+
#
|
24
|
+
# * +handle+ - The handle in the +MAP+ constant
|
25
|
+
# * +codepoint+ - The codepoint used to create the glyph (e.g. +0x2717+ for a ballot X)
|
26
|
+
# * +color+ - What color to output the glyph. Check +Dev::UI::Color+ for options.
|
27
|
+
#
|
9
28
|
def initialize(handle, codepoint, color)
|
10
29
|
@handle = handle
|
11
30
|
@codepoint = codepoint
|
@@ -17,30 +36,36 @@ module Dev
|
|
17
36
|
MAP[handle] = self
|
18
37
|
end
|
19
38
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
keys = Glyph.available.join(',')
|
33
|
-
"invalid glyph handle: #{@handle} " \
|
34
|
-
"-- must be one of Dev::UI::Glyph.available (#{keys})"
|
35
|
-
end
|
36
|
-
end
|
39
|
+
# Mapping of glyphs to terminal output
|
40
|
+
MAP = {}
|
41
|
+
# YELLOw SMALL STAR (⭑)
|
42
|
+
STAR = new('*', 0x2b51, Color::YELLOW)
|
43
|
+
# BLUE MATHEMATICAL SCRIPT SMALL i (𝒾)
|
44
|
+
INFO = new('i', 0x1d4be, Color::BLUE)
|
45
|
+
# BLUE QUESTION MARK (?)
|
46
|
+
QUESTION = new('?', 0x003f, Color::BLUE)
|
47
|
+
# GREEN CHECK MARK (✓)
|
48
|
+
CHECK = new('v', 0x2713, Color::GREEN)
|
49
|
+
# RED BALLOT X (✗)
|
50
|
+
X = new('x', 0x2717, Color::RED)
|
37
51
|
|
52
|
+
# Looks up a glyph by name
|
53
|
+
#
|
54
|
+
# ==== Raises
|
55
|
+
# Raises a InvalidGlyphHandle if the glyph is not available
|
56
|
+
# You likely need to create it with +.new+ or you made a typo
|
57
|
+
#
|
58
|
+
# ==== Returns
|
59
|
+
# Returns a terminal output-capable string
|
60
|
+
#
|
38
61
|
def self.lookup(name)
|
39
62
|
MAP.fetch(name.to_s)
|
40
63
|
rescue KeyError
|
41
64
|
raise InvalidGlyphHandle, name
|
42
65
|
end
|
43
66
|
|
67
|
+
# All available glyphs by name
|
68
|
+
#
|
44
69
|
def self.available
|
45
70
|
MAP.keys
|
46
71
|
end
|
@@ -3,18 +3,40 @@ require 'io/console'
|
|
3
3
|
module Dev
|
4
4
|
module UI
|
5
5
|
class InteractivePrompt
|
6
|
+
# Prompts the user with options
|
7
|
+
# Uses an interactive session to allow the user to pick an answer
|
8
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
9
|
+
#
|
10
|
+
# https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif
|
11
|
+
#
|
12
|
+
# ==== Example Usage:
|
13
|
+
#
|
14
|
+
# Ask an interactive question
|
15
|
+
# Dev::UI::InteractivePrompt.call(%w(rails go python))
|
16
|
+
#
|
6
17
|
def self.call(options)
|
7
18
|
list = new(options)
|
8
19
|
options[list.call - 1]
|
9
20
|
end
|
10
21
|
|
22
|
+
# Initializes a new +InteractivePrompt+
|
23
|
+
# Usually called from +self.call+
|
24
|
+
#
|
25
|
+
# ==== Example Usage:
|
26
|
+
#
|
27
|
+
# Dev::UI::InteractivePrompt.new(%w(rails go python))
|
28
|
+
#
|
11
29
|
def initialize(options)
|
12
30
|
@options = options
|
13
31
|
@active = 1
|
14
32
|
@marker = '>'
|
15
33
|
@answer = nil
|
34
|
+
@state = :root
|
16
35
|
end
|
17
36
|
|
37
|
+
# Calls the +InteractivePrompt+ and asks the question
|
38
|
+
# Usually used from +self.call+
|
39
|
+
#
|
18
40
|
def call
|
19
41
|
Dev::UI.raw { print(ANSI.hide_cursor) }
|
20
42
|
while @answer.nil?
|
@@ -24,7 +46,8 @@ module Dev
|
|
24
46
|
# This will put us back at the beginning of the options
|
25
47
|
# When we redraw the options, they will be overwritten
|
26
48
|
Dev::UI.raw do
|
27
|
-
@options.
|
49
|
+
num_lines = @options.join("\n").split("\n").reject(&:empty?).size
|
50
|
+
num_lines.times { print(ANSI.previous_line) }
|
28
51
|
print(ANSI.previous_line + ANSI.end_of_line + "\n")
|
29
52
|
end
|
30
53
|
end
|
@@ -39,47 +62,60 @@ module Dev
|
|
39
62
|
|
40
63
|
private
|
41
64
|
|
65
|
+
ESC = "\e"
|
66
|
+
|
67
|
+
def up
|
68
|
+
@active = @active - 1 >= 1 ? @active - 1 : @options.length
|
69
|
+
end
|
70
|
+
|
71
|
+
def down
|
72
|
+
@active = @active + 1 <= @options.length ? @active + 1 : 1
|
73
|
+
end
|
74
|
+
|
75
|
+
def select_n(n)
|
76
|
+
@active = n
|
77
|
+
@answer = n
|
78
|
+
end
|
79
|
+
|
80
|
+
def select_bool(char)
|
81
|
+
return unless (@options - %w(yes no)).empty?
|
82
|
+
opt = @options.detect { |o| o.start_with?(char) }
|
83
|
+
@active = @options.index(opt) + 1
|
84
|
+
@answer = @options.index(opt) + 1
|
85
|
+
end
|
86
|
+
|
87
|
+
# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon
|
42
88
|
def wait_for_user_input
|
43
89
|
char = read_char
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
when
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
when
|
61
|
-
|
90
|
+
case @state
|
91
|
+
when :root
|
92
|
+
case char
|
93
|
+
when ESC ; @state = :esc
|
94
|
+
when 'k' ; up
|
95
|
+
when 'j' ; down
|
96
|
+
when ('1'..@options.size.to_s) ; select_n(char.to_i)
|
97
|
+
when 'y', 'n' ; select_bool(char)
|
98
|
+
when " ", "\r", "\n" ; @answer = @active # <enter>
|
99
|
+
when "\u0003" ; raise Interrupt # Ctrl-c
|
100
|
+
end
|
101
|
+
when :esc
|
102
|
+
case char
|
103
|
+
when '[' ; @state = :esc_bracket
|
104
|
+
else ; raise Interrupt # unhandled escape sequence.
|
105
|
+
end
|
106
|
+
when :esc_bracket
|
107
|
+
@state = :root
|
108
|
+
case char
|
109
|
+
when 'A' ; up
|
110
|
+
when 'B' ; down
|
111
|
+
else ; raise Interrupt # unhandled escape sequence.
|
112
|
+
end
|
62
113
|
end
|
63
114
|
end
|
115
|
+
# rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon
|
64
116
|
|
65
|
-
# Will handle 2-3 character sequences like arrow keys and control-c
|
66
117
|
def read_char
|
67
|
-
raw_tty!
|
68
|
-
input = $stdin.getc.chr
|
69
|
-
return input unless input == "\e"
|
70
|
-
|
71
|
-
input << begin
|
72
|
-
$stdin.read_nonblock(3)
|
73
|
-
rescue
|
74
|
-
''
|
75
|
-
end
|
76
|
-
input << begin
|
77
|
-
$stdin.read_nonblock(2)
|
78
|
-
rescue
|
79
|
-
''
|
80
|
-
end
|
81
|
-
input
|
82
|
-
end
|
118
|
+
raw_tty! { $stdin.getc.chr }
|
83
119
|
rescue IOError
|
84
120
|
"\e"
|
85
121
|
end
|
@@ -95,8 +131,15 @@ module Dev
|
|
95
131
|
def render_options
|
96
132
|
@options.each_with_index do |choice, index|
|
97
133
|
num = index + 1
|
98
|
-
message = " #{num}.
|
99
|
-
message
|
134
|
+
message = " #{num}."
|
135
|
+
message += choice.split("\n").map { |l| " {{bold:#{l}}}" }.join("\n")
|
136
|
+
|
137
|
+
if num == @active
|
138
|
+
message = message.split("\n").map.with_index do |l, idx|
|
139
|
+
idx == 0 ? "{{blue:> #{l.strip}}}" : "{{blue:>#{l.strip}}}"
|
140
|
+
end.join("\n")
|
141
|
+
end
|
142
|
+
|
100
143
|
Dev::UI.with_frame_color(:blue) do
|
101
144
|
puts Dev::UI.fmt(message)
|
102
145
|
end
|