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 +4 -4
- data/README.md +23 -17
- data/lib/cli/ui/ansi.rb +157 -129
- data/lib/cli/ui/color.rb +39 -20
- data/lib/cli/ui/formatter.rb +45 -21
- data/lib/cli/ui/frame/frame_stack.rb +32 -13
- data/lib/cli/ui/frame/frame_style/box.rb +15 -4
- data/lib/cli/ui/frame/frame_style/bracket.rb +18 -7
- data/lib/cli/ui/frame/frame_style.rb +84 -87
- data/lib/cli/ui/frame.rb +55 -24
- 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 +49 -32
- data/lib/cli/ui/prompt/interactive_options.rb +91 -44
- data/lib/cli/ui/prompt/options_handler.rb +8 -0
- data/lib/cli/ui/prompt.rb +84 -31
- data/lib/cli/ui/sorbet_runtime_stub.rb +157 -0
- data/lib/cli/ui/spinner/async.rb +15 -4
- data/lib/cli/ui/spinner/spin_group.rb +83 -15
- data/lib/cli/ui/spinner.rb +48 -28
- data/lib/cli/ui/stdout_router.rb +71 -34
- 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 +325 -188
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb23f8ab0044a484b7f6f8dcb57240d5b5ba9a3db6caedb6090186c3d7a55143
|
4
|
+
data.tar.gz: 9c3e0d024f05ed428a0afa190fbc440008e58d8f58cf244c452259722883dd50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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
|
|
@@ -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
|
-
|
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
|
-
|
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
|

|
@@ -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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
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
|
-
|
136
|
+
# Show the cursor
|
137
|
+
#
|
138
|
+
sig { returns(String) }
|
139
|
+
def show_cursor
|
140
|
+
control('', '?25h')
|
141
|
+
end
|
133
142
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
143
|
+
# Hide the cursor
|
144
|
+
#
|
145
|
+
sig { returns(String) }
|
146
|
+
def hide_cursor
|
147
|
+
control('', '?25l')
|
148
|
+
end
|
139
149
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
150
|
+
# Save the cursor position
|
151
|
+
#
|
152
|
+
sig { returns(String) }
|
153
|
+
def cursor_save
|
154
|
+
control('', 's')
|
155
|
+
end
|
145
156
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
153
|
-
|
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
|
-
|
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
|
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
|