gorails 0.1.1 → 0.1.4

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