cli-ui 1.2.3 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71e70e5d4866f5af664b7fe2393ae41e74e4995a3e9514b42cccaae9bdf5e62a
4
- data.tar.gz: 30742fe01d001bb0b876c874efb2e2878e120b13334d15d2cc964e0bb0b0ac83
3
+ metadata.gz: 48913d44cb2cdae4f699a6f456cce15ca8b50b450317e4734f4bcd8062982d39
4
+ data.tar.gz: ae1d337eb7b280001153a25a5e92ee9b2e534b47d5e95c79a2b9a20371bdadf0
5
5
  SHA512:
6
- metadata.gz: ba955e4dab0ddc389e1f878f2ddc689a4cb5581aafe7e96f4eacaf7cf7cc79b23081bd2fbf21a85c648b679f265d6f442e0ac1b8abde62c14d4f1dc5c258c90c
7
- data.tar.gz: 1de7e63523615f6a39e756e106fb78000b05be0fa1ad00d70e471ba6b0b39fcfa702e624fa58d36d3d49da58526899337f33a5395b8f74513b726b84b04584d4
6
+ metadata.gz: 1e87d0b821c5911d1e10680619522b5f36a19213042e187cc54097850dd2ccb986db4a6dbf800234c43102377d168d6a5dda573f577763dff754e4cee58fc50b
7
+ data.tar.gz: 2cfb1d76a7b6f49f1f14eadf7237c644a2c2f1896dfff8dcaff9c5b68d354194df0da34339817d8153ef6815ff2898d15b6bb7bd6bb68198bdc614505072aaf3
@@ -0,0 +1 @@
1
+ * @Shopify/dev-infra
data/README.md CHANGED
@@ -115,6 +115,19 @@ puts CLI::UI.fmt "{{*}} {{x}} {{?}} {{v}}"
115
115
 
116
116
  ---
117
117
 
118
+ ### Status Widget
119
+
120
+ ```ruby
121
+ CLI::UI::Spinner.spin("building packages: {{@widget/status:1:2:3:4}}") do |spinner|
122
+ # spinner.update_title(...)
123
+ sleep(3)
124
+ end
125
+ ```
126
+
127
+ ![Status Widget](https://user-images.githubusercontent.com/1284/61405142-11042580-a8a7-11e9-9885-46ba44c46358.gif)
128
+
129
+ ---
130
+
118
131
  ### Progress Bar
119
132
 
120
133
  Show progress of a process or operation.
@@ -11,6 +11,7 @@ module CLI
11
11
  autoload :Truncater, 'cli/ui/truncater'
12
12
  autoload :Formatter, 'cli/ui/formatter'
13
13
  autoload :Spinner, 'cli/ui/spinner'
14
+ autoload :Widgets, 'cli/ui/widgets'
14
15
 
15
16
  # Convenience accessor to +CLI::UI::Spinner::SpinGroup+
16
17
  SpinGroup = Spinner::SpinGroup
@@ -31,6 +31,9 @@ module CLI
31
31
  BOLD = new('1', :bold)
32
32
  WHITE = new('97', :white)
33
33
 
34
+ # 240 is very dark gray; 255 is very light gray. 244 is somewhat dark.
35
+ GRAY = new('38;5;244', :grey)
36
+
34
37
  MAP = {
35
38
  red: RED,
36
39
  green: GREEN,
@@ -40,6 +43,7 @@ module CLI
40
43
  cyan: CYAN,
41
44
  reset: RESET,
42
45
  bold: BOLD,
46
+ gray: GRAY,
43
47
  }.freeze
44
48
 
45
49
  class InvalidColorName < ArgumentError
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
-
3
- require 'cli/ui'
4
- require 'strscan'
2
+ require('cli/ui')
3
+ require('strscan')
5
4
 
6
5
  module CLI
7
6
  module UI
@@ -16,7 +15,7 @@ module CLI
16
15
  'red' => '31',
17
16
  'green' => '32',
18
17
  'yellow' => '33',
19
- # default blue is low-contrast against black in some default terminal color scheme
18
+ # default blue is low-contrast against black in some default terminal color scheme
20
19
  'blue' => '94', # 9x = high-intensity fg color x
21
20
  'magenta' => '35',
22
21
  'cyan' => '36',
@@ -36,16 +35,17 @@ module CLI
36
35
  BEGIN_EXPR = '{{'
37
36
  END_EXPR = '}}'
38
37
 
38
+ SCAN_WIDGET = %r[@widget/(?<handle>\w+):(?<args>.*?)}}]
39
39
  SCAN_FUNCNAME = /\w+:/
