cli-ui 1.5.1 → 2.2.3
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 +28 -17
- data/lib/cli/ui/ansi.rb +172 -129
- data/lib/cli/ui/color.rb +39 -20
- data/lib/cli/ui/formatter.rb +46 -21
- data/lib/cli/ui/frame/frame_stack.rb +30 -50
- data/lib/cli/ui/frame/frame_style/box.rb +16 -5
- data/lib/cli/ui/frame/frame_style/bracket.rb +19 -8
- data/lib/cli/ui/frame/frame_style.rb +84 -87
- data/lib/cli/ui/frame.rb +79 -32
- data/lib/cli/ui/glyph.rb +44 -31
- data/lib/cli/ui/os.rb +44 -48
- data/lib/cli/ui/printer.rb +65 -47
- data/lib/cli/ui/progress.rb +50 -33
- data/lib/cli/ui/prompt/interactive_options.rb +114 -68
- data/lib/cli/ui/prompt/options_handler.rb +8 -0
- data/lib/cli/ui/prompt.rb +168 -58
- data/lib/cli/ui/sorbet_runtime_stub.rb +169 -0
- data/lib/cli/ui/spinner/async.rb +15 -4
- data/lib/cli/ui/spinner/spin_group.rb +174 -36
- data/lib/cli/ui/spinner.rb +48 -28
- data/lib/cli/ui/stdout_router.rb +229 -47
- data/lib/cli/ui/terminal.rb +37 -25
- data/lib/cli/ui/truncater.rb +7 -2
- data/lib/cli/ui/version.rb +3 -1
- data/lib/cli/ui/widgets/base.rb +23 -4
- data/lib/cli/ui/widgets/status.rb +19 -1
- data/lib/cli/ui/widgets.rb +42 -23
- data/lib/cli/ui/wrap.rb +8 -1
- data/lib/cli/ui.rb +336 -188
- data/vendor/reentrant_mutex.rb +78 -0
- metadata +11 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 140781de33cc19ef1c5bc946a0a887b3707e54475823affad8f5568e98fbc0dd
|
4
|
+
data.tar.gz: b754caad8da6b0d37ea17d807aef78351518c4f17d511a39decbfb29b0512791
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d841fb206c3de7427c4903aa5bb350241c5fe10bbf39c04cb1980eada288deda4b235bf1d5d99a293ebaa6cd78482dd2f49849d65ddd3e7929d63d2f35894bc
|
7
|
+
data.tar.gz: 6a99dc71d996abbff3fccdda8ce9ede069a7868ba467d86296bed3575e5726f50f76b65d4d427757f5ddaea211b7926d25045402ae5304e1593ae2fb6bc656fb
|
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/
|
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/
|
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
|
|
@@ -52,6 +52,11 @@ For large numbers of options, using `e`, `:`, or `G` will toggle "line select" m
|
|
52
52
|
CLI::UI.ask('What language/framework do you use?', options: %w(rails go ruby python))
|
53
53
|
```
|
54
54
|
|
55
|
+
To set the color of instruction text:
|
56
|
+
```ruby
|
57
|
+
CLI::UI::Prompt.instructions_color = CLI::UI::Color::GRAY
|
58
|
+
```
|
59
|
+
|
55
60
|
Can also assign callbacks to each option
|
56
61
|
|
57
62
|
```ruby
|
@@ -83,10 +88,10 @@ CLI::UI.ask('Is CLI UI Awesome?', default: 'It is great!')
|
|
83
88
|
Handle many multi-threaded processes while suppressing output unless there is an issue. Can update title to show state.
|
84
89
|
|
85
90
|
```ruby
|
86
|
-
|
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
|
-
|
91
|
+
CLI::UI::SpinGroup.new do |spin_group|
|
92
|
+
spin_group.add('Title') { |spinner| sleep 3.0 }
|
93
|
+
spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
|
94
|
+
end
|
90
95
|
```
|
91
96
|
|
92
97
|

