cli-ui 1.5.1 → 2.1.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: 1e28f7cb02b8647ee32e91b11f2b25b450c33a245514a520783d50cc6d5f7d84
4
- data.tar.gz: 894e2a52591d18c5210efa36beab81d761c18734a42f8362d8af0c97e3bd4ae2
3
+ metadata.gz: eb23f8ab0044a484b7f6f8dcb57240d5b5ba9a3db6caedb6090186c3d7a55143
4
+ data.tar.gz: 9c3e0d024f05ed428a0afa190fbc440008e58d8f58cf244c452259722883dd50
5
5
  SHA512:
6
- metadata.gz: af8bd2b36c0d4c65e93aa966062e040109f5a10347404b112331975f04eb9cfe2a8601a9a9d1d4c1787f92f5a306b81f68e21d1e4f6728705fdfc81dd58fc25c
7
- data.tar.gz: a94fa31ff9991a26202f8c4d4d85f87d9efdd23489ed47c5f719b0389a6106be4eee4ee86075fcddafe367d847b8299f69e6fe014fdffce629b0c9df43e156d1
6
+ metadata.gz: 36dccad36f8cc8eeff6bc9ad2e7deb2e37427ccf5887fb543d27485ff5cfc8e441355edc9cb92f954eef9e1a45fb5d2d78e8b84b510dec12809e3e1df2071cea
7
+ data.tar.gz: 31c99ff115d4061d4f9441063177784f9f6656cafee9bd6b85fc6d48e30ab855e4cf83138d8f305fb86542edd4fbe2226db26cf448e70b3b8677bf18ce4a388c
data/README.md CHANGED
@@ -3,7 +3,7 @@ CLI UI
3
3
 
4
4
  CLI UI is a small framework for generating nice command-line user interfaces
5
5
 