40
40
  SCAN_GLYPH = /.}}/
41
- SCAN_BODY = /
41
+ SCAN_BODY = %r{
42
42
  .*?
43
43
  (
44
44
  #{BEGIN_EXPR} |
45
45
  #{END_EXPR} |
46
46
  \z
47
47
  )
48
- /mx
48
+ }mx
49
49
 
50
50
  DISCARD_BRACES = 0..-3
51
51
 
@@ -123,7 +123,7 @@ module CLI
123
123
  end
124
124
 
125
125
  def parse_expr(sc, stack)
126
- if match = sc.scan(SCAN_GLYPH)
126
+ if (match = sc.scan(SCAN_GLYPH))
127
127
  glyph_handle = match[0]
128
128
  begin
129
129
  glyph = Glyph.lookup(glyph_handle)
@@ -136,7 +136,20 @@ module CLI
136
136
  index
137
137
  )
138
138
  end
139
- elsif match = sc.scan(SCAN_FUNCNAME)
139
+ elsif (match = sc.scan(SCAN_WIDGET))
140
+ match_data = SCAN_WIDGET.match(match) # Regexp.last_match doesn't work here
141
+ widget_handle = match_data['handle']
142
+ begin
143
+ widget = Widgets.lookup(widget_handle)
144
+ emit(widget.call(match_data['args']), stack)
145
+ rescue Widgets::InvalidWidgetHandle
146
+ index = sc.pos - 2 # rewind past '}}'
147
+ raise(FormatError.new(
148
+ "invalid widget handle at index #{index}: '#{widget_handle}'",
149
+ @text, index,
150
+ ))
151
+ end
152
+ elsif (match = sc.scan(SCAN_FUNCNAME))
140
153
  funcname = match.chop
141
154
  stack.push(funcname)
142
155
  else
@@ -249,26 +249,32 @@ module CLI
249
249
 
250
250
  o = +''
251
251
 
252
- is_ci = ![0, '', nil].include?(ENV['CI'])
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
253
266
 
254
267
  # Jumping around the line can cause some unwanted flashes
255
268
  o << CLI::UI::ANSI.hide_cursor
256
269
 
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
270
+ # reset to column 1 so that things like ^C don't ruin formatting
271
+ o << "\r"
266
272
 
267
273
  o << color.code
268
274
  o << CLI::UI::Box::Heavy::HORZ * termwidth # draw a full line
269
- o << print_at_x(prefix_start, prefix, is_ci)
275
+ o << print_at_x(prefix_start, prefix)
270
276
  o << color.code
271
- o << print_at_x(suffix_start, suffix, is_ci)
277
+ o << print_at_x(suffix_start, suffix)
272
278
  o << CLI::UI::Color::RESET.code
273
279
  o << CLI::UI::ANSI.show_cursor
274
280
  o << "\n"
@@ -276,12 +282,8 @@ module CLI
276
282
  o
277
283
  end
278
284
 
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
+ def print_at_x(x, str)
286
+ CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
285
287
  end
286
288
 
287
289
  module FrameStack
@@ -29,7 +29,7 @@ module CLI
29
29
  @handle = handle
30
30
  @codepoint = codepoint
31
31
  @color = color
32
- @char = [codepoint].pack('U')
32
+ @char = Array(codepoint).pack('U*')
33
33
  @to_s = color.code + char + Color::RESET.code
34
34
  @fmt = "{{#{color.name}:#{char}}}"
35
35
 
@@ -38,20 +38,14 @@ module CLI
38
38
 
39
39
  # Mapping of glyphs to terminal output
