gorails 0.1.0 → 0.1.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/CHANGELOG.md +14 -1
- data/Gemfile +3 -1
- data/Gemfile.lock +65 -0
- data/README.md +41 -12
- data/bin/update-deps +95 -0
- data/exe/gorails +18 -0
- data/gorails.gemspec +4 -3
- data/lib/gorails/commands/episodes.rb +25 -0
- data/lib/gorails/commands/example.rb +19 -0
- data/lib/gorails/commands/help.rb +21 -0
- data/lib/gorails/commands/jobs.rb +25 -0
- data/lib/gorails/commands/jumpstart.rb +29 -0
- data/lib/gorails/commands/railsbytes.rb +67 -0
- data/lib/gorails/commands.rb +19 -0
- data/lib/gorails/entry_point.rb +10 -0
- data/lib/gorails/version.rb +1 -1
- data/lib/gorails.rb +22 -1
- data/vendor/deps/cli-kit/REVISION +1 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
- data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
- data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
- data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
- data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
- data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
- data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
- data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
- data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
- data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
- data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
- data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
- data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
- data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
- data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
- data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
- data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
- data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
- data/vendor/deps/cli-ui/REVISION +1 -0
- data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
- data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
- data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
- data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
- data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
- data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
- data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
- data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
- data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
- data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
- data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
- data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
- data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
- data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
- metadata +114 -5
@@ -0,0 +1,216 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require('cli/ui')
|
5
|
+
require('strscan')
|
6
|
+
|
7
|
+
module CLI
|
8
|
+
module UI
|
9
|
+
class Formatter
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
# Available mappings of formattings
|
13
|
+
# To use any of them, you can use {{<key>:<string>}}
|
14
|
+
# There are presentational (colours and formatters)
|
15
|
+
# and semantic (error, info, command) formatters available
|
16
|
+
#
|
17
|
+
SGR_MAP = {
|
18
|
+
# presentational
|
19
|
+
'red' => '31',
|
20
|
+
'green' => '32',
|
21
|
+
'yellow' => '33',
|
22
|
+
# default blue is low-contrast against black in some default terminal color scheme
|
23
|
+
'blue' => '94', # 9x = high-intensity fg color x
|
24
|
+
'magenta' => '35',
|
25
|
+
'cyan' => '36',
|
26
|
+
'gray' => '38;5;244',
|
27
|
+
'white' => '97',
|
28
|
+
'bold' => '1',
|
29
|
+
'italic' => '3',
|
30
|
+
'underline' => '4',
|
31
|
+
'reset' => '0',
|
32
|
+
|
33
|
+
# semantic
|
34
|
+
'error' => '31', # red
|
35
|
+
'success' => '32', # success
|
36
|
+
'warning' => '33', # yellow
|
37
|
+
'info' => '94', # bright blue
|
38
|
+
'command' => '36', # cyan
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
BEGIN_EXPR = '{{'
|
42
|
+
END_EXPR = '}}'
|
43
|
+
|
44
|
+
SCAN_WIDGET = %r[@widget/(?<handle>\w+):(?<args>.*?)}}]
|
45
|
+
SCAN_FUNCNAME = /\w+:/
|
46
|
+
SCAN_GLYPH = /.}}/
|
47
|
+
SCAN_BODY = %r{
|
48
|
+
.*?
|
49
|
+
(
|
50
|
+
#{BEGIN_EXPR} |
|
51
|
+
#{END_EXPR} |
|
52
|
+
\z
|
53
|
+
)
|
54
|
+
}mx
|
55
|
+
|
56
|
+
DISCARD_BRACES = 0..-3
|
57
|
+
|
58
|
+
LITERAL_BRACES = Class.new
|
59
|
+
|
60
|
+
Stack = T.type_alias { T::Array[T.any(String, LITERAL_BRACES)] }
|
61
|
+
|
62
|
+
class FormatError < StandardError
|
63
|
+
extend T::Sig
|
64
|
+
|
65
|
+
sig { returns(String) }
|
66
|
+
attr_accessor :input
|
67
|
+
|
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)
|
73
|
+
super(message)
|
74
|
+
@input = input
|
75
|
+
@index = index
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Initialize a formatter with text.
|
80
|
+
#
|
81
|
+
# ===== Attributes
|
82
|
+
#
|
83
|
+
# * +text+ - the text to format
|
84
|
+
#
|
85
|
+
sig { params(text: String).void }
|
86
|
+
def initialize(text)
|
87
|
+
@text = text
|
88
|
+
@nodes = T.let([], T::Array[[String, Stack]])
|
89
|
+
end
|
90
|
+
|
91
|
+
# Format the text using a map.
|
92
|
+
#
|
93
|
+
# ===== Attributes
|
94
|
+
#
|
95
|
+
# * +sgr_map+ - the mapping of the formattings. Defaults to +SGR_MAP+
|
96
|
+
#
|
97
|
+
# ===== Options
|
98
|
+
#
|
99
|
+
# * +:enable_color+ - enable color output? Default is true unless output is redirected
|
100
|
+
#
|
101
|
+
sig { params(sgr_map: T::Hash[String, String], enable_color: T::Boolean).returns(String) }
|
102
|
+
def format(sgr_map = SGR_MAP, enable_color: CLI::UI.enable_color?)
|
103
|
+
@nodes.replace([])
|
104
|
+
stack = parse_body(StringScanner.new(@text))
|
105
|
+
prev_fmt = T.let(nil, T.nilable(Stack))
|
106
|
+
content = @nodes.each_with_object(+'') do |(text, fmt), str|
|
107
|
+
if prev_fmt != fmt && enable_color
|
108
|
+
text = apply_format(text, fmt, sgr_map)
|
109
|
+
end
|
110
|
+
str << text
|
111
|
+
prev_fmt = fmt
|
112
|
+
end
|
113
|
+
|
114
|
+
stack.reject! { |e| e.is_a?(LITERAL_BRACES) }
|
115
|
+
|
116
|
+
return content unless enable_color
|
117
|
+
return content if stack == prev_fmt
|
118
|
+
|
119
|
+
unless stack.empty? && (@nodes.size.zero? || T.must(@nodes.last)[1].empty?)
|
120
|
+
content << apply_format('', stack, sgr_map)
|
121
|
+
end
|
122
|
+
content
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
sig { params(text: String, fmt: Stack, sgr_map: T::Hash[String, String]).returns(String) }
|
128
|
+
def apply_format(text, fmt, sgr_map)
|
129
|
+
sgr = fmt.each_with_object(+'0') do |name, str|
|
130
|
+
next if name.is_a?(LITERAL_BRACES)
|
131
|
+
|
132
|
+
begin
|
133
|
+
str << ';' << sgr_map.fetch(name)
|
134
|
+
rescue KeyError
|
135
|
+
raise FormatError.new(
|
136
|
+
"invalid format specifier: #{name}",
|
137
|
+
@text,
|
138
|
+
-1
|
139
|
+
)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
CLI::UI::ANSI.sgr(sgr) + text
|
143
|
+
end
|
144
|
+
|
145
|
+
sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
|
146
|
+
def parse_expr(sc, stack)
|
147
|
+
if (match = sc.scan(SCAN_GLYPH))
|
148
|
+
glyph_handle = T.must(match[0])
|
149
|
+
begin
|
150
|
+
glyph = Glyph.lookup(glyph_handle)
|
151
|
+
emit(glyph.char, [glyph.color.name.to_s])
|
152
|
+
rescue Glyph::InvalidGlyphHandle
|
153
|
+
index = sc.pos - 2 # rewind past '}}'
|
154
|
+
raise FormatError.new(
|
155
|
+
"invalid glyph handle at index #{index}: '#{glyph_handle}'",
|
156
|
+
@text,
|
157
|
+
index
|
158
|
+
)
|
159
|
+
end
|
160
|
+
elsif (match = sc.scan(SCAN_WIDGET))
|
161
|
+
match_data = T.must(SCAN_WIDGET.match(match)) # Regexp.last_match doesn't work here
|
162
|
+
widget_handle = T.must(match_data['handle'])
|
163
|
+
begin
|
164
|
+
widget = Widgets.lookup(widget_handle)
|
165
|
+
emit(widget.call(T.must(match_data['args'])), stack)
|
166
|
+
rescue Widgets::InvalidWidgetHandle
|
167
|
+
index = sc.pos - 2 # rewind past '}}'
|
168
|
+
raise(FormatError.new(
|
169
|
+
"invalid widget handle at index #{index}: '#{widget_handle}'",
|
170
|
+
@text, index,
|
171
|
+
))
|
172
|
+
end
|
173
|
+
elsif (match = sc.scan(SCAN_FUNCNAME))
|
174
|
+
funcname = match.chop
|
175
|
+
stack.push(funcname)
|
176
|
+
else
|
177
|
+
# We read a {{ but it's not apparently Formatter syntax.
|
178
|
+
# We could error, but it's nicer to just pass through as text.
|
179
|
+
# We do kind of assume that the text will probably have balanced
|
180
|
+
# pairs of {{ }} at least.
|
181
|
+
emit('{{', stack)
|
182
|
+
stack.push(LITERAL_BRACES.new)
|
183
|
+
end
|
184
|
+
parse_body(sc, stack)
|
185
|
+
stack
|
186
|
+
end
|
187
|
+
|
188
|
+
sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
|
189
|
+
def parse_body(sc, stack = [])
|
190
|
+
match = sc.scan(SCAN_BODY)
|
191
|
+
if match&.end_with?(BEGIN_EXPR)
|
192
|
+
emit(T.must(match[DISCARD_BRACES]), stack)
|
193
|
+
parse_expr(sc, stack)
|
194
|
+
elsif match&.end_with?(END_EXPR)
|
195
|
+
emit(T.must(match[DISCARD_BRACES]), stack)
|
196
|
+
if stack.pop.is_a?(LITERAL_BRACES)
|
197
|
+
emit('}}', stack)
|
198
|
+
end
|
199
|
+
parse_body(sc, stack)
|
200
|
+
elsif match
|
201
|
+
emit(match, stack)
|
202
|
+
else
|
203
|
+
emit(sc.rest, stack)
|
204
|
+
end
|
205
|
+
stack
|
206
|
+
end
|
207
|
+
|
208
|
+
sig { params(text: String, stack: Stack).void }
|
209
|
+
def emit(text, stack)
|
210
|
+
return if text.empty?
|
211
|
+
|
212
|
+
@nodes << [text, stack.reject { |n| n.is_a?(LITERAL_BRACES) }]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# typed: true
|
2
|
+
module CLI
|
3
|
+
module UI
|
4
|
+
module Frame
|
5
|
+
module FrameStack
|
6
|
+
COLOR_ENVVAR = 'CLI_FRAME_STACK'
|
7
|
+
STYLE_ENVVAR = 'CLI_STYLE_STACK'
|
8
|
+
|
9
|
+
class StackItem
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(CLI::UI::Color) }
|
13
|
+
attr_reader :color
|
14
|
+
|
15
|
+
sig { returns(CLI::UI::Frame::FrameStyle) }
|
16
|
+
attr_reader :frame_style
|
17
|
+
|
18
|
+
sig do
|
19
|
+
params(color_name: CLI::UI::Colorable, style_name: FrameStylable)
|
20
|
+
.void
|
21
|
+
end
|
22
|
+
def initialize(color_name, style_name)
|
23
|
+
@color = CLI::UI.resolve_color(color_name)
|
24
|
+
@frame_style = CLI::UI.resolve_style(style_name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
extend T::Sig
|
30
|
+
|
31
|
+
# Fetch all items off the frame stack
|
32
|
+
sig { returns(T::Array[StackItem]) }
|
33
|
+
def items
|
34
|
+
colors = ENV.fetch(COLOR_ENVVAR, '').split(':').map(&:to_sym)
|
35
|
+
styles = ENV.fetch(STYLE_ENVVAR, '').split(':').map(&:to_sym)
|
36
|
+
|
37
|
+
colors.each_with_index.map do |color, i|
|
38
|
+
StackItem.new(color, styles[i] || Frame.frame_style)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Push a new item onto the frame stack.
|
43
|
+
#
|
44
|
+
# Either an item or a :color/:style pair should be pushed onto the stack.
|
45
|
+
#
|
46
|
+
# ==== Attributes
|
47
|
+
#
|
48
|
+
# * +item+ a +StackItem+ to push onto the stack. Defaults to nil
|
49
|
+
#
|
50
|
+
# ==== Options
|
51
|
+
#
|
52
|
+
# * +:color+ the color of the new stack item. Defaults to nil
|
53
|
+
# * +:style+ the style of the new stack item. Defaults to nil
|
54
|
+
#
|
55
|
+
# ==== Raises
|
56
|
+
#
|
57
|
+
# If both an item and a color/style pair are given, raises an +ArgumentError+
|
58
|
+
# If the given item is not a +StackItem+, raises an +ArgumentError+
|
59
|
+
#
|
60
|
+
sig do
|
61
|
+
params(
|
62
|
+
item: T.nilable(StackItem),
|
63
|
+
color: T.nilable(CLI::UI::Color),
|
64
|
+
style: T.nilable(CLI::UI::Frame::FrameStyle)
|
65
|
+
)
|
66
|
+
.void
|
67
|
+
end
|
68
|
+
def push(item = nil, color: nil, style: nil)
|
69
|
+
if color.nil? != style.nil? || item.nil? == color.nil?
|
70
|
+
raise ArgumentError, 'Must give one of item or color: and style:'
|
71
|
+
end
|
72
|
+
|
73
|
+
item ||= StackItem.new(T.must(color), T.must(style))
|
74
|
+
|
75
|
+
curr = items
|
76
|
+
curr << item
|
77
|
+
|
78
|
+
serialize(curr)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Removes and returns the last stack item off the stack
|
82
|
+
sig { returns(T.nilable(StackItem)) }
|
83
|
+
def pop
|
84
|
+
curr = items
|
85
|
+
ret = curr.pop
|
86
|
+
|
87
|
+
serialize(curr)
|
88
|
+
|
89
|
+
ret.nil? ? nil : ret
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Serializes the item stack into two ENV variables.
|
95
|
+
#
|
96
|
+
# This is done to preserve backward compatibility with earlier versions of cli/ui.
|
97
|
+
# This ensures that any code that relied upon previous stack behavior should continue
|
98
|
+
# to work.
|
99
|
+
sig { params(items: T::Array[StackItem]).void }
|
100
|
+
def serialize(items)
|
101
|
+
colors = []
|
102
|
+
styles = []
|
103
|
+
|
104
|
+
items.each do |item|
|
105
|
+
colors << item.color.name
|
106
|
+
styles << item.frame_style.style_name
|
107
|
+
end
|
108
|
+
|
109
|
+
ENV[COLOR_ENVVAR] = colors.join(':')
|
110
|
+
ENV[STYLE_ENVVAR] = styles.join(':')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# typed: true
|
2
|
+
module CLI
|
3
|
+
module UI
|
4
|
+
module Frame
|
5
|
+
module FrameStyle
|
6
|
+
module Box
|
7
|
+
extend FrameStyle
|
8
|
+
|
9
|
+
VERTICAL = '┃'
|
10
|
+
HORIZONTAL = '━'
|
11
|
+
DIVIDER = '┣'
|
12
|
+
TOP_LEFT = '┏'
|
13
|
+
BOTTOM_LEFT = '┗'
|
14
|
+
|
15
|
+
class << self
|
16
|
+
extend T::Sig
|
17
|
+
|
18
|
+
sig { override.returns(Symbol) }
|
19
|
+
def style_name
|
20
|
+
:box
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { override.returns(String) }
|
24
|
+
def prefix
|
25
|
+
VERTICAL
|
26
|
+
end
|
27
|
+
|
28
|
+
# Draws the "Open" line for this frame style
|
29
|
+
#
|
30
|
+
# ==== Attributes
|
31
|
+
#
|
32
|
+
# * +text+ - (required) the text/title to output in the frame
|
33
|
+
#
|
34
|
+
# ==== Options
|
35
|
+
#
|
36
|
+
# * +:color+ - (required) The color of the frame.
|
37
|
+
#
|
38
|
+
# ==== Output:
|
39
|
+
#
|
40
|
+
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
41
|
+
#
|
42
|
+
sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
|
43
|
+
def start(text, color:)
|
44
|
+
edge(text, color: color, first: TOP_LEFT)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Draws a "divider" line for the current frame style
|
48
|
+
#
|
49
|
+
# ==== Attributes
|
50
|
+
#
|
51
|
+
# * +text+ - (required) the text/title to output in the frame
|
52
|
+
#
|
53
|
+
# ==== Options
|
54
|
+
#
|
55
|
+
# * +:color+ - (required) The color of the frame.
|
56
|
+
#
|
57
|
+
# ==== Output:
|
58
|
+
#
|
59
|
+
# ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
60
|
+
#
|
61
|
+
sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
|
62
|
+
def divider(text, color:)
|
63
|
+
edge(text, color: color, first: DIVIDER)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Draws the "Close" line for this frame style
|
67
|
+
#
|
68
|
+
# ==== Attributes
|
69
|
+
#
|
70
|
+
# * +text+ - (required) the text/title to output in the frame
|
71
|
+
#
|
72
|
+
# ==== Options
|
73
|
+
#
|
74
|
+
# * +:color+ - (required) The color of the frame.
|
75
|
+
# * +:right_text+ - Text to print at the right of the line. Defaults to nil
|
76
|
+
#
|
77
|
+
# ==== Output:
|
78
|
+
#
|
79
|
+
# ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
80
|
+
#
|
81
|
+
sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) }
|
82
|
+
def close(text, color:, right_text: nil)
|
83
|
+
edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
sig do
|
89
|
+
params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String)
|
90
|
+
end
|
91
|
+
def edge(text, color:, first:, right_text: nil)
|
92
|
+
color = CLI::UI.resolve_color(color)
|
93
|
+
|
94
|
+
preamble = +''
|
95
|
+
|
96
|
+
preamble << color.code << first << (HORIZONTAL * 2)
|
97
|
+
|
98
|
+
unless text.empty?
|
99
|
+
preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
|
100
|
+
end
|
101
|
+
|
102
|
+
termwidth = CLI::UI::Terminal.width
|
103
|
+
|
104
|
+
suffix = +''
|
105
|
+
|
106
|
+
if right_text
|
107
|
+
suffix << ' ' << right_text << ' '
|
108
|
+
end
|
109
|
+
|
110
|
+
preamble_width = CLI::UI::ANSI.printing_width(preamble)
|
111
|
+
preamble_start = Frame.prefix_width
|
112
|
+
# If prefix_width is non-zero, we need to subtract the width of
|
113
|
+
# the final space, since we're going to write over it.
|
114
|
+
preamble_start -= 1 unless preamble_start.zero?
|
115
|
+
preamble_end = preamble_start + preamble_width
|
116
|
+
|
117
|
+
suffix_width = CLI::UI::ANSI.printing_width(suffix)
|
118
|
+
suffix_end = termwidth - 2
|
119
|
+
suffix_start = suffix_end - suffix_width
|
120
|
+
|
121
|
+
if preamble_end > suffix_start
|
122
|
+
suffix = ''
|
123
|
+
# if preamble_end > termwidth
|
124
|
+
# we *could* truncate it, but let's just let it overflow to the
|
125
|
+
# next line and call it poor usage of this API.
|
126
|
+
end
|
127
|
+
|
128
|
+
o = +''
|
129
|
+
|
130
|
+
# Shopify's CI system supports terminal emulation, but not some of
|
131
|
+
# the fancier features that we normally use to draw frames
|
132
|
+
# extra-reliably, so we fall back to a less foolproof strategy. This
|
133
|
+
# is probably better in general for cases with impoverished terminal
|
134
|
+
# emulators and no active user.
|
135
|
+
unless [0, '', nil].include?(ENV['CI'])
|
136
|
+
linewidth = [0, termwidth - (preamble_end + suffix_width + 1)].max
|
137
|
+
|
138
|
+
o << color.code << preamble
|
139
|
+
o << color.code << (HORIZONTAL * linewidth)
|
140
|
+
o << color.code << suffix
|
141
|
+
o << CLI::UI::Color::RESET.code << "\n"
|
142
|
+
return o
|
143
|
+
end
|
144
|
+
|
145
|
+
# Jumping around the line can cause some unwanted flashes
|
146
|
+
o << CLI::UI::ANSI.hide_cursor
|
147
|
+
|
148
|
+
# reset to column 1 so that things like ^C don't ruin formatting
|
149
|
+
o << "\r"
|
150
|
+
|
151
|
+
# This code will print out a full line with the given preamble and
|
152
|
+
# suffix, as exemplified below.
|
153
|
+
#
|
154
|
+
# preamble_start suffix_start
|
155
|
+
# | preamble_end | suffix_end
|
156
|
+
# | | | | termwidth
|
157
|
+
# | | | | |
|
158
|
+
# V V V V V
|
159
|
+
# --- Preamble text --------------------- suffix text --
|
160
|
+
o << color.code
|
161
|
+
o << print_at_x(preamble_start, HORIZONTAL * (termwidth - preamble_start)) # draw a full line
|
162
|
+
o << print_at_x(preamble_start, preamble)
|
163
|
+
o << color.code
|
164
|
+
o << print_at_x(suffix_start, suffix)
|
165
|
+
o << CLI::UI::Color::RESET.code
|
166
|
+
o << CLI::UI::ANSI.show_cursor
|
167
|
+
o << "\n"
|
168
|
+
|
169
|
+
o
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# typed: true
|
2
|
+
module CLI
|
3
|
+
module UI
|
4
|
+
module Frame
|
5
|
+
module FrameStyle
|
6
|
+
module Bracket
|
7
|
+
extend FrameStyle
|
8
|
+
|
9
|
+
VERTICAL = '┃'
|
10
|
+
HORIZONTAL = '━'
|
11
|
+
DIVIDER = '┣'
|
12
|
+
TOP_LEFT = '┏'
|
13
|
+
BOTTOM_LEFT = '┗'
|
14
|
+
|
15
|
+
class << self
|
16
|
+
extend T::Sig
|
17
|
+
|
18
|
+
sig { override.returns(Symbol) }
|
19
|
+
def style_name
|
20
|
+
:bracket
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { override.returns(String) }
|
24
|
+
def prefix
|
25
|
+
VERTICAL
|
26
|
+
end
|
27
|
+
|
28
|
+
# Draws the "Open" line for this frame style
|
29
|
+
#
|
30
|
+
# ==== Attributes
|
31
|
+
#
|
32
|
+
# * +text+ - (required) the text/title to output in the frame
|
33
|
+
#
|
34
|
+
# ==== Options
|
35
|
+
#
|
36
|
+
# * +:color+ - (required) The color of the frame.
|
37
|
+
#
|
38
|
+
# ==== Output
|
39
|
+
#
|
40
|
+
# ┏━━ Open
|
41
|
+
#
|
42
|
+
sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
|
43
|
+
def start(text, color:)
|
44
|
+
edge(text, color: color, first: TOP_LEFT)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Draws a "divider" line for the current frame style
|
48
|
+
#
|
49
|
+
# ==== Attributes
|
50
|
+
#
|
51
|
+
# * +text+ - (required) the text/title to output in the frame
|
52
|
+
#
|
53
|
+
# ==== Options
|
54
|
+
#
|
55
|
+
# * +:color+ - (required) The color of the frame.
|
56
|
+
#
|
57
|
+
# ==== Output:
|
58
|
+
#
|
59
|
+
# ┣━━ Divider
|
60
|
+
#
|
61
|
+
sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
|
62
|
+
def divider(text, color:)
|
63
|
+
edge(text, color: color, first: DIVIDER)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Draws the "Close" line for this frame style
|
67
|
+
#
|
68
|
+
# ==== Attributes
|
69
|
+
#
|
70
|
+
# * +text+ - (required) the text/title to output in the frame
|
71
|
+
#
|
72
|
+
# ==== Options
|
73
|
+
#
|
74
|
+
# * +:color+ - (required) The color of the frame.
|
75
|
+
# * +:right_text+ - Text to print at the right of the line. Defaults to nil
|
76
|
+
#
|
77
|
+
# ==== Output:
|
78
|
+
#
|
79
|
+
# ┗━━ Close
|
80
|
+
#
|
81
|
+
sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) }
|
82
|
+
def close(text, color:, right_text: nil)
|
83
|
+
edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
sig do
|
89
|
+
params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String)
|
90
|
+
end
|
91
|
+
def edge(text, color:, first:, right_text: nil)
|
92
|
+
color = CLI::UI.resolve_color(color)
|
93
|
+
|
94
|
+
preamble = +''
|
95
|
+
|
96
|
+
preamble << color.code << first << (HORIZONTAL * 2)
|
97
|
+
|
98
|
+
unless text.empty?
|
99
|
+
preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
|
100
|
+
end
|
101
|
+
|
102
|
+
suffix = +''
|
103
|
+
|
104
|
+
if right_text
|
105
|
+
suffix << ' ' << right_text << ' '
|
106
|
+
end
|
107
|
+
|
108
|
+
o = +''
|
109
|
+
|
110
|
+
# Shopify's CI system supports terminal emulation, but not some of
|
111
|
+
# the fancier features that we normally use to draw frames
|
112
|
+
# extra-reliably, so we fall back to a less foolproof strategy. This
|
113
|
+
# is probably better in general for cases with impoverished terminal
|
114
|
+
# emulators and no active user.
|
115
|
+
unless [0, '', nil].include?(ENV['CI'])
|
116
|
+
o << color.code << preamble
|
117
|
+
o << color.code << suffix
|
118
|
+
o << CLI::UI::Color::RESET.code
|
119
|
+
o << "\n"
|
120
|
+
|
121
|
+
return o
|
122
|
+
end
|
123
|
+
|
124
|
+
preamble_start = Frame.prefix_width
|
125
|
+
|
126
|
+
# If prefix_width is non-zero, we need to subtract the width of
|
127
|
+
# the final space, since we're going to write over it.
|
128
|
+
preamble_start -= 1 unless preamble_start.zero?
|
129
|
+
|
130
|
+
# Jumping around the line can cause some unwanted flashes
|
131
|
+
o << CLI::UI::ANSI.hide_cursor
|
132
|
+
|
133
|
+
# reset to column 1 so that things like ^C don't ruin formatting
|
134
|
+
o << "\r"
|
135
|
+
|
136
|
+
o << color.code
|
137
|
+
o << print_at_x(preamble_start, preamble + color.code + suffix)
|
138
|
+
o << CLI::UI::Color::RESET.code
|
139
|
+
o << CLI::UI::ANSI.show_cursor
|
140
|
+
o << "\n"
|
141
|
+
|
142
|
+
o
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|