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.
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