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,292 @@
1
+ # typed: true
2
+ module CLI
3
+ module UI
4
+ module Spinner
5
+ class SpinGroup
6
+ extend T::Sig
7
+
8
+ # Initializes a new spin group
9
+ # This lets you add +Task+ objects to the group to multi-thread work
10
+ #
11
+ # ==== Options
12
+ #
13
+ # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
14
+ #
15
+ # ==== Example Usage
16
+ #
17
+ # CLI::UI::SpinGroup.new do |spin_group|
18
+ # spin_group.add('Title') { |spinner| sleep 3.0 }
19
+ # spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
20
+ # end
21
+ #
22
+ # Output:
23
+ #
24
+ # https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
25
+ #
26
+ sig { params(auto_debrief: T::Boolean).void }
27
+ def initialize(auto_debrief: true)
28
+ @m = Mutex.new
29
+ @consumed_lines = 0
30
+ @tasks = []
31
+ @auto_debrief = auto_debrief
32
+ @start = Time.new
33
+ if block_given?
34
+ yield self
35
+ wait
36
+ end
37
+ end
38
+
39
+ class Task
40
+ extend T::Sig
41
+
42
+ sig { returns(String) }
43
+ attr_reader :title, :stdout, :stderr
44
+
45
+ sig { returns(T::Boolean) }
46
+ attr_reader :success
47
+
48
+ sig { returns(T.nilable(Exception)) }
49
+ attr_reader :exception
50
+
51
+ # Initializes a new Task
52
+ # This is managed entirely internally by +SpinGroup+
53
+ #
54
+ # ==== Attributes
55
+ #
56
+ # * +title+ - Title of the task
57
+ # * +block+ - Block for the task, will be provided with an instance of the spinner
58
+ #
59
+ sig { params(title: String, block: T.proc.params(task: Task).returns(T.untyped)).void }
60
+ def initialize(title, &block)
61
+ @title = title
62
+ @always_full_render = title =~ Formatter::SCAN_WIDGET
63
+ @thread = Thread.new do
64
+ cap = CLI::UI::StdoutRouter::Capture.new(with_frame_inset: false) { block.call(self) }
65
+ begin
66
+ cap.run
67
+ ensure
68
+ @stdout = cap.stdout
69
+ @stderr = cap.stderr
70
+ end
71
+ end
72
+
73
+ @m = Mutex.new
74
+ @force_full_render = false
75
+ @done = false
76
+ @exception = nil
77
+ @success = false
78
+ end
79
+
80
+ # Checks if a task is finished
81
+ #
82
+ sig { returns(T::Boolean) }
83
+ def check
84
+ return true if @done
85
+ return false if @thread.alive?
86
+
87
+ @done = true
88
+ begin
89
+ status = @thread.join.status
90
+ @success = (status == false)
91
+ @success = false if @thread.value == TASK_FAILED
92
+ rescue => exc
93
+ @exception = exc
94
+ @success = false
95
+ end
96
+
97
+ @done
98
+ end
99
+
100
+ # Re-renders the task if required:
101
+ #
102
+ # We try to be as lazy as possible in re-rendering the full line. The
103
+ # spinner rune will change on each render for the most part, but the
104
+ # body text will rarely have changed. If the body text *has* changed,
105
+ # we set @force_full_render.
106
+ #
107
+ # Further, if the title string includes any CLI::UI::Widgets, we
108
+ # assume that it may change from render to render, since those
109
+ # evaluate more dynamically than the rest of our format codes, which
110
+ # are just text formatters. This is controlled by @always_full_render.
111
+ #
112
+ # ==== Attributes
113
+ #
114
+ # * +index+ - index of the task
115
+ # * +force+ - force rerender of the task
116
+ # * +width+ - current terminal width to format for
117
+ #
118
+ sig { params(index: Integer, force: T::Boolean, width: Integer).returns(String) }
119
+ def render(index, force = true, width: CLI::UI::Terminal.width)
120
+ @m.synchronize do
121
+ if force || @always_full_render || @force_full_render
122
+ full_render(index, width)
123
+ else
124
+ partial_render(index)
125
+ end
126
+ ensure
127
+ @force_full_render = false
128
+ end
129
+ end
130
+
131
+ # Update the spinner title
132
+ #
133
+ # ==== Attributes
134
+ #
135
+ # * +title+ - title to change the spinner to
136
+ #
137
+ sig { params(new_title: String).void }
138
+ def update_title(new_title)
139
+ @m.synchronize do
140
+ @always_full_render = new_title =~ Formatter::SCAN_WIDGET
141
+ @title = new_title
142
+ @force_full_render = true
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ sig { params(index: Integer, terminal_width: Integer).returns(String) }
149
+ def full_render(index, terminal_width)
150
+ prefix = inset +
151
+ glyph(index) +
152
+ CLI::UI::Color::RESET.code +
153
+ ' '
154
+
155
+ truncation_width = terminal_width - CLI::UI::ANSI.printing_width(prefix)
156
+
157
+ prefix +
158
+ CLI::UI.resolve_text(title, truncate_to: truncation_width) +
159
+ "\e[K"
160
+ end
161
+
162
+ sig { params(index: Integer).returns(String) }
163
+ def partial_render(index)
164
+ CLI::UI::ANSI.cursor_forward(inset_width) + glyph(index) + CLI::UI::Color::RESET.code
165
+ end
166
+
167
+ sig { params(index: Integer).returns(String) }
168
+ def glyph(index)
169
+ if @done
170
+ @success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s
171
+ else
172
+ GLYPHS[index]
173
+ end
174
+ end
175
+
176
+ sig { returns(String) }
177
+ def inset
178
+ @inset ||= CLI::UI::Frame.prefix
179
+ end
180
+
181
+ sig { returns(Integer) }
182
+ def inset_width
183
+ @inset_width ||= CLI::UI::ANSI.printing_width(inset)
184
+ end
185
+ end
186
+
187
+ # Add a new task
188
+ #
189
+ # ==== Attributes
190
+ #
191
+ # * +title+ - Title of the task
192
+ # * +block+ - Block for the task, will be provided with an instance of the spinner
193
+ #
194
+ # ==== Example Usage:
195
+ # spin_group = CLI::UI::SpinGroup.new
196
+ # spin_group.add('Title') { |spinner| sleep 1.0 }
197
+ # spin_group.wait
198
+ #
199
+ sig { params(title: String, block: T.proc.params(task: Task).void).void }
200
+ def add(title, &block)
201
+ @m.synchronize do
202
+ @tasks << Task.new(title, &block)
203
+ end
204
+ end
205
+
206
+ # Tells the group you're done adding tasks and to wait for all of them to finish
207
+ #
208
+ # ==== Example Usage:
209
+ # spin_group = CLI::UI::SpinGroup.new
210
+ # spin_group.add('Title') { |spinner| sleep 1.0 }
211
+ # spin_group.wait
212
+ #
213
+ sig { returns(T::Boolean) }
214
+ def wait
215
+ idx = 0
216
+
217
+ loop do
218
+ all_done = T.let(true, T::Boolean)
219
+
220
+ width = CLI::UI::Terminal.width
221
+
222
+ @m.synchronize do
223
+ CLI::UI.raw do
224
+ @tasks.each.with_index do |task, int_index|
225
+ nat_index = int_index + 1
226
+ task_done = task.check
227
+ all_done = false unless task_done
228
+
229
+ if nat_index > @consumed_lines
230
+ print(task.render(idx, true, width: width) + "\n")
231
+ @consumed_lines += 1
232
+ else
233
+ offset = @consumed_lines - int_index
234
+ move_to = CLI::UI::ANSI.cursor_up(offset) + "\r"
235
+ move_from = "\r" + CLI::UI::ANSI.cursor_down(offset)
236
+
237
+ print(move_to + task.render(idx, idx.zero?, width: width) + move_from)
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ break if all_done
244
+
245
+ idx = (idx + 1) % GLYPHS.size
246
+ Spinner.index = idx
247
+ sleep(PERIOD)
248
+ end
249
+
250
+ if @auto_debrief
251
+ debrief
252
+ else
253
+ @m.synchronize do
254
+ @tasks.all?(&:success)
255
+ end
256
+ end
257
+ end
258
+
259
+ # Debriefs failed tasks is +auto_debrief+ is true
260
+ #
261
+ sig { returns(T::Boolean) }
262
+ def debrief
263
+ @m.synchronize do
264
+ @tasks.each do |task|
265
+ next if task.success
266
+
267
+ e = task.exception
268
+ out = task.stdout
269
+ err = task.stderr
270
+
271
+ CLI::UI::Frame.open('Task Failed: ' + task.title, color: :red, timing: Time.new - @start) do
272
+ if e
273
+ puts "#{e.class}: #{e.message}"
274
+ puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
275
+ end
276
+
277
+ CLI::UI::Frame.divider('STDOUT')
278
+ out = '(empty)' if out.nil? || out.strip.empty?
279
+ puts out
280
+
281
+ CLI::UI::Frame.divider('STDERR')
282
+ err = '(empty)' if err.nil? || err.strip.empty?
283
+ puts err
284
+ end
285
+ end
286
+ @tasks.all?(&:success)
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,82 @@
1
+ # typed: true
2
+ # frozen-string-literal: true
3
+
4
+ require 'cli/ui'
5
+
6
+ module CLI
7
+ module UI
8
+ module Spinner
9
+ extend T::Sig
10
+
11
+ autoload :Async, 'cli/ui/spinner/async'
12
+ autoload :SpinGroup, 'cli/ui/spinner/spin_group'
13
+
14
+ PERIOD = 0.1 # seconds
15
+ TASK_FAILED = :task_failed
16
+
17
+ RUNES = if CLI::UI::OS.current.use_emoji?
18
+ ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze
19
+ else
20
+ ['\\', '|', '/', '-', '\\', '|', '/', '-'].freeze
21
+ end
22
+
23
+ colors = [CLI::UI::Color::CYAN.code] * (RUNES.size / 2).ceil +
24
+ [CLI::UI::Color::MAGENTA.code] * (RUNES.size / 2).to_i
25
+ GLYPHS = colors.zip(RUNES).map(&:join)
26
+
27
+ class << self
28
+ extend T::Sig
29
+
30
+ sig { returns(T.nilable(Integer)) }
31
+ attr_accessor(:index)
32
+
33
+ # We use this from CLI::UI::Widgets::Status to render an additional
34
+ # spinner next to the "working" element. While this global state looks
35
+ # a bit repulsive at first, it's worth realizing that:
36
+ #
37
+ # * It's managed by the SpinGroup#wait method, not individual tasks; and
38
+ # * It would be complete insanity to run two separate but concurrent SpinGroups.
39
+ #
40
+ # While it would be possible to stitch through some connection between
41
+ # the SpinGroup and the Widgets included in its title, this is simpler
42
+ # in practice and seems unlikely to cause issues in practice.
43
+ sig { returns(String) }
44
+ def current_rune
45
+ RUNES[index || 0]
46
+ end
47
+ end
48
+
49
+ # Adds a single spinner
50
+ # Uses an interactive session to allow the user to pick an answer
51
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
52
+ #
53
+ # https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
54
+ #
55
+ # ==== Attributes
56
+ #
57
+ # * +title+ - Title of the spinner to use
58
+ #
59
+ # ==== Options
60
+ #
61
+ # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
62
+ #
63
+ # ==== Block
64
+ #
65
+ # * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
66
+ #
67
+ # ==== Example Usage:
68
+ #
69
+ # CLI::UI::Spinner.spin('Title') { sleep 1.0 }
70
+ #
71
+ sig do
72
+ params(title: String, auto_debrief: T::Boolean, block: T.proc.params(task: SpinGroup::Task).void)
73
+ .returns(T::Boolean)
74
+ end
75
+ def self.spin(title, auto_debrief: true, &block)
76
+ sg = SpinGroup.new(auto_debrief: auto_debrief)
77
+ sg.add(title, &block)
78
+ sg.wait
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,264 @@
1
+ # typed: true
2
+ require 'cli/ui'
3
+ require 'stringio'
4
+
5
+ module CLI
6
+ module UI
7
+ module StdoutRouter
8
+ class Writer
9
+ extend T::Sig
10
+
11
+ sig { params(stream: IOLike, name: Symbol).void }
12
+ def initialize(stream, name)
13
+ @stream = stream
14
+ @name = name
15
+ end
16
+
17
+ sig { params(args: String).void }
18
+ def write(*args)
19
+ args = args.map do |str|
20
+ if auto_frame_inset?
21
+ str = str.dup # unfreeze
22
+ str = str.force_encoding(Encoding::UTF_8)
23
+ apply_line_prefix(str, CLI::UI::Frame.prefix)
24
+ else
25
+ @pending_newline = false
26
+ str
27
+ end
28
+ end
29
+
30
+ # hook return of false suppresses output.
31
+ if (hook = Thread.current[:cliui_output_hook])
32
+ return if hook.call(args.map(&:to_s).join, @name) == false
33
+ end
34
+
35
+ T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, args))
36
+ if (dup = StdoutRouter.duplicate_output_to)
37
+ T.unsafe(dup).write(*prepend_id(dup, args))
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ sig { params(stream: IOLike, args: T::Array[String]).returns(T::Array[String]) }
44
+ def prepend_id(stream, args)
45
+ return args unless prepend_id_for_stream(stream)
46
+
47
+ args.map do |a|
48
+ next a if a.chomp.empty? # allow new lines to be new lines
49
+
50
+ "[#{Thread.current[:cliui_output_id][:id]}] #{a}"
51
+ end
52
+ end
53
+
54
+ sig { params(stream: IOLike).returns(T::Boolean) }
55
+ def prepend_id_for_stream(stream)
56
+ return false unless Thread.current[:cliui_output_id]
57
+ return true if Thread.current[:cliui_output_id][:streams].include?(stream)
58
+
59
+ false
60
+ end
61
+
62
+ sig { returns(T::Boolean) }
63
+ def auto_frame_inset?
64
+ !Thread.current[:no_cliui_frame_inset]
65
+ end
66
+
67
+ sig { params(str: String, prefix: String).returns(String) }
68
+ def apply_line_prefix(str, prefix)
69
+ return '' if str.empty?
70
+
71
+ prefixed = +''
72
+ str.force_encoding(Encoding::UTF_8).lines.each do |line|
73
+ if @pending_newline
74
+ prefixed << line
75
+ @pending_newline = false
76
+ else
77
+ prefixed << prefix << line
78
+ end
79
+ end
80
+ @pending_newline = !str.end_with?("\n")
81
+ prefixed
82
+ end
83
+ end
84
+
85
+ class Capture
86
+ extend T::Sig
87
+
88
+ @m = Mutex.new
89
+ @active_captures = 0
90
+ @saved_stdin = nil
91
+
92
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
93
+ def self.with_stdin_masked(&block)
94
+ @m.synchronize do
95
+ if @active_captures.zero?
96
+ @saved_stdin = $stdin
97
+ $stdin, w = IO.pipe
98
+ $stdin.close
99
+ w.close
100
+ end
101
+ @active_captures += 1
102
+ end
103
+
104
+ yield
105
+ ensure
106
+ @m.synchronize do
107
+ @active_captures -= 1
108
+ if @active_captures.zero?
109
+ $stdin = @saved_stdin
110
+ end
111
+ end
112
+ end
113
+
114
+ sig do
115
+ params(with_frame_inset: T::Boolean, block: T.proc.void).void
116
+ end
117
+ def initialize(with_frame_inset: true, &block)
118
+ @with_frame_inset = with_frame_inset
119
+ @block = block
120
+ @stdout = ''
121
+ @stderr = ''
122
+ end
123
+
124
+ sig { returns(String) }
125
+ attr_reader :stdout, :stderr
126
+
127
+ sig { returns(T.untyped) }
128
+ def run
129
+ require 'stringio'
130
+
131
+ StdoutRouter.assert_enabled!
132
+
133
+ out = StringIO.new
134
+ err = StringIO.new
135
+
136
+ prev_frame_inset = Thread.current[:no_cliui_frame_inset]
137
+ prev_hook = Thread.current[:cliui_output_hook]
138
+
139
+ if Thread.current.respond_to?(:report_on_exception)
140
+ Thread.current.report_on_exception = false
141
+ end
142
+
143
+ self.class.with_stdin_masked do
144
+ Thread.current[:no_cliui_frame_inset] = !@with_frame_inset
145
+ Thread.current[:cliui_output_hook] = ->(data, stream) do
146
+ case stream
147
+ when :stdout then out.write(data)
148
+ when :stderr then err.write(data)
149
+ else raise
150
+ end
151
+ false # suppress writing to terminal
152
+ end
153
+
154
+ begin
155
+ @block.call
156
+ ensure
157
+ @stdout = out.string
158
+ @stderr = err.string
159
+ end
160
+ end
161
+ ensure
162
+ Thread.current[:cliui_output_hook] = prev_hook
163
+ Thread.current[:no_cliui_frame_inset] = prev_frame_inset
164
+ end
165
+ end
166
+
167
+ class << self
168
+ extend T::Sig
169
+
170
+ WRITE_WITHOUT_CLI_UI = :write_without_cli_ui
171
+
172
+ NotEnabled = Class.new(StandardError)
173
+
174
+ sig { returns(T.nilable(IOLike)) }
175
+ attr_accessor :duplicate_output_to
176
+
177
+ sig do
178
+ type_parameters(:T)
179
+ .params(on_streams: T::Array[IOLike], block: T.proc.params(id: String).returns(T.type_parameter(:T)))
180
+ .returns(T.type_parameter(:T))
181
+ end
182
+ def with_id(on_streams:, &block)
183
+ require 'securerandom'
184
+ id = format('%05d', rand(10**5))
185
+ Thread.current[:cliui_output_id] = {
186
+ id: id,
187
+ streams: on_streams.map { |stream| T.cast(stream, IOLike) },
188
+ }
189
+ yield(id)
190
+ ensure
191
+ Thread.current[:cliui_output_id] = nil
192
+ end
193
+
194
+ sig { returns(T.nilable(T::Hash[Symbol, T.any(String, IOLike)])) }
195
+ def current_id
196
+ Thread.current[:cliui_output_id]
197
+ end
198
+
199
+ sig { void }
200
+ def assert_enabled!
201
+ raise NotEnabled unless enabled?
202
+ end
203
+
204
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
205
+ def with_enabled(&block)
206
+ enable
207
+ yield
208
+ ensure
209
+ disable
210
+ end
211
+
212
+ # TODO: remove this
213
+ sig { void }
214
+ def ensure_activated
215
+ enable unless enabled?
216
+ end
217
+
218
+ sig { returns(T::Boolean) }
219
+ def enable
220
+ return false if enabled?($stdout) || enabled?($stderr)
221
+
222
+ activate($stdout, :stdout)
223
+ activate($stderr, :stderr)
224
+ true
225
+ end
226
+
227
+ sig { params(stream: IOLike).returns(T::Boolean) }
228
+ def enabled?(stream = $stdout)
229
+ stream.respond_to?(WRITE_WITHOUT_CLI_UI)
230
+ end
231
+
232
+ sig { returns(T::Boolean) }
233
+ def disable
234
+ return false unless enabled?($stdout) && enabled?($stderr)
235
+
236
+ deactivate($stdout)
237
+ deactivate($stderr)
238
+ true
239
+ end
240
+
241
+ private
242
+
243
+ sig { params(stream: IOLike).void }
244
+ def deactivate(stream)
245
+ sc = stream.singleton_class
246
+ sc.send(:remove_method, :write)
247
+ sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI)
248
+ end
249
+
250
+ sig { params(stream: IOLike, streamname: Symbol).void }
251
+ def activate(stream, streamname)
252
+ writer = StdoutRouter::Writer.new(stream, streamname)
253
+
254
+ raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI)
255
+
256
+ stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write)
257
+ stream.define_singleton_method(:write) do |*args|
258
+ writer.write(*args)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,53 @@
1
+ # typed: true
2
+ require 'cli/ui'
3
+ require 'io/console'
4
+
5
+ module CLI
6
+ module UI
7
+ module Terminal
8
+ extend T::Sig
9
+
10
+ DEFAULT_WIDTH = 80
11
+ DEFAULT_HEIGHT = 24
12
+
13
+ # Returns the width of the terminal, if possible
14
+ # Otherwise will return DEFAULT_WIDTH
15
+ #
16
+ sig { returns(Integer) }
17
+ def self.width
18
+ winsize[1]
19
+ end
20
+
21
+ # Returns the width of the terminal, if possible
22
+ # Otherwise, will return DEFAULT_HEIGHT
23
+ #
24
+ sig { returns(Integer) }
25
+ def self.height
26
+ winsize[0]
27
+ end
28
+
29
+ sig { returns([Integer, Integer]) }
30
+ def self.winsize
31
+ @winsize ||= begin
32
+ winsize = IO.console.winsize
33
+ setup_winsize_trap
34
+
35
+ if winsize.any?(&:zero?)
36
+ [DEFAULT_HEIGHT, DEFAULT_WIDTH]
37
+ else
38
+ winsize
39
+ end
40
+ rescue
41
+ [DEFAULT_HEIGHT, DEFAULT_WIDTH]
42
+ end
43
+ end
44
+
45
+ sig { void }
46
+ def self.setup_winsize_trap
47
+ @winsize_trap ||= Signal.trap('WINCH') do
48
+ @winsize = nil
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end