rfix 2.0.4 → 3.0.0

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/exe/rfix +11 -90
  3. data/lib/rfix.rb +10 -9
  4. data/lib/rfix/branch/reference.rb +2 -2
  5. data/lib/rfix/branch/upstream.rb +2 -4
  6. data/lib/rfix/cli/command.rb +14 -1
  7. data/lib/rfix/cli/command/all.rb +21 -0
  8. data/lib/rfix/cli/command/base.rb +30 -19
  9. data/lib/rfix/cli/command/branch.rb +2 -0
  10. data/lib/rfix/cli/command/help.rb +2 -0
  11. data/lib/rfix/cli/command/info.rb +6 -1
  12. data/lib/rfix/cli/command/local.rb +2 -0
  13. data/lib/rfix/cli/command/origin.rb +2 -0
  14. data/lib/rfix/cli/command/setup.rb +2 -0
  15. data/lib/rfix/cli/command/status.rb +39 -0
  16. data/lib/rfix/collector.rb +69 -0
  17. data/lib/rfix/diff.rb +69 -0
  18. data/lib/rfix/extension/comment_config.rb +15 -0
  19. data/lib/rfix/extension/offense.rb +17 -14
  20. data/lib/rfix/extension/pastel.rb +7 -4
  21. data/lib/rfix/extension/progresbar.rb +15 -0
  22. data/lib/rfix/extension/strings.rb +10 -2
  23. data/lib/rfix/file.rb +5 -3
  24. data/lib/rfix/file/base.rb +21 -14
  25. data/lib/rfix/file/deleted.rb +2 -0
  26. data/lib/rfix/file/ignored.rb +2 -0
  27. data/lib/rfix/file/null.rb +17 -0
  28. data/lib/rfix/file/tracked.rb +39 -23
  29. data/lib/rfix/file/undefined.rb +17 -0
  30. data/lib/rfix/file/untracked.rb +3 -1
  31. data/lib/rfix/formatter.rb +67 -71
  32. data/lib/rfix/highlighter.rb +1 -3
  33. data/lib/rfix/rake/gemfile.rb +26 -23
  34. data/lib/rfix/repository.rb +59 -96
  35. data/lib/rfix/types.rb +24 -14
  36. data/lib/rfix/version.rb +1 -1
  37. data/rfix.gemspec +11 -3
  38. data/vendor/cli-ui/Gemfile +17 -0
  39. data/vendor/cli-ui/Gemfile.lock +60 -0
  40. data/vendor/cli-ui/LICENSE.txt +21 -0
  41. data/vendor/cli-ui/README.md +224 -0
  42. data/vendor/cli-ui/Rakefile +20 -0
  43. data/vendor/cli-ui/bin/console +14 -0
  44. data/vendor/cli-ui/cli-ui.gemspec +25 -0
  45. data/vendor/cli-ui/dev.yml +14 -0
  46. data/vendor/cli-ui/lib/cli/ui.rb +233 -0
  47. data/vendor/cli-ui/lib/cli/ui/ansi.rb +157 -0
  48. data/vendor/cli-ui/lib/cli/ui/color.rb +84 -0
  49. data/vendor/cli-ui/lib/cli/ui/formatter.rb +192 -0
  50. data/vendor/cli-ui/lib/cli/ui/frame.rb +269 -0
  51. data/vendor/cli-ui/lib/cli/ui/frame/frame_stack.rb +98 -0
  52. data/vendor/cli-ui/lib/cli/ui/frame/frame_style.rb +120 -0
  53. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/box.rb +166 -0
  54. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
  55. data/vendor/cli-ui/lib/cli/ui/glyph.rb +84 -0
  56. data/vendor/cli-ui/lib/cli/ui/os.rb +67 -0
  57. data/vendor/cli-ui/lib/cli/ui/printer.rb +59 -0
  58. data/vendor/cli-ui/lib/cli/ui/progress.rb +90 -0
  59. data/vendor/cli-ui/lib/cli/ui/prompt.rb +297 -0
  60. data/vendor/cli-ui/lib/cli/ui/prompt/interactive_options.rb +484 -0
  61. data/vendor/cli-ui/lib/cli/ui/prompt/options_handler.rb +29 -0
  62. data/vendor/cli-ui/lib/cli/ui/spinner.rb +66 -0
  63. data/vendor/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
  64. data/vendor/cli-ui/lib/cli/ui/spinner/spin_group.rb +263 -0
  65. data/vendor/cli-ui/lib/cli/ui/stdout_router.rb +232 -0
  66. data/vendor/cli-ui/lib/cli/ui/terminal.rb +46 -0
  67. data/vendor/cli-ui/lib/cli/ui/truncater.rb +102 -0
  68. data/vendor/cli-ui/lib/cli/ui/version.rb +5 -0
  69. data/vendor/cli-ui/lib/cli/ui/widgets.rb +77 -0
  70. data/vendor/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
  71. data/vendor/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
  72. data/vendor/cli-ui/lib/cli/ui/wrap.rb +56 -0
  73. data/vendor/cli-ui/test/cli/ui/ansi_test.rb +32 -0
  74. data/vendor/cli-ui/test/cli/ui/cli_ui_test.rb +23 -0
  75. data/vendor/cli-ui/test/cli/ui/color_test.rb +40 -0
  76. data/vendor/cli-ui/test/cli/ui/formatter_test.rb +79 -0
  77. data/vendor/cli-ui/test/cli/ui/glyph_test.rb +68 -0
  78. data/vendor/cli-ui/test/cli/ui/printer_test.rb +103 -0
  79. data/vendor/cli-ui/test/cli/ui/progress_test.rb +46 -0
  80. data/vendor/cli-ui/test/cli/ui/prompt/options_handler_test.rb +39 -0
  81. data/vendor/cli-ui/test/cli/ui/prompt_test.rb +348 -0
  82. data/vendor/cli-ui/test/cli/ui/spinner/spin_group_test.rb +39 -0
  83. data/vendor/cli-ui/test/cli/ui/spinner_test.rb +141 -0
  84. data/vendor/cli-ui/test/cli/ui/stdout_router_test.rb +32 -0
  85. data/vendor/cli-ui/test/cli/ui/terminal_test.rb +26 -0
  86. data/vendor/cli-ui/test/cli/ui/truncater_test.rb +31 -0
  87. data/vendor/cli-ui/test/cli/ui/widgets/status_test.rb +49 -0
  88. data/vendor/cli-ui/test/cli/ui/widgets_test.rb +15 -0
  89. data/vendor/cli-ui/test/test_helper.rb +53 -0
  90. data/vendor/cli-ui/tmp/cache/bootsnap/compile-cache/d9/c036af0f3dc494 +0 -0
  91. data/vendor/cli-ui/tmp/cache/bootsnap/load-path-cache +0 -0
  92. data/vendor/dry-cli/lib/dry/cli/command.rb +2 -1
  93. data/vendor/dry-cli/tmp/cache/bootsnap/compile-cache/ff/a22a5daafbd74c +0 -0
  94. data/vendor/dry-cli/tmp/cache/bootsnap/load-path-cache +0 -0
  95. data/vendor/strings-ansi/tmp/cache/bootsnap/compile-cache/79/49cf49407b370e +0 -0
  96. data/vendor/strings-ansi/tmp/cache/bootsnap/load-path-cache +0 -0
  97. metadata +170 -9
  98. data/lib/rfix/extension/string.rb +0 -12
  99. data/lib/rfix/indicator.rb +0 -19