40
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)
51
- # Bug emoji (🐛)
52
- BUG = new('b', 0x1f41b, Color::WHITE)
53
- # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
54
- CHEVRON = new('>', 0xbb, Color::YELLOW)
41
+ STAR = new('*', 0x2b51, Color::YELLOW) # YELLOW SMALL STAR (⭑)
42
+ INFO = new('i', 0x1d4be, Color::BLUE) # BLUE MATHEMATICAL SCRIPT SMALL i (𝒾)
43
+ QUESTION = new('?', 0x003f, Color::BLUE) # BLUE QUESTION MARK (?)
44
+ CHECK = new('v', 0x2713, Color::GREEN) # GREEN CHECK MARK (✓)
45
+ X = new('x', 0x2717, Color::RED) # RED BALLOT X ()
46
+ BUG = new('b', 0x1f41b, Color::WHITE) # Bug emoji (🐛)
47
+ CHEVRON = new('>', 0xbb, Color::YELLOW) # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
48
+ HOURGLASS = new('H', [0x231b, 0xfe0e], Color::BLUE) # HOURGLASS + VARIATION SELECTOR 15 (⌛︎)
55
49
 
56
50
  # Looks up a glyph by name
57
51
  #
@@ -88,6 +88,33 @@ module CLI
88
88
  end
89
89
  end
90
90
 
91
+ # Asks the user for a single-line answer, without displaying the characters while typing.
92
+ # Typically used for password prompts
93
+ #
94
+ # ==== Return Value
95
+ #
96
+ # The password, without a trailing newline.
97
+ # If the user simply presses "Enter" without typing any password, this will return an empty string.
98
+ def ask_password(question)
99
+ require 'io/console'
100
+
101
+ CLI::UI.with_frame_color(:blue) do
102
+ STDOUT.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
103
+
104
+ # noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
105
+ # No fancy Readline integration (like echoing back) is required for a password prompt anyway.
106
+ password = STDIN.noecho do
107
+ # Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
108
+ # " 123 \n".chomp => " 123 "
109
+ STDIN.gets.chomp
110
+ end
111
+
112
+ STDOUT.puts # Complete the line
113
+
114
+ password
115
+ end
116
+ end
117
+
91
118
  # Asks the user a yes/no question.
92
119
  # Can use arrows, y/n, numbers (1/2), and vim bindings to control
93
120
  #
@@ -288,6 +288,8 @@ module CLI
288
288
  case char
289
289
  when 'A' ; up
290
290
  when 'B' ; down
291
+ when 'C' ; # Ignore right key
292
+ when 'D' ; # Ignore left key
291
293
  else ; raise Interrupt # unhandled escape sequence.
292
294
  end
293
295
  end
@@ -1,3 +1,4 @@
1
+ # frozen-string-literal: true
1
2
  require 'cli/ui'
2
3
 
3
4
  module CLI
@@ -9,11 +10,30 @@ module CLI
9
10
  PERIOD = 0.1 # seconds
10
11
  TASK_FAILED = :task_failed
11
12
 
13
+ RUNES = %w(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏).freeze
14
+
12
15
  begin
13
- runes = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
14
16
  colors = [CLI::UI::Color::CYAN.code] * 5 + [CLI::UI::Color::MAGENTA.code] * 5
15
- raise unless runes.size == colors.size
16
- GLYPHS = colors.zip(runes).map(&:join)
17
+ raise unless RUNES.size == colors.size
18
+ GLYPHS = colors.zip(RUNES).map(&:join)
19
+ end
20
+
21
+ class << self
22
+ attr_accessor(:index)
23
+
24
+ # We use this from CLI::UI::Widgets::Status to render an additional
25
+ # spinner next to the "working" element. While this global state looks
26
+ # a bit repulsive at first, it's worth realizing that:
27
+ #
28
+ # * It's managed by the SpinGroup#wait method, not individual tasks; and
29
+ # * It would be complete insanity to run two separate but concurrent SpinGroups.
30
+ #
31
+ # While it would be possible to stitch through some connection between
32
+ # the SpinGroup and the Widgets included in its title, this is simpler
33
+ # in practice and seems unlikely to cause issues in practice.
34
+ def current_rune
35
+ RUNES[index || 0]
36
+ end
17
37
  end
18
38
 
19
39
  # Adds a single spinner
@@ -40,6 +40,7 @@ module CLI
40
40
  #
41
41
  def initialize(title, &block)
42
42
  @title = title
43
+ @always_full_render = title =~ Formatter::SCAN_WIDGET
43
44
  @thread = Thread.new do
44
45
  cap = CLI::UI::StdoutRouter::Capture.new(self, with_frame_inset: false, &block)
45
46
  begin
@@ -75,7 +76,17 @@ module CLI
75
76
  @done
76
77
  end
77
78
 
