gorails 0.1.0 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile +3 -1
  4. data/Gemfile.lock +65 -0
  5. data/README.md +41 -12
  6. data/bin/update-deps +95 -0
  7. data/exe/gorails +18 -0
  8. data/gorails.gemspec +4 -3
  9. data/lib/gorails/commands/episodes.rb +25 -0
  10. data/lib/gorails/commands/example.rb +19 -0
  11. data/lib/gorails/commands/help.rb +21 -0
  12. data/lib/gorails/commands/jobs.rb +25 -0
  13. data/lib/gorails/commands/jumpstart.rb +29 -0
  14. data/lib/gorails/commands/railsbytes.rb +67 -0
  15. data/lib/gorails/commands.rb +19 -0
  16. data/lib/gorails/entry_point.rb +10 -0
  17. data/lib/gorails/version.rb +1 -1
  18. data/lib/gorails.rb +22 -1
  19. data/vendor/deps/cli-kit/REVISION +1 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  36. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  37. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  38. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  39. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  40. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  41. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  42. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  43. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  44. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  45. data/vendor/deps/cli-ui/REVISION +1 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  65. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  68. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  69. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  70. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  71. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  72. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  73. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  74. 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