6
- - [Master Documentation](http://www.rubydoc.info/github/Shopify/cli-ui/master/CLI/UI)
6
+ - [Master Documentation](http://www.rubydoc.info/github/Shopify/cli-ui/main/CLI/UI)
7
7
  - [Documentation of the Rubygems version](http://www.rubydoc.info/gems/cli-ui/)
8
8
  - [Rubygems](https://rubygems.org/gems/cli-ui)
9
9
 
@@ -23,7 +23,7 @@ In your code, simply add a `require 'cli/ui'`. Most options assume `CLI::UI::Std
23
23
 
24
24
  ## Features
25
25
 
26
- This may not be an exhaustive list. Please check our [documentation](http://www.rubydoc.info/github/Shopify/cli-ui/master/CLI/UI) for more information.
26
+ This may not be an exhaustive list. Please check our [documentation](http://www.rubydoc.info/github/Shopify/cli-ui/main/CLI/UI) for more information.
27
27
 
28
28
  ---
29
29
 
@@ -83,10 +83,10 @@ CLI::UI.ask('Is CLI UI Awesome?', default: 'It is great!')
83
83
  Handle many multi-threaded processes while suppressing output unless there is an issue. Can update title to show state.
84
84
 
85
85
  ```ruby
86
- spin_group = CLI::UI::SpinGroup.new
87
- spin_group.add('Title') { |spinner| sleep 3.0 }
88
- spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
89
- spin_group.wait
86
+ CLI::UI::SpinGroup.new do |spin_group|
87
+ spin_group.add('Title') { |spinner| sleep 3.0 }
88
+ spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
89
+ end
90
90
  ```
91
91
 
92
92
  ![Spinner Group](https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif)
@@ -175,6 +175,12 @@ end
175
175
 
176
176
  ---
177
177
 
178
+ ## Sorbet
179
+
180
+ We make use of [Sorbet](https://sorbet.org/) in cli-ui. We provide stubs for Sorbet so that you can use this gem even
181
+ if you aren't using Sorbet. We activate these stubs if `T` is undefined when the gem is loaded. For this reason, if you
182
+ would like to use this gem and your project _does_ use Sorbet, ensure you load Sorbet _before_ loading cli-ui.
183
+
178
184
  ## Example Usage
179
185
 
180
186
  The following code makes use of nested-framing, multi-threaded spinners, formatted text, and more.
@@ -187,22 +193,22 @@ CLI::UI::StdoutRouter.enable
187
193
  CLI::UI::Frame.open('{{*}} {{bold:a}}', color: :green) do
188
194
  CLI::UI::Frame.open('{{i}} b', color: :magenta) do
189
195
  CLI::UI::Frame.open('{{?}} c', color: :cyan) do
190
- sg = CLI::UI::SpinGroup.new
191
- sg.add('wow') do |spinner|
192
- sleep(2.5)
193
- spinner.update_title('second round!')
194
- sleep (1.0)
196
+ CLI::UI::SpinGroup.new do |sg|
197
+ sg.add('wow') do |spinner|
198
+ sleep(2.5)
199
+ spinner.update_title('second round!')
200
+ sleep (1.0)
201
+ end
202
+ sg.add('such spin') { sleep(1.6) }
203
+ sg.add('many glyph') { sleep(2.0) }
195
204
  end
196
- sg.add('such spin') { sleep(1.6) }
197
- sg.add('many glyph') { sleep(2.0) }
198
- sg.wait
199
205
  end
200
206
  end
201
207
  CLI::UI::Frame.divider('{{v}} lol')
202
208
  puts CLI::UI.fmt '{{info:words}} {{red:oh no!}} {{green:success!}}'
203
- sg = CLI::UI::SpinGroup.new
204
- sg.add('more spins') { sleep(0.5) ; raise 'oh no' }
205
- sg.wait
209
+ CLI::UI::SpinGroup.new do |sg|
210
+ sg.add('more spins') { sleep(0.5) ; raise 'oh no' }
211
+ end
206
212
  end
207
213
  ```
208
214
 
data/lib/cli/ui/ansi.rb CHANGED
@@ -1,156 +1,184 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
4
 
3
5
  module CLI
4
6
  module UI
5
7
  module ANSI
8
+ extend T::Sig
9
+
6
10
  ESC = "\x1b"
7
11
 
8
- # ANSI escape sequences (like \x1b[31m) have zero width.
9
- # when calculating the padding width, we must exclude them.
10
- # This also implements a basic version of utf8 character width calculation like
11
- # we could get for real from something like utf8proc.
12
- #
13
- def self.printing_width(str)
14
- zwj = false
15
- strip_codes(str).codepoints.reduce(0) do |acc, cp|
16
- if zwj
17
- zwj = false
18
- next acc
19
- end
20
- case cp
21
- when 0x200d # zero-width joiner
22
- zwj = true
23
- acc
24
- when "\n"
25
- acc
26
- else
27
- acc + 1
12
+ class << self
13
+ extend T::Sig
14
+
15
+ # ANSI escape sequences (like \x1b[31m) have zero width.
16
+ # when calculating the padding width, we must exclude them.
17
+ # This also implements a basic version of utf8 character width calculation like
18
+ # we could get for real from something like utf8proc.
19
+ #
20
+ sig { params(str: String).returns(Integer) }
21
+ def printing_width(str)
22
+ zwj = T.let(false, T::Boolean)
23
+ strip_codes(str).codepoints.reduce(0) do |acc, cp|
24
+ if zwj
25
+ zwj = false
26
+ next acc
27
+ end
28
+ case cp
29
+ when 0x200d # zero-width joiner
30
+ zwj = true
31
+ acc
32
+ when "\n"
33
+ acc
34
+ else
35
+ acc + 1
36
+ end
28
37
  end
29
38
  end
30
- end
31
39
 
32
- # Strips ANSI codes from a str
33
- #
34
- # ==== Attributes
35
- #
36
- # - +str+ - The string from which to strip codes
37
- #
38
- def self.strip_codes(str)
39
- str.gsub(/\x1b\[[\d;]+[A-z]|\r/, '')
40
- end
40
+ # Strips ANSI codes from a str
41
+ #
42
+ # ==== Attributes
43
+ #
44
+ # - +str+ - The string from which to strip codes
45
+ #
46
+ sig { params(str: String).returns(String) }
47
+ def strip_codes(str)
48
+ str.gsub(/\x1b\[[\d;]+[A-z]|\r/, '')
49
+ end
41
50
 
42
- # Returns an ANSI control sequence
43
- #
44
- # ==== Attributes
45
- #
46
- # - +args+ - Argument to pass to the ANSI control sequence
47
- # - +cmd+ - ANSI control sequence Command
48
- #
49
- def self.control(args, cmd)
50
- ESC + '[' + args + cmd
51
- end
51
+ # Returns an ANSI control sequence
52
+ #
53
+ # ==== Attributes
54
+ #
55
+ # - +args+ - Argument to pass to the ANSI control sequence
56
+ # - +cmd+ - ANSI control sequence Command
57
+ #
58
+ sig { params(args: String, cmd: String).returns(String) }
59
+ def control(args, cmd)
60
+ ESC + '[' + args + cmd
61
+ end
52
62
 
53
- # https://en.wikipedia.org/wiki/ANSI_escape_code#graphics
54
- def self.sgr(params)
55
- control(params.to_s, 'm')
56
- end
63
+ # https://en.wikipedia.org/wiki/ANSI_escape_code#graphics
64
+ sig { params(params: String).returns(String) }
65
+ def sgr(params)
66
+ control(params, 'm')
67
+ end
57
68
 
58
- # Cursor Movement
59
-
60
- # Move the cursor up n lines
61
- #
62
- # ==== Attributes
63
- #
64
- # * +n+ - number of lines by which to move the cursor up
65
- #
66
- def self.cursor_up(n = 1)
67
- return '' if n.zero?
68
- control(n.to_s, 'A')
69
- end
69
+ # Cursor Movement
70
70
 
71
- # Move the cursor down n lines
72
- #
73
- # ==== Attributes
74
- #
75
- # * +n+ - number of lines by which to move the cursor down
76
- #
77
- def self.cursor_down(n = 1)
78
- return '' if n.zero?
79
- control(n.to_s, 'B')
80
- end
71
+ # Move the cursor up n lines
72
+ #
73
+ # ==== Attributes
74
+ #
75
+ # * +n+ - number of lines by which to move the cursor up
76
+ #
77
+ sig { params(n: Integer).returns(String) }
78
+ def cursor_up(n = 1)
79
+ return '' if n.zero?
81
80
 
82
- # Move the cursor forward n columns
83
- #
84
- # ==== Attributes
85
- #
86
- # * +n+ - number of columns by which to move the cursor forward
87
- #
88
- def self.cursor_forward(n = 1)
89
- return '' if n.zero?
90
- control(n.to_s, 'C')
91
- end
81
+ control(n.to_s, 'A')
82
+ end
92
83
 
93
- # Move the cursor back n columns
94
- #
95
- # ==== Attributes
96
- #
97
- # * +n+ - number of columns by which to move the cursor back
98
- #
99
- def self.cursor_back(n = 1)
100
- return '' if n.zero?
101
- control(n.to_s, 'D')
102
- end
84
+ # Move the cursor down n lines
85
+ #
86
+ # ==== Attributes
87
+ #
88
+ # * +n+ - number of lines by which to move the cursor down
89
+ #
90
+ sig { params(n: Integer).returns(String) }
91
+ def cursor_down(n = 1)
92
+ return '' if n.zero?
93
+
94
+ control(n.to_s, 'B')
95
+ end
103
96
 
104
- # Move the cursor to a specific column
105
- #
106
- # ==== Attributes
107
- #
108
- # * +n+ - The column to move to
109
- #
110
- def self.cursor_horizontal_absolute(n = 1)
111
- cmd = control(n.to_s, 'G')
112
- cmd += control('1', 'D') if CLI::UI::OS.current.shift_cursor_on_line_reset?
113
- cmd
114
- end
97
+ # Move the cursor forward n columns
98
+ #
99
+ # ==== Attributes
100
+ #
101
+ # * +n+ - number of columns by which to move the cursor forward
102
+ #
103
+ sig { params(n: Integer).returns(String) }
104
+ def cursor_forward(n = 1)
105
+ return '' if n.zero?
106
+
107
+ control(n.to_s, 'C')
108
+ end
115
109
 
116
- # Show the cursor
117
- #
118
- def self.show_cursor
119
- control('', '?25h')
120
- end
110
+ # Move the cursor back n columns
111
+ #
112
+ # ==== Attributes
113
+ #
114
+ # * +n+ - number of columns by which to move the cursor back
115
+ #
116
+ sig { params(n: Integer).returns(String) }
117
+ def cursor_back(n = 1)
118
+ return '' if n.zero?
119
+
120
+ control(n.to_s, 'D')
121
+ end
121
122
 
122
- # Hide the cursor
123
- #
124
- def self.hide_cursor
125
- control('', '?25l')
126
- end
123
+ # Move the cursor to a specific column
124
+ #
125
+ # ==== Attributes
126
+ #
127
+ # * +n+ - The column to move to
128
+ #
129
+ sig { params(n: Integer).returns(String) }
130
+ def cursor_horizontal_absolute(n = 1)
131
+ cmd = control(n.to_s, 'G')
132
+ cmd += cursor_back if CLI::UI::OS.current.shift_cursor_back_on_horizontal_absolute?
133
+ cmd
134
+ end
127
135
 
128
- # Save the cursor position
129
- #
130
- def self.cursor_save
131
- control('', 's')
132
- end
136
+ # Show the cursor
137
+ #
138
+ sig { returns(String) }
139
+ def show_cursor
140
+ control('', '?25h')
141
+ end
133
142
 
134
- # Restore the saved cursor position
135
- #
136
- def self.cursor_restore
137
- control('', 'u')
138
- end
143
+ # Hide the cursor
144
+ #
145
+ sig { returns(String) }
146
+ def hide_cursor
147
+ control('', '?25l')
148
+ end
139
149
 
140
- # Move to the next line
141
- #
142
- def self.next_line
143
- cursor_down + cursor_horizontal_absolute
144
- end
150
+ # Save the cursor position
151
+ #
152
+ sig { returns(String) }
153
+ def cursor_save
154
+ control('', 's')
155
+ end
145
156
 
146
- # Move to the previous line
147
- #
148
- def self.previous_line
149
- cursor_up + cursor_horizontal_absolute
150
- end
157
+ # Restore the saved cursor position
158
+ #
159
+ sig { returns(String) }
160
+ def cursor_restore
161
+ control('', 'u')
162
+ end
163
+
164
+ # Move to the next line
165
+ #
166
+ sig { returns(String) }
167
+ def next_line
168
+ cursor_down + cursor_horizontal_absolute
169
+ end
170
+
171
+ # Move to the previous line
172
+ #
173
+ sig { returns(String) }
174
+ def previous_line
175
+ cursor_up + cursor_horizontal_absolute
176
+ end
151
177
 
152
- def self.clear_to_end_of_line
153
- control('', 'K')
178
+ sig { returns(String) }
179
+ def clear_to_end_of_line
180
+ control('', 'K')
181
+ end
154
182
  end
155
183
  end
156
184
  end
data/lib/cli/ui/color.rb CHANGED
@@ -1,9 +1,17 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
4
 
3
5
  module CLI
4
6
  module UI
5
7
  class Color
6
- attr_reader :sgr, :name, :code
8
+ extend T::Sig
9
+
10
+ sig { returns(String) }
11
+ attr_reader :sgr, :code
12
+
13
+ sig { returns(Symbol) }
14
+ attr_reader :name
7
15
 
8
16
  # Creates a new color mapping
9
17
  # Signatures can be found here:
@@ -14,6 +22,7 @@ module CLI
14
22
  # * +sgr+ - The color signature
15
23
  # * +name+ - The name of the color
16
24
  #
25
+ sig { params(sgr: String, name: Symbol).void }
17
26
  def initialize(sgr, name)
18
27
  @sgr = sgr
19
28
  @code = CLI::UI::ANSI.sgr(sgr)
@@ -32,7 +41,7 @@ module CLI
32
41
  WHITE = new('97', :white)
33
42
 
34
43
  # 240 is very dark gray; 255 is very light gray. 244 is somewhat dark.
35
- GRAY = new('38;5;244', :grey)
44
+ GRAY = new('38;5;244', :gray)
36
45
 
37
46
  MAP = {
38
47
  red: RED,
@@ -47,11 +56,15 @@ module CLI
47
56
  }.freeze
48
57
 
49
58
  class InvalidColorName < ArgumentError
59
+ extend T::Sig
60
+
61
+ sig { params(name: Symbol).void }
50
62
  def initialize(name)
51
63
  super
52
64
  @name = name
53
65
  end
54
66
 
67
+ sig { returns(String) }
55
68
  def message
56
69
  keys = Color.available.map(&:inspect).join(',')
57
70
  "invalid color: #{@name.inspect} " \
@@ -59,25 +72,31 @@ module CLI
59
72
  end
60
73
  end
61
74
 
62
- # Looks up a color code by name
63
- #
64
- # ==== Raises
65
- # Raises a InvalidColorName if the color is not available
66
- # You likely need to add it to the +MAP+ or you made a typo
67
- #
68
- # ==== Returns
69
- # Returns a color code
70
- #
71
- def self.lookup(name)
72
- MAP.fetch(name)
73
- rescue KeyError
74
- raise InvalidColorName, name
75
- end
75
+ class << self
76
+ extend T::Sig
76
77
 
77
- # All available colors by name
78
- #
79
- def self.available
80
- MAP.keys
78
+ # Looks up a color code by name
79
+ #
80
+ # ==== Raises
81
+ # Raises a InvalidColorName if the color is not available
82
+ # You likely need to add it to the +MAP+ or you made a typo
83
+ #
84
+ # ==== Returns
85
+ # Returns a color code
86
+ #
87
+ sig { params(name: T.any(Symbol, String)).returns(Color) }
88
+ def lookup(name)
89
+ MAP.fetch(name.to_sym)
90
+ rescue KeyError
91
+ raise InvalidColorName, name
92
+ end
93
+
94
+ # All available colors by name
95
+ #
96
+ sig { returns(T::Array[Symbol]) }
97
+ def available
98
+ MAP.keys
99
+ end
81
100
  end
82
101
  end
83
102
  end