78
- # Re-renders the task if required
79
+ # Re-renders the task if required:
80
+ #
81
+ # We try to be as lazy as possible in re-rendering the full line. The
82
+ # spinner rune will change on each render for the most part, but the
83
+ # body text will rarely have changed. If the body text *has* changed,
84
+ # we set @force_full_render.
85
+ #
86
+ # Further, if the title string includes any CLI::UI::Widgets, we
87
+ # assume that it may change from render to render, since those
88
+ # evaluate more dynamically than the rest of our format codes, which
89
+ # are just text formatters. This is controlled by @always_full_render.
79
90
  #
80
91
  # ==== Attributes
81
92
  #
@@ -84,8 +95,11 @@ module CLI
84
95
  # * +width+ - current terminal width to format for
85
96
  #
86
97
  def render(index, force = true, width: CLI::UI::Terminal.width)
87
- return full_render(index, width) if force || @force_full_render
88
- partial_render(index)
98
+ if force || @always_full_render || @force_full_render
99
+ full_render(index, width)
100
+ else
101
+ partial_render(index)
102
+ end
89
103
  ensure
90
104
  @force_full_render = false
91
105
  end
@@ -97,6 +111,7 @@ module CLI
97
111
  # * +title+ - title to change the spinner to
98
112
  #
99
113
  def update_title(new_title)
114
+ @always_full_render = new_title =~ Formatter::SCAN_WIDGET
100
115
  @title = new_title
101
116
  @force_full_render = true
102
117
  end
@@ -194,6 +209,7 @@ module CLI
194
209
  break if all_done
195
210
 
196
211
  idx = (idx + 1) % GLYPHS.size
212
+ Spinner.index = idx
197
213
  sleep(PERIOD)
198
214
  end
199
215
 
@@ -1,5 +1,5 @@
1
1
  module CLI
2
2
  module UI
3
- VERSION = "1.2.3"
3
+ VERSION = "1.3.0"
4
4
  end
5
5
  end