|
@@ -175,6 +180,12 @@ end
|
|
175
180
|
|
176
181
|
---
|
177
182
|
|
183
|
+
## Sorbet
|
184
|
+
|
185
|
+
We make use of [Sorbet](https://sorbet.org/) in cli-ui. We provide stubs for Sorbet so that you can use this gem even
|
186
|
+
if you aren't using Sorbet. We activate these stubs if `T` is undefined when the gem is loaded. For this reason, if you
|
187
|
+
would like to use this gem and your project _does_ use Sorbet, ensure you load Sorbet _before_ loading cli-ui.
|
188
|
+
|
178
189
|
## Example Usage
|
179
190
|
|
180
191
|
The following code makes use of nested-framing, multi-threaded spinners, formatted text, and more.
|
@@ -187,22 +198,22 @@ CLI::UI::StdoutRouter.enable
|
|
187
198
|
CLI::UI::Frame.open('{{*}} {{bold:a}}', color: :green) do
|
188
199
|
CLI::UI::Frame.open('{{i}} b', color: :magenta) do
|
189
200
|
CLI::UI::Frame.open('{{?}} c', color: :cyan) do
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
201
|
+
CLI::UI::SpinGroup.new do |sg|
|
202
|
+
sg.add('wow') do |spinner|
|
203
|
+
sleep(2.5)
|
204
|
+
spinner.update_title('second round!')
|
205
|
+
sleep (1.0)
|
206
|
+
end
|
207
|
+
sg.add('such spin') { sleep(1.6) }
|
208
|
+
sg.add('many glyph') { sleep(2.0) }
|
195
209
|
end
|
196
|
-
sg.add('such spin') { sleep(1.6) }
|
197
|
-
sg.add('many glyph') { sleep(2.0) }
|
198
|
-
sg.wait
|
199
210
|
end
|
200
211
|
end
|
201
212
|
CLI::UI::Frame.divider('{{v}} lol')
|
202
213
|
puts CLI::UI.fmt '{{info:words}} {{red:oh no!}} {{green:success!}}'
|
203
|
-
|
204
|
-
|
205
|
-
|
214
|
+
CLI::UI::SpinGroup.new do |sg|
|
215
|
+
sg.add('more spins') { sleep(0.5) ; raise 'oh no' }
|
216
|
+
end
|
206
217
|
end
|
207
218
|
```
|
208
219
|
|
data/lib/cli/ui/ansi.rb
CHANGED
@@ -1,156 +1,199 @@
|
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
str
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
end
|
136
|
+
sig { returns(String) }
|
137
|
+
def enter_alternate_screen
|
138
|
+
control('?1049', 'h')
|
139
|
+
end
|
133
140
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
end
|
141
|
+
sig { returns(String) }
|
142
|
+
def exit_alternate_screen
|
143
|
+
control('?1049', 'l')
|
144
|
+
end
|
139
145
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
end
|
146
|
+
sig { returns(Regexp) }
|
147
|
+
def match_alternate_screen
|
148
|
+
/#{Regexp.escape(control('?1049', ''))}[hl]/
|
149
|
+
end
|
145
150
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
+
# Show the cursor
|
152
|
+
#
|
153
|
+
sig { returns(String) }
|
154
|
+
def show_cursor
|
155
|
+
control('', '?25h')
|
156
|
+
end
|
157
|
+
|
158
|
+
# Hide the cursor
|
159
|
+
#
|
160
|
+
sig { returns(String) }
|
161
|
+
def hide_cursor
|
162
|
+
control('', '?25l')
|
163
|
+
end
|
164
|
+
|
165
|
+
# Save the cursor position
|
166
|
+
#
|
167
|
+
sig { returns(String) }
|
168
|
+
def cursor_save
|
169
|
+
control('', 's')
|
170
|
+
end
|
171
|
+
|
172
|
+
# Restore the saved cursor position
|
173
|
+
#
|
174
|
+
sig { returns(String) }
|
175
|
+
def cursor_restore
|
176
|
+
control('', 'u')
|
177
|
+
end
|
151
178
|
|
152
|
-
|
153
|
-
|
179
|
+
# Move to the next line
|
180
|
+
#
|
181
|
+
sig { returns(String) }
|
182
|
+
def next_line
|
183
|
+
cursor_down + cursor_horizontal_absolute
|
184
|
+
end
|
185
|
+
|
186
|
+
# Move to the previous line
|
187
|
+
#
|
188
|
+
sig { returns(String) }
|
189
|
+
def previous_line
|
190
|
+
cursor_up + cursor_horizontal_absolute
|
191
|
+
end
|
192
|
+
|
193
|
+
sig { returns(String) }
|
194
|
+
def clear_to_end_of_line
|
195
|
+
control('', 'K')
|
196
|
+
end
|
154
197
|
end
|
155
198
|
end
|
156
199
|
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
|
-
|
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', :
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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.to_sym
|
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
|