cli-ui 1.2.2 → 1.5.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/README.md +45 -1
- data/lib/cli/ui.rb +75 -29
- data/lib/cli/ui/ansi.rb +10 -6
- data/lib/cli/ui/color.rb +12 -7
- data/lib/cli/ui/formatter.rb +34 -21
- data/lib/cli/ui/frame.rb +111 -152
- data/lib/cli/ui/frame/frame_stack.rb +98 -0
- data/lib/cli/ui/frame/frame_style.rb +120 -0
- data/lib/cli/ui/frame/frame_style/box.rb +166 -0
- data/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
- data/lib/cli/ui/glyph.rb +23 -17
- data/lib/cli/ui/os.rb +67 -0
- data/lib/cli/ui/printer.rb +59 -0
- data/lib/cli/ui/progress.rb +9 -7
- data/lib/cli/ui/prompt.rb +97 -21
- data/lib/cli/ui/prompt/interactive_options.rb +75 -61
- data/lib/cli/ui/prompt/options_handler.rb +7 -2
- data/lib/cli/ui/spinner.rb +23 -5
- data/lib/cli/ui/spinner/spin_group.rb +34 -12
- data/lib/cli/ui/stdout_router.rb +13 -8
- data/lib/cli/ui/terminal.rb +26 -16
- data/lib/cli/ui/truncater.rb +4 -4
- data/lib/cli/ui/version.rb +1 -1
- data/lib/cli/ui/widgets.rb +77 -0
- data/lib/cli/ui/widgets/base.rb +27 -0
- data/lib/cli/ui/widgets/status.rb +61 -0
- data/lib/cli/ui/wrap.rb +56 -0
- metadata +17 -16
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -17
- data/.travis.yml +0 -5
- data/Gemfile +0 -16
- data/Rakefile +0 -20
- data/bin/console +0 -14
- data/cli-ui.gemspec +0 -27
- data/dev.yml +0 -14
- data/lib/cli/ui/box.rb +0 -15
data/lib/cli/ui/frame.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
require 'cli/ui'
|
3
|
+
require 'cli/ui/frame/frame_stack'
|
4
|
+
require 'cli/ui/frame/frame_style'
|
2
5
|
|
3
6
|
module CLI
|
4
7
|
module UI
|
@@ -7,6 +10,22 @@ module CLI
|
|
7
10
|
class << self
|
8
11
|
DEFAULT_FRAME_COLOR = CLI::UI.resolve_color(:cyan)
|
9
12
|
|
13
|
+
def frame_style
|
14
|
+
@frame_style ||= FrameStyle::Box
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set the default frame style.
|
18
|
+
#
|
19
|
+
# Raises ArgumentError if +frame_style+ is not valid
|
20
|
+
#
|
21
|
+
# ==== Attributes
|
22
|
+
#
|
23
|
+
# * +symbol+ or +FrameStyle+ - the default frame style to use for frames
|
24
|
+
#
|
25
|
+
def frame_style=(frame_style)
|
26
|
+
@frame_style = CLI::UI.resolve_style(frame_style)
|
27
|
+
end
|
28
|
+
|
10
29
|
# Opens a new frame. Can be nested
|
11
30
|
# Can be invoked in two ways: block and blockless
|
12
31
|
# * In block form, the frame is closed automatically when the block returns
|
@@ -27,6 +46,7 @@ module CLI
|
|
27
46
|
# * +:failure_text+ - If the block failed, what do we output? Defaults to nil
|
28
47
|
# * +:success_text+ - If the block succeeds, what do we output? Defaults to nil
|
29
48
|
# * +:timing+ - How long did the frame content take? Invalid for blockless. Defaults to true for the block form
|
49
|
+
# * +frame_style+ - The frame style to use for this frame
|
30
50
|
#
|
31
51
|
# ==== Example
|
32
52
|
#
|
@@ -34,7 +54,7 @@ module CLI
|
|
34
54
|
#
|
35
55
|
# CLI::UI::Frame.open('Open') { puts 'hi' }
|
36
56
|
#
|
37
|
-
# Output:
|
57
|
+
# Default Output:
|
38
58
|
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
39
59
|
# ┃ hi
|
40
60
|
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (0.0s) ━━
|
@@ -43,7 +63,7 @@ module CLI
|
|
43
63
|
#
|
44
64
|
# CLI::UI::Frame.open('Open')
|
45
65
|
#
|
46
|
-
# Output:
|
66
|
+
# Default Output:
|
47
67
|
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
48
68
|
#
|
49
69
|
#
|
@@ -52,27 +72,28 @@ module CLI
|
|
52
72
|
color: DEFAULT_FRAME_COLOR,
|
53
73
|
failure_text: nil,
|
54
74
|
success_text: nil,
|
55
|
-
timing: nil
|
75
|
+
timing: nil,
|
76
|
+
frame_style: self.frame_style
|
56
77
|
)
|
78
|
+
frame_style = CLI::UI.resolve_style(frame_style)
|
57
79
|
color = CLI::UI.resolve_color(color)
|
58
80
|
|
59
81
|
unless block_given?
|
60
82
|
if failure_text
|
61
|
-
raise ArgumentError,
|
83
|
+
raise ArgumentError, 'failure_text is not compatible with blockless invocation'
|
62
84
|
elsif success_text
|
63
|
-
raise ArgumentError,
|
64
|
-
elsif
|
65
|
-
raise ArgumentError,
|
85
|
+
raise ArgumentError, 'success_text is not compatible with blockless invocation'
|
86
|
+
elsif timing
|
87
|
+
raise ArgumentError, 'timing is not compatible with blockless invocation'
|
66
88
|
end
|
67
89
|
end
|
68
90
|
|
69
|
-
|
70
|
-
|
71
|
-
t_start = Time.now.to_f
|
91
|
+
t_start = Time.now
|
72
92
|
CLI::UI.raw do
|
73
|
-
|
93
|
+
print(prefix.chop)
|
94
|
+
puts frame_style.open(text, color: color)
|
74
95
|
end
|
75
|
-
FrameStack.push(color)
|
96
|
+
FrameStack.push(color: color, style: frame_style)
|
76
97
|
|
77
98
|
return unless block_given?
|
78
99
|
|
@@ -82,14 +103,14 @@ module CLI
|
|
82
103
|
success = yield
|
83
104
|
rescue
|
84
105
|
closed = true
|
85
|
-
t_diff =
|
106
|
+
t_diff = elasped(t_start, timing)
|
86
107
|
close(failure_text, color: :red, elapsed: t_diff)
|
87
108
|
raise
|
88
109
|
else
|
89
110
|
success
|
90
111
|
ensure
|
91
112
|
unless closed
|
92
|
-
t_diff =
|
113
|
+
t_diff = elasped(t_start, timing)
|
93
114
|
if success != false
|
94
115
|
close(success_text, color: color, elapsed: t_diff)
|
95
116
|
else
|
@@ -99,8 +120,8 @@ module CLI
|
|
99
120
|
end
|
100
121
|
end
|
101
122
|
|
102
|
-
#
|
103
|
-
#
|
123
|
+
# Adds a divider in a frame
|
124
|
+
# Used to separate information within a single frame
|
104
125
|
#
|
105
126
|
# ==== Attributes
|
106
127
|
#
|
@@ -109,31 +130,38 @@ module CLI
|
|
109
130
|
# ==== Options
|
110
131
|
#
|
111
132
|
# * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
|
112
|
-
# *
|
133
|
+
# * +frame_style+ - The frame style to use for this frame
|
113
134
|
#
|
114
135
|
# ==== Example
|
115
136
|
#
|
116
|
-
# CLI::UI::Frame.
|
137
|
+
# CLI::UI::Frame.open('Open') { CLI::UI::Frame.divider('Divider') }
|
117
138
|
#
|
118
|
-
# Output:
|
119
|
-
#
|
139
|
+
# Default Output:
|
140
|
+
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
141
|
+
# ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
142
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
120
143
|
#
|
144
|
+
# ==== Raises
|
121
145
|
#
|
122
|
-
|
123
|
-
|
146
|
+
# MUST be inside an open frame or it raises a +UnnestedFrameException+
|
147
|
+
#
|
148
|
+
def divider(text, color: nil, frame_style: nil)
|
149
|
+
fs_item = FrameStack.pop
|
150
|
+
raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
|
151
|
+
|
152
|
+
color = CLI::UI.resolve_color(color) || fs_item.color
|
153
|
+
frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style
|
124
154
|
|
125
|
-
FrameStack.pop
|
126
|
-
kwargs = {}
|
127
|
-
if elapsed
|
128
|
-
kwargs[:right_text] = "(#{elapsed.round(2)}s)"
|
129
|
-
end
|
130
155
|
CLI::UI.raw do
|
131
|
-
|
156
|
+
print(prefix.chop)
|
157
|
+
puts frame_style.divider(text, color: color)
|
132
158
|
end
|
159
|
+
|
160
|
+
FrameStack.push(fs_item)
|
133
161
|
end
|
134
162
|
|
135
|
-
#
|
136
|
-
#
|
163
|
+
# Closes a frame
|
164
|
+
# Automatically called for a block-form +open+
|
137
165
|
#
|
138
166
|
# ==== Attributes
|
139
167
|
#
|
@@ -141,51 +169,70 @@ module CLI
|
|
141
169
|
#
|
142
170
|
# ==== Options
|
143
171
|
#
|
144
|
-
# * +:color+ - The color of the frame. Defaults to
|
172
|
+
# * +:color+ - The color of the frame. Defaults to nil
|
173
|
+
# * +:elapsed+ - How long did the frame take? Defaults to nil
|
174
|
+
# * +frame_style+ - The frame style to use for this frame. Defaults to nil
|
145
175
|
#
|
146
176
|
# ==== Example
|
147
177
|
#
|
148
|
-
# CLI::UI::Frame.
|
178
|
+
# CLI::UI::Frame.close('Close')
|
149
179
|
#
|
150
|
-
# Output:
|
151
|
-
#
|
152
|
-
# ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
153
|
-
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
180
|
+
# Default Output:
|
181
|
+
# ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
154
182
|
#
|
155
183
|
# ==== Raises
|
156
184
|
#
|
157
185
|
# MUST be inside an open frame or it raises a +UnnestedFrameException+
|
158
186
|
#
|
159
|
-
def
|
187
|
+
def close(text, color: nil, elapsed: nil, frame_style: nil)
|
160
188
|
fs_item = FrameStack.pop
|
161
|
-
raise UnnestedFrameException,
|
162
|
-
|
163
|
-
|
189
|
+
raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
|
190
|
+
|
191
|
+
color = CLI::UI.resolve_color(color) || fs_item.color
|
192
|
+
frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style
|
193
|
+
|
194
|
+
kwargs = {}
|
195
|
+
if elapsed
|
196
|
+
kwargs[:right_text] = "(#{elapsed.round(2)}s)"
|
197
|
+
end
|
164
198
|
|
165
199
|
CLI::UI.raw do
|
166
|
-
|
200
|
+
print(prefix.chop)
|
201
|
+
puts frame_style.close(text, color: color, **kwargs)
|
167
202
|
end
|
168
|
-
FrameStack.push(item)
|
169
203
|
end
|
170
204
|
|
171
205
|
# Determines the prefix of a frame entry taking multi-nested frames into account
|
172
206
|
#
|
173
207
|
# ==== Options
|
174
208
|
#
|
175
|
-
# * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+
|
209
|
+
# * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+
|
176
210
|
#
|
177
|
-
def prefix(color:
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
211
|
+
def prefix(color: Thread.current[:cliui_frame_color_override])
|
212
|
+
+''.tap do |output|
|
213
|
+
items = FrameStack.items
|
214
|
+
|
215
|
+
items[0..-2].each do |item|
|
216
|
+
output << item.color.code << item.frame_style.prefix
|
217
|
+
end
|
218
|
+
|
219
|
+
if (item = items.last)
|
220
|
+
final_color = color || item.color
|
221
|
+
output << CLI::UI.resolve_color(final_color).code \
|
222
|
+
<< item.frame_style.prefix \
|
223
|
+
<< ' ' \
|
224
|
+
<< CLI::UI::Color::RESET.code
|
225
|
+
end
|
182
226
|
end
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
227
|
+
end
|
228
|
+
|
229
|
+
# The width of a prefix given the number of Frames in the stack
|
230
|
+
def prefix_width
|
231
|
+
w = FrameStack.items.reduce(0) do |width, item|
|
232
|
+
width + item.frame_style.prefix_width
|
187
233
|
end
|
188
|
-
|
234
|
+
|
235
|
+
w.zero? ? w : w + 1
|
189
236
|
end
|
190
237
|
|
191
238
|
# Override a color for a given thread.
|
@@ -202,107 +249,19 @@ module CLI
|
|
202
249
|
Thread.current[:cliui_frame_color_override] = prev
|
203
250
|
end
|
204
251
|
|
205
|
-
# The width of a prefix given the number of Frames in the stack
|
206
|
-
#
|
207
|
-
def prefix_width
|
208
|
-
w = FrameStack.items.size
|
209
|
-
w.zero? ? 0 : w + 1
|
210
|
-
end
|
211
|
-
|
212
252
|
private
|
213
253
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
prefix << ' ' << text << ' '
|
226
|
-
end
|
227
|
-
|
228
|
-
termwidth = CLI::UI::Terminal.width
|
229
|
-
|
230
|
-
suffix = +''
|
231
|
-
if right_text
|
232
|
-
suffix << ' ' << right_text << ' '
|
233
|
-
end
|
234
|
-
|
235
|
-
suffix_width = CLI::UI::ANSI.printing_width(suffix)
|
236
|
-
suffix_end = termwidth - 2
|
237
|
-
suffix_start = suffix_end - suffix_width
|
238
|
-
|
239
|
-
prefix_width = CLI::UI::ANSI.printing_width(prefix)
|
240
|
-
prefix_start = 0
|
241
|
-
prefix_end = prefix_start + prefix_width
|
242
|
-
|
243
|
-
if prefix_end > suffix_start
|
244
|
-
suffix = ''
|
245
|
-
# if prefix_end > termwidth
|
246
|
-
# we *could* truncate it, but let's just let it overflow to the
|
247
|
-
# next line and call it poor usage of this API.
|
248
|
-
end
|
249
|
-
|
250
|
-
o = +''
|
251
|
-
|
252
|
-
is_ci = ![0, '', nil].include?(ENV['CI'])
|
253
|
-
|
254
|
-
# Jumping around the line can cause some unwanted flashes
|
255
|
-
o << CLI::UI::ANSI.hide_cursor
|
256
|
-
|
257
|
-
o << 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
|
-
CLI::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
|
-
"\r"
|
265
|
-
end
|
266
|
-
|
267
|
-
o << color.code
|
268
|
-
o << CLI::UI::Box::Heavy::HORZ * termwidth # draw a full line
|
269
|
-
o << print_at_x(prefix_start, prefix, is_ci)
|
270
|
-
o << color.code
|
271
|
-
o << print_at_x(suffix_start, suffix, is_ci)
|
272
|
-
o << CLI::UI::Color::RESET.code
|
273
|
-
o << CLI::UI::ANSI.show_cursor
|
274
|
-
o << "\n"
|
275
|
-
|
276
|
-
o
|
277
|
-
end
|
278
|
-
|
279
|
-
def print_at_x(x, str, is_ci)
|
280
|
-
if is_ci
|
281
|
-
CLI::UI::ANSI.cursor_restore + CLI::UI::ANSI.cursor_forward(x) + str
|
282
|
-
else
|
283
|
-
CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
module FrameStack
|
288
|
-
ENVVAR = 'CLI_FRAME_STACK'
|
289
|
-
|
290
|
-
def self.items
|
291
|
-
ENV.fetch(ENVVAR, '').split(':').map(&:to_sym)
|
292
|
-
end
|
293
|
-
|
294
|
-
def self.push(item)
|
295
|
-
curr = items
|
296
|
-
curr << item.name
|
297
|
-
ENV[ENVVAR] = curr.join(':')
|
298
|
-
end
|
299
|
-
|
300
|
-
def self.pop
|
301
|
-
curr = items
|
302
|
-
ret = curr.pop
|
303
|
-
ENV[ENVVAR] = curr.join(':')
|
304
|
-
ret.nil? ? nil : ret.to_sym
|
305
|
-
end
|
254
|
+
# If timing is:
|
255
|
+
# Numeric: return it
|
256
|
+
# false: return nil
|
257
|
+
# true or nil: defaults to Time.new
|
258
|
+
# Time: return the difference with start
|
259
|
+
def elasped(start, timing)
|
260
|
+
return timing if timing.is_a?(Numeric)
|
261
|
+
return if timing.is_a?(FalseClass)
|
262
|
+
|
263
|
+
timing = Time.new if timing.is_a?(TrueClass) || timing.nil?
|
264
|
+
timing - start
|
306
265
|
end
|
307
266
|
end
|
308
267
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module CLI
|
2
|
+
module UI
|
3
|
+
module Frame
|
4
|
+
module FrameStack
|
5
|
+
COLOR_ENVVAR = 'CLI_FRAME_STACK'
|
6
|
+
STYLE_ENVVAR = 'CLI_STYLE_STACK'
|
7
|
+
|
8
|
+
class StackItem
|
9
|
+
attr_reader :color, :frame_style
|
10
|
+
|
11
|
+
def initialize(color_name, style_name)
|
12
|
+
@color = CLI::UI.resolve_color(color_name)
|
13
|
+
@frame_style = CLI::UI.resolve_style(style_name)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Fetch all items off the frame stack
|
19
|
+
def items
|
20
|
+
colors = ENV.fetch(COLOR_ENVVAR, '').split(':').map(&:to_sym)
|
21
|
+
styles = ENV.fetch(STYLE_ENVVAR, '').split(':').map(&:to_sym)
|
22
|
+
|
23
|
+
colors.length.times.map do |i|
|
24
|
+
StackItem.new(colors[i], styles[i] || Frame.frame_style)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Push a new item onto the frame stack.
|
29
|
+
#
|
30
|
+
# Either an item or a :color/:style pair should be pushed onto the stack.
|
31
|
+
#
|
32
|
+
# ==== Attributes
|
33
|
+
#
|
34
|
+
# * +item+ a +StackItem+ to push onto the stack. Defaults to nil
|
35
|
+
#
|
36
|
+
# ==== Options
|
37
|
+
#
|
38
|
+
# * +:color+ the color of the new stack item. Defaults to nil
|
39
|
+
# * +:style+ the style of the new stack item. Defaults to nil
|
40
|
+
#
|
41
|
+
# ==== Raises
|
42
|
+
#
|
43
|
+
# If both an item and a color/style pair are given, raises an +ArgumentError+
|
44
|
+
# If the given item is not a +StackItem+, raises an +ArgumentError+
|
45
|
+
#
|
46
|
+
def push(item = nil, color: nil, style: nil)
|
47
|
+
unless item.nil?
|
48
|
+
unless item.is_a?(StackItem)
|
49
|
+
raise ArgumentError, 'item must be a StackItem'
|
50
|
+
end
|
51
|
+
|
52
|
+
unless color.nil? && style.nil?
|
53
|
+
raise ArgumentError, 'Must give one of item or color: and style:'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
item ||= StackItem.new(color, style)
|
58
|
+
|
59
|
+
curr = items
|
60
|
+
curr << item
|
61
|
+
|
62
|
+
serialize(curr)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Removes and returns the last stack item off the stack
|
66
|
+
def pop
|
67
|
+
curr = items
|
68
|
+
ret = curr.pop
|
69
|
+
|
70
|
+
serialize(curr)
|
71
|
+
|
72
|
+
ret.nil? ? nil : ret
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Serializes the item stack into two ENV variables.
|
78
|
+
#
|
79
|
+
# This is done to preserve backward compatibility with earlier versions of cli/ui.
|
80
|
+
# This ensures that any code that relied upon previous stack behavior should continue
|
81
|
+
# to work.
|
82
|
+
def serialize(items)
|
83
|
+
colors = []
|
84
|
+
styles = []
|
85
|
+
|
86
|
+
items.each do |item|
|
87
|
+
colors << item.color.name
|
88
|
+
styles << item.frame_style.name
|
89
|
+
end
|
90
|
+
|
91
|
+
ENV[COLOR_ENVVAR] = colors.join(':')
|
92
|
+
ENV[STYLE_ENVVAR] = styles.join(':')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|