cli-ui 1.3.0 → 1.4.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.
@@ -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,8 +72,10 @@ 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?
@@ -61,18 +83,17 @@ module CLI
61
83
  raise ArgumentError, "failure_text is not compatible with blockless invocation"
62
84
  elsif success_text
63
85
  raise ArgumentError, "success_text is not compatible with blockless invocation"
64
- elsif !timing.nil?
86
+ elsif timing
65
87
  raise ArgumentError, "timing is not compatible with blockless invocation"
66
88
  end
67
89
  end
68
90
 
69
- timing = true if timing.nil?
70
-
71
- t_start = Time.now.to_f
91
+ t_start = Time.now
72
92
  CLI::UI.raw do
73
- puts edge(text, color: color, first: CLI::UI::Box::Heavy::TL)
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 = timing ? (Time.now.to_f - t_start) : nil
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 = timing ? (Time.now.to_f - t_start) : nil
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
- # Closes a frame
103
- # Automatically called for a block-form +open+
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
- # * +:elapsed+ - How long did the frame take? Defaults to nil
133
+ # * +frame_style+ - The frame style to use for this frame
113
134
  #
114
135
  # ==== Example
115
136
  #
116
- # CLI::UI::Frame.close('Close')
137
+ # CLI::UI::Frame.open('Open') { CLI::UI::Frame.divider('Divider') }
117
138
  #
118
- # Output:
119
- # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
139
+ # Default Output:
140
+ # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
141
+ # ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
142
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
120
143
  #
144
+ # ==== Raises
121
145
  #
122
- def close(text, color: DEFAULT_FRAME_COLOR, elapsed: nil)
123
- color = CLI::UI.resolve_color(color)
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
- puts edge(text, color: color, first: CLI::UI::Box::Heavy::BL, **kwargs)
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
- # Adds a divider in a frame
136
- # Used to separate information within a single frame
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 +DEFAULT_FRAME_COLOR+
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.open('Open') { CLI::UI::Frame.divider('Divider') }
178
+ # CLI::UI::Frame.close('Close')
149
179
  #
150
- # Output:
151
- # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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 divider(text, color: nil)
187
+ def close(text, color: nil, elapsed: nil, frame_style: nil)
160
188
  fs_item = FrameStack.pop
161
- raise UnnestedFrameException, "no frame nesting to unnest" unless fs_item
162
- color = CLI::UI.resolve_color(color)
163
- item = CLI::UI.resolve_color(fs_item)
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
- puts edge(text, color: (color || item), first: CLI::UI::Box::Heavy::DIV)
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]+ or nil
209
+ # * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+
176
210
  #
177
- def prefix(color: nil)
178
- pfx = +''
179
- items = FrameStack.items
180
- items[0..-2].each do |item|
181
- pfx << CLI::UI.resolve_color(item).code << CLI::UI::Box::Heavy::VERT
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
- if item = items.last
184
- c = Thread.current[:cliui_frame_color_override] || color || item
185
- pfx << CLI::UI.resolve_color(c).code \
186
- << CLI::UI::Box::Heavy::VERT << ' ' << CLI::UI::Color::RESET.code
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
- pfx
234
+
235
+ w.zero? ? w : w + 1
189
236
  end
190
237
 
191
238
  # Override a color for a given thread.
@@ -202,109 +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
- def edge(text, color: raise, first: raise, right_text: nil)
215
- color = CLI::UI.resolve_color(color)
216
- text = CLI::UI.resolve_text("{{#{color.name}:#{text}}}")
217
-
218
- prefix = +''
219
- FrameStack.items.each do |item|
220
- prefix << CLI::UI.resolve_color(item).code << CLI::UI::Box::Heavy::VERT
221
- end
222
- prefix << color.code << first << (CLI::UI::Box::Heavy::HORZ * 2)
223
- text ||= ''
224
- unless text.empty?
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
- # Shopify's CI system supports terminal emulation, but not some of
253
- # the fancier features that we normally use to draw frames
254
- # extra-reliably, so we fall back to a less foolproof strategy. This
255
- # is probably better in general for cases with impoverished terminal
256
- # emulators and no active user.
257
- if (is_ci = ![0, '', nil].include?(ENV['CI']))
258
- linewidth = [0, termwidth - (prefix_width + suffix_width)].max
259
-
260
- o << color.code << prefix
261
- o << color.code << (CLI::UI::Box::Heavy::HORZ * linewidth)
262
- o << color.code << suffix
263
- o << CLI::UI::Color::RESET.code << "\n"
264
- return o
265
- end
266
-
267
- # Jumping around the line can cause some unwanted flashes
268
- o << CLI::UI::ANSI.hide_cursor
269
-
270
- # reset to column 1 so that things like ^C don't ruin formatting
271
- o << "\r"
272
-
273
- o << color.code
274
- o << CLI::UI::Box::Heavy::HORZ * termwidth # draw a full line
275
- o << print_at_x(prefix_start, prefix)
276
- o << color.code
277
- o << print_at_x(suffix_start, suffix)
278
- o << CLI::UI::Color::RESET.code
279
- o << CLI::UI::ANSI.show_cursor
280
- o << "\n"
281
-
282
- o
283
- end
284
-
285
- def print_at_x(x, str)
286
- CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
287
- end
288
-
289
- module FrameStack
290
- ENVVAR = 'CLI_FRAME_STACK'
291
-
292
- def self.items
293
- ENV.fetch(ENVVAR, '').split(':').map(&:to_sym)
294
- end
295
-
296
- def self.push(item)
297
- curr = items
298
- curr << item.name
299
- ENV[ENVVAR] = curr.join(':')
300
- end
301
-
302
- def self.pop
303
- curr = items
304
- ret = curr.pop
305
- ENV[ENVVAR] = curr.join(':')
306
- ret.nil? ? nil : ret.to_sym
307
- 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
308
265
  end
309
266
  end
310
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