rfix 2.0.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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