cli-ui 1.5.0 → 2.0.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 +17 -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 +67 -11
- 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 -21
- data/.dependabot/config.yml +0 -8
- data/.github/CODEOWNERS +0 -1
- data/.github/probots.yml +0 -2
- data/.gitignore +0 -14
- data/.rubocop.yml +0 -41
- data/.travis.yml +0 -7
- data/Gemfile +0 -17
- data/Gemfile.lock +0 -60
- data/Rakefile +0 -20
- data/bin/console +0 -14
- data/cli-ui.gemspec +0 -27
- data/dev.yml +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8dec5198cdbbb3a6060fc448a9a03b5bc6eca461b94a62ead0f128664bb0ee2
|
4
|
+
data.tar.gz: bed02e7e27771141c08859c11d7e01f690438bb46d4022f5225d8cb42e39efe4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6944f7c1b8492e69bd078b2ca0d9901d26694e4542976cb07729efff2cb59438cbc2ec61692b75ccf178ffcec896a297264be5734520695f9bc11779f2698ead
|
7
|
+
data.tar.gz: c9795b1749d3db7cb57bccca815495ed9b2e8ee02092fe95b46bf451f2993e65c33f8b48e9f6520b2984b7278db64cb9e92bb1fe58a761691a9b313dab6ef464
|
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
|