@@ -0,0 +1,75 @@
1
+ require('cli/ui')
2
+
3
+ module CLI
4
+ module UI
5
+ # Widgets are formatter objects with more custom implementations than the
6
+ # other features, which all center around formatting text with colours,
7
+ # etc.
8
+ #
9
+ # If you want to extend CLI::UI with your own widgets, you may want to do
10
+ # something like this:
11
+ #
12
+ # require('cli/ui')
13
+ # class MyWidget < CLI::UI::Widgets::Base
14
+ # # ...
15
+ # end
16
+ # CLI::UI::Widgets.register('my-widget') { MyWidget }
17
+ # puts(CLI::UI.fmt("{{@widget/my-widget:args}}"))
18
+ module Widgets
19
+ MAP = {}
20
+
21
+ autoload(:Base, 'cli/ui/widgets/base')
22
+
23
+ def self.register(name, &cb)
24
+ MAP[name] = cb
25
+ end
26
+
27
+ autoload(:Status, 'cli/ui/widgets/status')
28
+ register('status') { Widgets::Status }
29
+
30
+ # Looks up a widget by handle
31
+ #
32
+ # ==== Raises
33
+ # Raises InvalidWidgetHandle if the widget is not available.
34
+ #
35
+ # ==== Returns
36
+ # A callable widget, to be invoked like `.call(argstring)`
37
+ #
38
+ def self.lookup(handle)
39
+ MAP.fetch(handle.to_s).call
40
+ rescue KeyError, NameError
41
+ raise(InvalidWidgetHandle, handle)
42
+ end
43
+
44
+ # All available widgets by name
45
+ #
46
+ def self.available
47
+ MAP.keys
48
+ end
49
+
50
+ class InvalidWidgetHandle < ArgumentError
51
+ def initialize(handle)
52
+ @handle = handle
53
+ end
54
+
55
+ def message
56
+ keys = Widget.available.join(',')
57
+ "invalid widget handle: #{@handle} " \
58
+ "-- must be one of CLI::UI::Widgets.available (#{keys})"
59
+ end
60
+ end
61
+
62
+ class InvalidWidgetArguments < ArgumentError
63
+ def initialize(argstring, pattern)
64
+ @argstring = argstring
65
+ @pattern = pattern
66
+ end
67
+
68
+ def message
69
+ "invalid widget arguments: #{@argstring} " \
70
+ "-- must match pattern: #{@pattern.inspect}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,27 @@
1
+ require('cli/ui')
2
+
3
+ module CLI
4
+ module UI
5
+ module Widgets
6
+ class Base
7
+ def self.call(argstring)
8
+ new(argstring).render
9
+ end
10
+
11
+ def initialize(argstring)
12
+ pat = self.class.argparse_pattern
13
+ unless (@match_data = pat.match(argstring))
14
+ raise(Widgets::InvalidWidgetArguments.new(argstring, pat))
15
+ end
16
+ @match_data.names.each do |name|
17
+ instance_variable_set(:"@#{name}", @match_data[name])
18
+ end
19
+ end
20
+
21
+ def self.argparse_pattern
22
+ const_get(:ARGPARSE_PATTERN)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # frozen-string-literal: true
2
+ require('cli/ui')
3
+
4
+ module CLI
5
+ module UI
6
+ module Widgets
7
+ class Status < Widgets::Base
8
+ ARGPARSE_PATTERN = %r{
9
+ \A (?<succeeded> \d+)
10
+ : (?<failed> \d+)
11
+ : (?<working> \d+)
12
+ : (?<pending> \d+) \z
13
+ }x # e.g. "1:23:3:404"
14
+ OPEN = Color::RESET.code + Color::BOLD.code + '[' + Color::RESET.code
15
+ CLOSE = Color::RESET.code + Color::BOLD.code + ']' + Color::RESET.code
16
+ ARROW = Color::RESET.code + Color::GRAY.code + '◂' + Color::RESET.code
17
+ COMMA = Color::RESET.code + Color::GRAY.code + ',' + Color::RESET.code
18
+
19
+ SPINNER_STOPPED = '⠿'
20
+ EMPTY_SET = '∅'
21
+
22
+ def render
23
+ if zero?(@succeeded) && zero?(@failed) && zero?(@working) && zero?(@pending)
24
+ Color::RESET.code + Color::BOLD.code + EMPTY_SET + Color::RESET.code
25
+ else
26
+ # [ 0✓ , 2✗ ◂ 3⠼ ◂ 4⌛︎ ]
27
+ "#{OPEN}#{succeeded_part}#{COMMA}#{failed_part}#{ARROW}#{working_part}#{ARROW}#{pending_part}#{CLOSE}"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def zero?(num_str)
34
+ num_str == '0'
35
+ end
36
+
37
+ def colorize_if_nonzero(num_str, rune, color)
38
+ color = Color::GRAY if zero?(num_str)
39
+ color.code + num_str + rune
40
+ end
41
+
42
+ def succeeded_part
43
+ colorize_if_nonzero(@succeeded, Glyph::CHECK.char, Color::GREEN)
44
+ end
45
+
46
+ def failed_part
47
+ colorize_if_nonzero(@failed, Glyph::X.char, Color::RED)
48
+ end
49
+
50
+ def working_part
51
+ rune = zero?(@working) ? SPINNER_STOPPED : Spinner.current_rune
52
+ colorize_if_nonzero(@working, rune, Color::BLUE)
53
+ end
54
+
55
+ def pending_part
56
+ colorize_if_nonzero(@pending, Glyph::HOURGLASS.char, Color::WHITE)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cli-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2019-03-25 00:00:00.000000000 Z
13
+ date: 2019-08-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
@@ -49,6 +49,7 @@ executables: []
49
49
  extensions: []
50
50
  extra_rdoc_files: []
51
51
  files:
52
+ - ".github/CODEOWNERS"
52
53
  - ".github/probots.yml"
53
54
  - ".gitignore"
54
55
  - ".rubocop.yml"
@@ -78,6 +79,9 @@ files:
78
79
  - lib/cli/ui/terminal.rb
79
80
  - lib/cli/ui/truncater.rb
80
81
  - lib/cli/ui/version.rb
82
+ - lib/cli/ui/widgets.rb
83
+ - lib/cli/ui/widgets/base.rb
84
+ - lib/cli/ui/widgets/status.rb
81
85
  homepage: https://github.com/shopify/cli-ui
82
86
  licenses:
83
87
  - MIT
@@ -97,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
101
  - !ruby/object:Gem::Version
98
102
  version: '0'
99
103
  requirements: []
100
- rubygems_version: 3.0.2
104
+ rubygems_version: 3.0.3
101
105
  signing_key:
102
106
  specification_version: 4
103
107
  summary: Terminal UI framework