cli-ui 1.2.2 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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, "failure_text is not compatible with blockless invocation"
83
+ raise ArgumentError, 'failure_text is not compatible with blockless invocation'
62
84
  elsif success_text
63
- raise ArgumentError, "success_text is not compatible with blockless invocation"
64
- elsif !timing.nil?
65
- raise ArgumentError, "timing is not compatible with blockless invocation"
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
- 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,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
- 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
- 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