lib/cli/ui/stdout_router.rb in cli-ui-2.1.0 vs lib/cli/ui/stdout_router.rb in cli-ui-2.2.0
- old
+ new
@@ -1,9 +1,10 @@
# typed: true
require 'cli/ui'
require 'stringio'
+require_relative '../../../vendor/reentrant_mutex'
module CLI
module UI
module StdoutRouter
class Writer
@@ -33,11 +34,15 @@
return if hook.call(args.map(&:to_s).join, @name) == false
end
T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, args))
if (dup = StdoutRouter.duplicate_output_to)
- T.unsafe(dup).write(*prepend_id(dup, args))
+ begin
+ T.unsafe(dup).write(*prepend_id(dup, args))
+ rescue IOError
+ # Ignore
+ end
end
end
private
@@ -84,61 +89,134 @@
end
class Capture
extend T::Sig
- @m = Mutex.new
+ @capture_mutex = Mutex.new
+ @stdin_mutex = ReentrantMutex.new
@active_captures = 0
@saved_stdin = nil
class << self
extend T::Sig
+ sig { returns(T.nilable(Capture)) }
+ def current_capture
+ Thread.current[:cliui_current_capture]
+ end
+
+ sig { returns(Capture) }
+ def current_capture!
+ T.must(current_capture)
+ end
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
+ def in_alternate_screen(&block)
+ stdin_synchronize do
+ previous_print_captured_output = current_capture&.print_captured_output
+ current_capture&.print_captured_output = true
+ Spinner::SpinGroup.pause_spinners do
+ if outermost_uncaptured?
+ begin
+ prev_hook = Thread.current[:cliui_output_hook]
+ Thread.current[:cliui_output_hook] = nil
+ replay = current_capture!.stdout.gsub(ANSI.match_alternate_screen, '')
+ CLI::UI.raw do
+ print("#{ANSI.enter_alternate_screen}#{replay}")
+ end
+ ensure
+ Thread.current[:cliui_output_hook] = prev_hook
+ end
+ end
+ block.call
+ ensure
+ print(ANSI.exit_alternate_screen) if outermost_uncaptured?
+ end
+ ensure
+ current_capture&.print_captured_output = !!previous_print_captured_output
+ end
+ end
+
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
+ def stdin_synchronize(&block)
+ @stdin_mutex.synchronize do
+ case $stdin
+ when BlockingInput
+ $stdin.synchronize do
+ block.call
+ end
+ else
+ block.call
+ end
+ end
+ end
+
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
def with_stdin_masked(&block)
- @m.synchronize do
+ @capture_mutex.synchronize do
if @active_captures.zero?
- @saved_stdin = $stdin
- $stdin, w = IO.pipe
- $stdin.close
- w.close
+ @stdin_mutex.synchronize do
+ @saved_stdin = $stdin
+ $stdin = BlockingInput.new(@saved_stdin)
+ end
end
@active_captures += 1
end
yield
ensure
- @m.synchronize do
+ @capture_mutex.synchronize do
@active_captures -= 1
if @active_captures.zero?
- $stdin = @saved_stdin
+ @stdin_mutex.synchronize do
+ $stdin = @saved_stdin
+ end
end
end
end
+
+ private
+
+ sig { returns(T::Boolean) }
+ def outermost_uncaptured?
+ @stdin_mutex.count == 1 && $stdin.is_a?(BlockingInput)
+ end
end
sig do
- params(with_frame_inset: T::Boolean, block: T.proc.void).void
+ params(
+ with_frame_inset: T::Boolean,
+ merged_output: T::Boolean,
+ duplicate_output_to: IO,
+ block: T.proc.void,
+ ).void
end
- def initialize(with_frame_inset: true, &block)
+ def initialize(
+ with_frame_inset: true,
+ merged_output: false,
+ duplicate_output_to: File.open(File::NULL, 'w'),
+ &block
+ )
@with_frame_inset = with_frame_inset
+ @merged_output = merged_output
+ @duplicate_output_to = duplicate_output_to
@block = block
- @stdout = ''
- @stderr = ''
+ @print_captured_output = false
+ @out = StringIO.new
+ @err = StringIO.new
end
- sig { returns(String) }
- attr_reader :stdout, :stderr
+ sig { returns(T::Boolean) }
+ attr_accessor :print_captured_output
sig { returns(T.untyped) }
def run
require 'stringio'
StdoutRouter.assert_enabled!
- out = StringIO.new
- err = StringIO.new
+ Thread.current[:cliui_current_capture] = self
prev_frame_inset = Thread.current[:no_cliui_frame_inset]
prev_hook = Thread.current[:cliui_output_hook]
if Thread.current.respond_to?(:report_on_exception)
@@ -146,27 +224,93 @@
end
self.class.with_stdin_masked do
Thread.current[:no_cliui_frame_inset] = !@with_frame_inset
Thread.current[:cliui_output_hook] = ->(data, stream) do
+ stream = :stdout if @merged_output
case stream
- when :stdout then out.write(data)
- when :stderr then err.write(data)
+ when :stdout
+ @out.write(data)
+ @duplicate_output_to.write(data)
+ when :stderr
+ @err.write(data)
else raise
end
- false # suppress writing to terminal
+ print_captured_output # suppress writing to terminal by default
end
- begin
- @block.call
- ensure
- @stdout = out.string
- @stderr = err.string
- end
+ @block.call
end
ensure
Thread.current[:cliui_output_hook] = prev_hook
Thread.current[:no_cliui_frame_inset] = prev_frame_inset
+ Thread.current[:cliui_current_capture] = nil
+ end
+
+ sig { returns(String) }
+ def stdout
+ @out.string
+ end
+
+ sig { returns(String) }
+ def stderr
+ @err.string
+ end
+
+ class BlockingInput
+ extend T::Sig
+
+ sig { params(stream: IO).void }
+ def initialize(stream)
+ @stream = stream
+ @m = ReentrantMutex.new
+ end
+
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
+ def synchronize(&block)
+ @m.synchronize do
+ previous_allowed_to_read = Thread.current[:cliui_allowed_to_read]
+ Thread.current[:cliui_allowed_to_read] = true
+ block.call
+ ensure
+ Thread.current[:cliui_allowed_to_read] = previous_allowed_to_read
+ end
+ end
+
+ READING_METHODS = [
+ :each,
+ :each_byte,
+ :each_char,
+ :each_codepoint,
+ :each_line,
+ :getbyte,
+ :getc,
+ :getch,
+ :gets,
+ :read,
+ :read_nonblock,
+ :readbyte,
+ :readchar,
+ :readline,
+ :readlines,
+ :readpartial,
+ ]
+
+ NON_READING_METHODS = IO.instance_methods(false) - READING_METHODS
+
+ READING_METHODS.each do |method|
+ define_method(method) do |*args, **kwargs, &block|
+ raise(IOError, 'closed stream') unless Thread.current[:cliui_allowed_to_read]
+
+ @stream.send(method, *args, **kwargs, &block)
+ end
+ end
+
+ NON_READING_METHODS.each do |method|
+ define_method(method) do |*args, **kwargs, &block|
+ @stream.send(method, *args, **kwargs, &block)
+ end
+ end
end
end
class << self
extend T::Sig