@@ -0,0 +1,29 @@
1
+ module CLI
2
+ module UI
3
+ module Prompt
4
+ # A class that handles the various options of an InteractivePrompt and their callbacks
5
+ class OptionsHandler
6
+ def initialize
7
+ @options = {}
8
+ end
9
+
10
+ def options
11
+ @options.keys
12
+ end
13
+
14
+ def option(option, &handler)
15
+ @options[option] = handler
16
+ end
17
+
18
+ def call(options)
19
+ case options
20
+ when Array
21
+ options.map { |option| @options[option].call(options) }
22
+ else
23
+ @options[options].call(options)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,66 @@
1
+ # frozen-string-literal: true
2
+ require 'cli/ui'
3
+
4
+ module CLI
5
+ module UI
6
+ module Spinner
7
+ autoload :Async, 'cli/ui/spinner/async'
8
+ autoload :SpinGroup, 'cli/ui/spinner/spin_group'
9
+
10
+ PERIOD = 0.1 # seconds
11
+ TASK_FAILED = :task_failed
12
+
13
+ RUNES = CLI::UI::OS.current.supports_emoji? ? %w(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏).freeze : %w(\\ | / - \\ | / -).freeze
14
+
15
+ colors = [CLI::UI::Color::CYAN.code] * (RUNES.size / 2).ceil +
16
+ [CLI::UI::Color::MAGENTA.code] * (RUNES.size / 2).to_i
17
+ GLYPHS = colors.zip(RUNES).map(&:join)
18
+
19
+ class << self
20
+ attr_accessor(:index)
21
+
22
+ # We use this from CLI::UI::Widgets::Status to render an additional
23
+ # spinner next to the "working" element. While this global state looks
24
+ # a bit repulsive at first, it's worth realizing that:
25
+ #
26
+ # * It's managed by the SpinGroup#wait method, not individual tasks; and
27
+ # * It would be complete insanity to run two separate but concurrent SpinGroups.
28
+ #
29
+ # While it would be possible to stitch through some connection between
30
+ # the SpinGroup and the Widgets included in its title, this is simpler
31
+ # in practice and seems unlikely to cause issues in practice.
32
+ def current_rune
33
+ RUNES[index || 0]
34
+ end
35
+ end
36
+
37
+ # Adds a single spinner
38
+ # Uses an interactive session to allow the user to pick an answer
39
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
40
+ #
41
+ # https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
42
+ #
43
+ # ==== Attributes
44
+ #
45
+ # * +title+ - Title of the spinner to use
46
+ #
47
+ # ==== Options
48
+ #
49
+ # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
50
+ #
51
+ # ==== Block
52
+ #
53
+ # * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
54
+ #
55
+ # ==== Example Usage:
56
+ #
57
+ # CLI::UI::Spinner.spin('Title') { sleep 1.0 }
58
+ #
59
+ def self.spin(title, auto_debrief: true, &block)
60
+ sg = SpinGroup.new(auto_debrief: auto_debrief)
61
+ sg.add(title, &block)
62
+ sg.wait
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ module CLI
2
+ module UI
3
+ module Spinner
4
+ class Async
5
+ # Convenience method for +initialize+
6
+ #
7
+ def self.start(title)
8
+ new(title)
9
+ end
10
+
11
+ # Initializes a new asynchronous spinner with no specific end.
12
+ # Must call +.stop+ to end the spinner
13
+ #
14
+ # ==== Attributes
15
+ #
16
+ # * +title+ - Title of the spinner to use
17
+ #
18
+ # ==== Example Usage:
19
+ #
20
+ # CLI::UI::Spinner::Async.new('Title')
21
+ #
22
+ def initialize(title)
23
+ require 'thread'
24
+ sg = CLI::UI::Spinner::SpinGroup.new
25
+ @m = Mutex.new
26
+ @cv = ConditionVariable.new
27
+ sg.add(title) { @m.synchronize { @cv.wait(@m) } }
28
+ @t = Thread.new { sg.wait }
29
+ end
30
+
31
+ # Stops an asynchronous spinner
32
+ #
33
+ def stop
34
+ @m.synchronize { @cv.signal }
35
+ @t.value
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,263 @@
1
+ module CLI
2
+ module UI
3
+ module Spinner
4
+ class SpinGroup
5
+ # Initializes a new spin group
6
+ # This lets you add +Task+ objects to the group to multi-thread work
7
+ #
8
+ # ==== Options
9
+ #
10
+ # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
11
+ #
12
+ # ==== Example Usage
13
+ #
14
+ # spin_group = CLI::UI::SpinGroup.new
15
+ # spin_group.add('Title') { |spinner| sleep 3.0 }
16
+ # spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
17
+ # spin_group.wait
18
+ #
19
+ # Output:
20
+ #
21
+ # https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
22
+ #
23
+ def initialize(auto_debrief: true)
24
+ @m = Mutex.new
25
+ @consumed_lines = 0
26
+ @tasks = []
27
+ @auto_debrief = auto_debrief
28
+ @start = Time.new
29
+ end
30
+
31
+ class Task
32
+ attr_reader :title, :exception, :success, :stdout, :stderr
33
+
34
+ # Initializes a new Task
35
+ # This is managed entirely internally by +SpinGroup+
36
+ #
37
+ # ==== Attributes
38
+ #
39
+ # * +title+ - Title of the task
40
+ # * +block+ - Block for the task, will be provided with an instance of the spinner
41
+ #
42
+ def initialize(title, &block)
43
+ @title = title
44
+ @always_full_render = title =~ Formatter::SCAN_WIDGET
45
+ @thread = Thread.new do
46
+ cap = CLI::UI::StdoutRouter::Capture.new(self, with_frame_inset: false, &block)
47
+ begin
48
+ cap.run
49
+ ensure
50
+ @stdout = cap.stdout
51
+ @stderr = cap.stderr
52
+ end
53
+ end
54
+
55
+ @m = Mutex.new
56
+ @force_full_render = false
57
+ @done = false
58
+ @exception = nil
59
+ @success = false
60
+ end
61
+
62
+ # Checks if a task is finished
63
+ #
64
+ def check
65
+ return true if @done
66
+ return false if @thread.alive?
67
+
68
+ @done = true
69
+ begin
70
+ status = @thread.join.status
71
+ @success = (status == false)
72
+ @success = false if @thread.value == TASK_FAILED
73
+ rescue => exc
74
+ @exception = exc
75
+ @success = false
76
+ end
77
+
78
+ @done
79
+ end
80
+
81
+ # Re-renders the task if required:
82
+ #
83
+ # We try to be as lazy as possible in re-rendering the full line. The
84
+ # spinner rune will change on each render for the most part, but the
85
+ # body text will rarely have changed. If the body text *has* changed,
86
+ # we set @force_full_render.
87
+ #
88
+ # Further, if the title string includes any CLI::UI::Widgets, we
89
+ # assume that it may change from render to render, since those
90
+ # evaluate more dynamically than the rest of our format codes, which
91
+ # are just text formatters. This is controlled by @always_full_render.
92
+ #
93
+ # ==== Attributes
94
+ #
95
+ # * +index+ - index of the task
96
+ # * +force+ - force rerender of the task
97
+ # * +width+ - current terminal width to format for
98
+ #
99
+ def render(index, force = true, width: CLI::UI::Terminal.width)
100
+ @m.synchronize do
101
+ if force || @always_full_render || @force_full_render
102
+ full_render(index, width)
103
+ else
104
+ partial_render(index)
105
+ end
106
+ ensure
107
+ @force_full_render = false
108
+ end
109
+ end
110
+
111
+ # Update the spinner title
112
+ #
113
+ # ==== Attributes
114
+ #
115
+ # * +title+ - title to change the spinner to
116
+ #
117
+ def update_title(new_title)
118
+ @m.synchronize do
119
+ @always_full_render = new_title =~ Formatter::SCAN_WIDGET
120
+ @title = new_title
121
+ @force_full_render = true
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def full_render(index, terminal_width)
128
+ prefix = inset +
129
+ glyph(index) +
130
+ CLI::UI::Color::RESET.code +
131
+ ' '
132
+
133
+ truncation_width = terminal_width - CLI::UI::ANSI.printing_width(prefix)
134
+
135
+ prefix +
136
+ CLI::UI.resolve_text(title, truncate_to: truncation_width) +
137
+ "\e[K"
138
+ end
139
+
140
+ def partial_render(index)
141
+ CLI::UI::ANSI.cursor_forward(inset_width) + glyph(index) + CLI::UI::Color::RESET.code
142
+ end
143
+
144
+ def glyph(index)
145
+ if @done
146
+ @success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s
147
+ else
148
+ GLYPHS[index]
149
+ end
150
+ end
151
+
152
+ def inset
153
+ @inset ||= CLI::UI::Frame.prefix
154
+ end
155
+
156
+ def inset_width
157
+ @inset_width ||= CLI::UI::ANSI.printing_width(inset)
158
+ end
159
+ end
160
+
161
+ # Add a new task
162
+ #
163
+ # ==== Attributes
164
+ #
165
+ # * +title+ - Title of the task
166
+ # * +block+ - Block for the task, will be provided with an instance of the spinner
167
+ #
168
+ # ==== Example Usage:
169
+ # spin_group = CLI::UI::SpinGroup.new
170
+ # spin_group.add('Title') { |spinner| sleep 1.0 }
171
+ # spin_group.wait
172
+ #
173
+ def add(title, &block)
174
+ @m.synchronize do
175
+ @tasks << Task.new(title, &block)
176
+ end
177
+ end
178
+
179
+ # Tells the group you're done adding tasks and to wait for all of them to finish
180
+ #
181
+ # ==== Example Usage:
182
+ # spin_group = CLI::UI::SpinGroup.new
183
+ # spin_group.add('Title') { |spinner| sleep 1.0 }
184
+ # spin_group.wait
185
+ #
186
+ def wait
187
+ idx = 0
188
+
189
+ loop do
190
+ all_done = true
191
+
192
+ width = CLI::UI::Terminal.width
193
+
194
+ @m.synchronize do
195
+ CLI::UI.raw do
196
+ @tasks.each.with_index do |task, int_index|
197
+ nat_index = int_index + 1
198
+ task_done = task.check
199
+ all_done = false unless task_done
200
+
201
+ if nat_index > @consumed_lines
202
+ print(task.render(idx, true, width: width) + "\n")
203
+ @consumed_lines += 1
204
+ else
205
+ offset = @consumed_lines - int_index
206
+ move_to = CLI::UI::ANSI.cursor_up(offset) + "\r"
207
+ move_from = "\r" + CLI::UI::ANSI.cursor_down(offset)
208
+
209
+ print(move_to + task.render(idx, idx.zero?, width: width) + move_from)
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ break if all_done
216
+
217
+ idx = (idx + 1) % GLYPHS.size
218
+ Spinner.index = idx
219
+ sleep(PERIOD)
220
+ end
221
+
222
+ if @auto_debrief
223
+ debrief
224
+ else
225
+ @m.synchronize do
226
+ @tasks.all?(&:success)
227
+ end
228
+ end
229
+ end
230
+
231
+ # Debriefs failed tasks is +auto_debrief+ is true
232
+ #
233
+ def debrief
234
+ @m.synchronize do
235
+ @tasks.each do |task|
236
+ next if task.success
237
+
238
+ e = task.exception
239
+ out = task.stdout
240
+ err = task.stderr
241
+
242
+ CLI::UI::Frame.open('Task Failed: ' + task.title, color: :red, timing: Time.new - @start) do
243
+ if e
244
+ puts "#{e.class}: #{e.message}"
245
+ puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
246
+ end
247
+
248
+ CLI::UI::Frame.divider('STDOUT')
249
+ out = '(empty)' if out.nil? || out.strip.empty?
250
+ puts out
251
+
252
+ CLI::UI::Frame.divider('STDERR')
253
+ err = '(empty)' if err.nil? || err.strip.empty?
254
+ puts err
255
+ end
256
+ end
257
+ @tasks.all?(&:success)
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,232 @@
1
+ require 'cli/ui'
2
+ require 'stringio'
3
+
4
+ module CLI
5
+ module UI
6
+ module StdoutRouter
7
+ class << self
8
+ attr_accessor :duplicate_output_to
9
+ end
10
+
11
+ class Writer
12
+ def initialize(stream, name)
13
+ @stream = stream
14
+ @name = name
15
+ end
16
+
17
+ def write(*args)
18
+ args = args.map do |str|
19
+ if auto_frame_inset?
20
+ str = str.dup # unfreeze
21
+ str = str.force_encoding(Encoding::UTF_8)
22
+ apply_line_prefix(str, CLI::UI::Frame.prefix)
23
+ else
24
+ @pending_newline = false
25
+ str
26
+ end
27
+ end
28
+
29
+ # hook return of false suppresses output.
30
+ if (hook = Thread.current[:cliui_output_hook])
31
+ return if hook.call(args.map(&:to_s).join, @name) == false
32
+ end
33
+
34
+ @stream.write_without_cli_ui(*prepend_id(@stream, args))
35
+ if (dup = StdoutRouter.duplicate_output_to)
36
+ dup.write(*prepend_id(dup, args))
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def prepend_id(stream, args)
43
+ return args unless prepend_id_for_stream(stream)
44
+ args.map do |a|
45
+ next a if a.chomp.empty? # allow new lines to be new lines
46
+ "[#{Thread.current[:cliui_output_id][:id]}] #{a}"
47
+ end
48
+ end
49
+
50
+ def prepend_id_for_stream(stream)
51
+ return false unless Thread.current[:cliui_output_id]
52
+ return true if Thread.current[:cliui_output_id][:streams].include?(stream)
53
+ false
54
+ end
55
+
56
+ def auto_frame_inset?
57
+ !Thread.current[:no_cliui_frame_inset]
58
+ end
59
+
60
+ def apply_line_prefix(str, prefix)
61
+ return '' if str.empty?
62
+ prefixed = +''
63
+ str.force_encoding(Encoding::UTF_8).lines.each do |line|
64
+ if @pending_newline
65
+ prefixed << line
66
+ @pending_newline = false
67
+ else
68
+ prefixed << prefix << line
69
+ end
70
+ end
71
+ @pending_newline = !str.end_with?("\n")
72
+ prefixed
73
+ end
74
+ end
75
+
76
+ class Capture
77
+ @m = Mutex.new
78
+ @active_captures = 0
79
+ @saved_stdin = nil
80
+
81
+ def self.with_stdin_masked
82
+ @m.synchronize do
83
+ if @active_captures.zero?
84
+ @saved_stdin = $stdin
85
+ $stdin, w = IO.pipe
86
+ $stdin.close
87
+ w.close
88
+ end
89
+ @active_captures += 1
90
+ end
91
+
92
+ yield
93
+ ensure
94
+ @m.synchronize do
95
+ @active_captures -= 1
96
+ if @active_captures.zero?
97
+ $stdin = @saved_stdin
98
+ end
99
+ end
100
+ end
101
+
102
+ def initialize(*block_args, with_frame_inset: true, &block)
103
+ @with_frame_inset = with_frame_inset
104
+ @block_args = block_args
105
+ @block = block
106
+ end
107
+
108
+ attr_reader :stdout, :stderr
109
+
110
+ def run
111
+ require 'stringio'
112
+
113
+ StdoutRouter.assert_enabled!
114
+
115
+ out = StringIO.new
116
+ err = StringIO.new
117
+
118
+ prev_frame_inset = Thread.current[:no_cliui_frame_inset]
119
+ prev_hook = Thread.current[:cliui_output_hook]
120
+
121
+ if Thread.current.respond_to?(:report_on_exception)
122
+ Thread.current.report_on_exception = false
123
+ end
124
+
125
+ self.class.with_stdin_masked do
126
+ Thread.current[:no_cliui_frame_inset] = !@with_frame_inset
127
+ Thread.current[:cliui_output_hook] = ->(data, stream) do
128
+ case stream
129
+ when :stdout then out.write(data)
130
+ when :stderr then err.write(data)
131
+ else raise
132
+ end
133
+ false # suppress writing to terminal
134
+ end
135
+
136
+ begin
137
+ @block.call(*@block_args)
138
+ ensure
139
+ @stdout = out.string
140
+ @stderr = err.string
141
+ end
142
+ end
143
+ ensure
144
+ Thread.current[:cliui_output_hook] = prev_hook
145
+ Thread.current[:no_cliui_frame_inset] = prev_frame_inset
146
+ end
147
+ end
148
+
149
+ class << self
150
+ WRITE_WITHOUT_CLI_UI = :write_without_cli_ui
151
+
152
+ NotEnabled = Class.new(StandardError)
153
+
154
+ def with_id(on_streams:)
155
+ unless on_streams.is_a?(Array) && on_streams.all? { |s| s.respond_to?(:write) }
156
+ raise ArgumentError, <<~EOF
157
+ on_streams must be an array of objects that respond to `write`
158
+ These do not respond to write
159
+ #{on_streams.reject { |s| s.respond_to?(:write) }.map.with_index { |s| s.class.to_s }.join("\n")}
160
+ EOF
161
+ end
162
+
163
+ require 'securerandom'
164
+ id = format('%05d', rand(10**5))
165
+ Thread.current[:cliui_output_id] = {
166
+ id: id,
167
+ streams: on_streams,
168
+ }
169
+ yield(id)
170
+ ensure
171
+ Thread.current[:cliui_output_id] = nil
172
+ end
173
+
174
+ def current_id
175
+ Thread.current[:cliui_output_id]
176
+ end
177
+
178
+ def assert_enabled!
179
+ raise NotEnabled unless enabled?
180
+ end
181
+
182
+ def with_enabled
183
+ enable
184
+ yield
185
+ ensure
186
+ disable
187
+ end
188
+
189
+ # TODO: remove this
190
+ def ensure_activated
191
+ enable unless enabled?
192
+ end
193
+
194
+ def enable
195
+ return false if enabled?($stdout) || enabled?($stderr)
196
+ activate($stdout, :stdout)
197
+ activate($stderr, :stderr)
198
+ true
199
+ end
200
+
201
+ def enabled?(stream = $stdout)
202
+ stream.respond_to?(WRITE_WITHOUT_CLI_UI)
203
+ end
204
+
205
+ def disable
206
+ return false unless enabled?($stdout) && enabled?($stderr)
207
+ deactivate($stdout)
208
+ deactivate($stderr)
209
+ true
210
+ end
211
+
212
+ private
213
+
214
+ def deactivate(stream)
215
+ sc = stream.singleton_class
216
+ sc.send(:remove_method, :write)
217
+ sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI)
218
+ end
219
+
220
+ def activate(stream, streamname)
221
+ writer = StdoutRouter::Writer.new(stream, streamname)
222
+
223
+ raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI)
224
+ stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write)
225
+ stream.define_singleton_method(:write) do |*args|
226
+ writer.write(*args)
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end