|
@@ -187,22 +187,22 @@ CLI::UI::StdoutRouter.enable
|
|
187
187
|
CLI::UI::Frame.open('{{*}} {{bold:a}}', color: :green) do
|
188
188
|
CLI::UI::Frame.open('{{i}} b', color: :magenta) do
|
189
189
|
CLI::UI::Frame.open('{{?}} c', color: :cyan) do
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
190
|
+
CLI::UI::SpinGroup.new do |sg|
|
191
|
+
sg.add('wow') do |spinner|
|
192
|
+
sleep(2.5)
|
193
|
+
spinner.update_title('second round!')
|
194
|
+
sleep (1.0)
|
195
|
+
end
|
196
|
+
sg.add('such spin') { sleep(1.6) }
|
197
|
+
sg.add('many glyph') { sleep(2.0) }
|
195
198
|
end
|
196
|
-
sg.add('such spin') { sleep(1.6) }
|
197
|
-
sg.add('many glyph') { sleep(2.0) }
|
198
|
-
sg.wait
|
199
199
|
end
|
200
200
|
end
|
201
201
|
CLI::UI::Frame.divider('{{v}} lol')
|
202
202
|
puts CLI::UI.fmt '{{info:words}} {{red:oh no!}} {{green:success!}}'
|
203
|
-
|
204
|
-
|
205
|
-
|
203
|
+
CLI::UI::SpinGroup.new do |sg|
|
204
|
+
sg.add('more spins') { sleep(0.5) ; raise 'oh no' }
|
205
|
+
end
|
206
206
|
end
|
207
207
|
```
|
208
208
|
|
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
|
data/lib/cli/ui/formatter.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
|
+
# typed: true
|
1
2
|
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require('cli/ui')
|
3
5
|
require('strscan')
|
4
6
|
|
5
7
|
module CLI
|
6
8
|
module UI
|
7
9
|
class Formatter
|
10
|
+
extend T::Sig
|
11
|
+
|
8
12
|
# Available mappings of formattings
|
9
13
|
# To use any of them, you can use {{<key>:<string>}}
|
10
14
|
# There are presentational (colours and formatters)
|
@@ -19,6 +23,8 @@ module CLI
|
|
19
23
|
'blue' => '94', # 9x = high-intensity fg color x
|
20
24
|
'magenta' => '35',
|
21
25
|
'cyan' => '36',
|
26
|
+
'gray' => '38;5;244',
|
27
|
+
'white' => '97',
|
22
28
|
'bold' => '1',
|
23
29
|
'italic' => '3',
|
24
30
|
'underline' => '4',
|
@@ -49,12 +55,21 @@ module CLI
|
|
49
55
|
|
50
56
|
DISCARD_BRACES = 0..-3
|
51
57
|
|
52
|
-
LITERAL_BRACES =
|
58
|
+
LITERAL_BRACES = Class.new
|
59
|
+
|
60
|
+
Stack = T.type_alias { T::Array[T.any(String, LITERAL_BRACES)] }
|
53
61
|
|
54
62
|
class FormatError < StandardError
|
55
|
-
|
63
|
+
extend T::Sig
|
64
|
+
|
65
|
+
sig { returns(String) }
|
66
|
+
attr_accessor :input
|
56
67
|
|
57
|
-
|
68
|
+
sig { returns(Integer) }
|
69
|
+
attr_accessor :index
|
70
|
+
|
71
|
+
sig { params(message: String, input: String, index: Integer).void }
|
72
|
+
def initialize(message, input, index)
|
58
73
|
super(message)
|
59
74
|
@input = input
|
60
75
|
@index = index
|
@@ -67,8 +82,10 @@ module CLI
|
|
67
82
|
#
|
68
83
|
# * +text+ - the text to format
|
69
84
|
#
|
85
|
+
sig { params(text: String).void }
|
70
86
|
def initialize(text)
|
71
87
|
@text = text
|
88
|
+
@nodes = T.let([], T::Array[[String, Stack]])
|
72
89
|
end
|
73
90
|
|
74
91
|
# Format the text using a map.
|
@@ -81,10 +98,11 @@ module CLI
|
|
81
98
|
#
|
82
99
|
# * +:enable_color+ - enable color output? Default is true unless output is redirected
|
83
100
|
#
|
101
|
+
sig { params(sgr_map: T::Hash[String, String], enable_color: T::Boolean).returns(String) }
|
84
102
|
def format(sgr_map = SGR_MAP, enable_color: CLI::UI.enable_color?)
|
85
|
-
@nodes
|
103
|
+
@nodes.replace([])
|
86
104
|
stack = parse_body(StringScanner.new(@text))
|
87
|
-
prev_fmt = nil
|
105
|
+
prev_fmt = T.let(nil, T.nilable(Stack))
|
88
106
|
content = @nodes.each_with_object(+'') do |(text, fmt), str|
|
89
107
|
if prev_fmt != fmt && enable_color
|
90
108
|
text = apply_format(text, fmt, sgr_map)
|
@@ -93,12 +111,12 @@ module CLI
|
|
93
111
|
prev_fmt = fmt
|
94
112
|
end
|
95
113
|
|
96
|
-
stack.reject! { |e| e
|
114
|
+
stack.reject! { |e| e.is_a?(LITERAL_BRACES) }
|
97
115
|
|
98
116
|
return content unless enable_color
|
99
117
|
return content if stack == prev_fmt
|
100
118
|
|
101
|
-
unless stack.empty? && (@nodes.size.zero? || @nodes.last[1].empty?)
|
119
|
+
unless stack.empty? && (@nodes.size.zero? || T.must(@nodes.last)[1].empty?)
|
102
120
|
content << apply_format('', stack, sgr_map)
|
103
121
|
end
|
104
122
|
content
|
@@ -106,25 +124,28 @@ module CLI
|
|
106
124
|
|
107
125
|
private
|
108
126
|
|
127
|
+
sig { params(text: String, fmt: Stack, sgr_map: T::Hash[String, String]).returns(String) }
|
109
128
|
def apply_format(text, fmt, sgr_map)
|
110
129
|
sgr = fmt.each_with_object(+'0') do |name, str|
|
111
|
-
next if name
|
130
|
+
next if name.is_a?(LITERAL_BRACES)
|
131
|
+
|
112
132
|
begin
|
113
133
|
str << ';' << sgr_map.fetch(name)
|
114
134
|
rescue KeyError
|
115
135
|
raise FormatError.new(
|
116
136
|
"invalid format specifier: #{name}",
|
117
137
|
@text,
|
118
|
-
-1
|
138
|
+
-1,
|
119
139
|
)
|
120
140
|
end
|
121
141
|
end
|
122
142
|
CLI::UI::ANSI.sgr(sgr) + text
|
123
143
|
end
|
124
144
|
|
145
|
+
sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
|
125
146
|
def parse_expr(sc, stack)
|
126
147
|
if (match = sc.scan(SCAN_GLYPH))
|
127
|
-
glyph_handle = match[0]
|
148
|
+
glyph_handle = T.must(match[0])
|
128
149
|
begin
|
129
150
|
glyph = Glyph.lookup(glyph_handle)
|
130
151
|
emit(glyph.char, [glyph.color.name.to_s])
|
@@ -133,20 +154,20 @@ module CLI
|
|
133
154
|
raise FormatError.new(
|
134
155
|
"invalid glyph handle at index #{index}: '#{glyph_handle}'",
|
135
156
|
@text,
|
136
|
-
index
|
157
|
+
index,
|
137
158
|
)
|
138
159
|
end
|
139
160
|
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']
|
161
|
+
match_data = T.must(SCAN_WIDGET.match(match)) # Regexp.last_match doesn't work here
|
162
|
+
widget_handle = T.must(match_data['handle'])
|
142
163
|
begin
|
143
164
|
widget = Widgets.lookup(widget_handle)
|
144
|
-
emit(widget.call(match_data['args']), stack)
|
165
|
+
emit(widget.call(T.must(match_data['args'])), stack)
|
145
166
|
rescue Widgets::InvalidWidgetHandle
|
146
167
|
index = sc.pos - 2 # rewind past '}}'
|
147
168
|
raise(FormatError.new(
|
148
169
|
"invalid widget handle at index #{index}: '#{widget_handle}'",
|
149
|
-
@text, index
|
170
|
+
@text, index
|
150
171
|
))
|
151
172
|
end
|
152
173
|
elsif (match = sc.scan(SCAN_FUNCNAME))
|
@@ -158,20 +179,21 @@ module CLI
|
|
158
179
|
# We do kind of assume that the text will probably have balanced
|
159
180
|
# pairs of {{ }} at least.
|
160
181
|
emit('{{', stack)
|
161
|
-
stack.push(LITERAL_BRACES)
|
182
|
+
stack.push(LITERAL_BRACES.new)
|
162
183
|
end
|
163
184
|
parse_body(sc, stack)
|
164
185
|
stack
|
165
186
|
end
|
166
187
|
|
188
|
+
sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
|
167
189
|
def parse_body(sc, stack = [])
|
168
190
|
match = sc.scan(SCAN_BODY)
|
169
191
|
if match&.end_with?(BEGIN_EXPR)
|
170
|
-
emit(match[DISCARD_BRACES], stack)
|
192
|
+
emit(T.must(match[DISCARD_BRACES]), stack)
|
171
193
|
parse_expr(sc, stack)
|
172
194
|
elsif match&.end_with?(END_EXPR)
|
173
|
-
emit(match[DISCARD_BRACES], stack)
|
174
|
-
if stack.pop
|
195
|
+
emit(T.must(match[DISCARD_BRACES]), stack)
|
196
|
+
if stack.pop.is_a?(LITERAL_BRACES)
|
175
197
|
emit('}}', stack)
|
176
198
|
end
|
177
199
|
parse_body(sc, stack)
|
@@ -183,9 +205,11 @@ module CLI
|
|
183
205
|
stack
|
184
206
|
end
|
185
207
|
|
208
|
+
sig { params(text: String, stack: Stack).void }
|
186
209
|
def emit(text, stack)
|
187
|
-
return if text.
|
188
|
-
|
210
|
+
return if text.empty?
|
211
|
+
|
212
|
+
@nodes << [text, stack.reject { |n| n.is_a?(LITERAL_BRACES) }]
|
189
213
|
end
|
190
214
|
end
|
191
215
|
end
|