# typed: true require 'cli/ui' require 'stringio' module CLI module UI module StdoutRouter class Writer extend T::Sig sig { params(stream: IOLike, name: Symbol).void } def initialize(stream, name) @stream = stream @name = name end sig { params(args: String).void } def write(*args) args = args.map do |str| if auto_frame_inset? str = str.dup # unfreeze str = str.force_encoding(Encoding::UTF_8) apply_line_prefix(str, CLI::UI::Frame.prefix) else @pending_newline = false str end end # hook return of false suppresses output. if (hook = Thread.current[:cliui_output_hook]) 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)) end end private sig { params(stream: IOLike, args: T::Array[String]).returns(T::Array[String]) } def prepend_id(stream, args) return args unless prepend_id_for_stream(stream) args.map do |a| next a if a.chomp.empty? # allow new lines to be new lines "[#{Thread.current[:cliui_output_id][:id]}] #{a}" end end sig { params(stream: IOLike).returns(T::Boolean) } def prepend_id_for_stream(stream) return false unless Thread.current[:cliui_output_id] return true if Thread.current[:cliui_output_id][:streams].include?(stream) false end sig { returns(T::Boolean) } def auto_frame_inset? !Thread.current[:no_cliui_frame_inset] end sig { params(str: String, prefix: String).returns(String) } def apply_line_prefix(str, prefix) return '' if str.empty? prefixed = +'' str.force_encoding(Encoding::UTF_8).lines.each do |line| if @pending_newline prefixed << line @pending_newline = false else prefixed << prefix << line end end @pending_newline = !str.end_with?("\n") prefixed end end class Capture extend T::Sig @m = Mutex.new @active_captures = 0 @saved_stdin = nil sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) } def self.with_stdin_masked(&block) @m.synchronize do if @active_captures.zero? @saved_stdin = $stdin $stdin, w = IO.pipe $stdin.close w.close end @active_captures += 1 end yield ensure @m.synchronize do @active_captures -= 1 if @active_captures.zero? $stdin = @saved_stdin end end end sig do params(with_frame_inset: T::Boolean, block: T.proc.void).void end def initialize(with_frame_inset: true, &block) @with_frame_inset = with_frame_inset @block = block @stdout = '' @stderr = '' end sig { returns(String) } attr_reader :stdout, :stderr sig { returns(T.untyped) } def run require 'stringio' StdoutRouter.assert_enabled! out = StringIO.new err = StringIO.new prev_frame_inset = Thread.current[:no_cliui_frame_inset] prev_hook = Thread.current[:cliui_output_hook] if Thread.current.respond_to?(:report_on_exception) Thread.current.report_on_exception = false end self.class.with_stdin_masked do Thread.current[:no_cliui_frame_inset] = !@with_frame_inset Thread.current[:cliui_output_hook] = ->(data, stream) do case stream when :stdout then out.write(data) when :stderr then err.write(data) else raise end false # suppress writing to terminal end begin @block.call ensure @stdout = out.string @stderr = err.string end end ensure Thread.current[:cliui_output_hook] = prev_hook Thread.current[:no_cliui_frame_inset] = prev_frame_inset end end class << self extend T::Sig WRITE_WITHOUT_CLI_UI = :write_without_cli_ui NotEnabled = Class.new(StandardError) sig { returns(T.nilable(IOLike)) } attr_accessor :duplicate_output_to sig do type_parameters(:T) .params(on_streams: T::Array[IOLike], block: T.proc.params(id: String).returns(T.type_parameter(:T))) .returns(T.type_parameter(:T)) end def with_id(on_streams:, &block) require 'securerandom' id = format('%05d', rand(10**5)) Thread.current[:cliui_output_id] = { id: id, streams: on_streams.map { |stream| T.cast(stream, IOLike) }, } yield(id) ensure Thread.current[:cliui_output_id] = nil end sig { returns(T.nilable(T::Hash[Symbol, T.any(String, IOLike)])) } def current_id Thread.current[:cliui_output_id] end sig { void } def assert_enabled! raise NotEnabled unless enabled? end sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) } def with_enabled(&block) enable yield ensure disable end # TODO: remove this sig { void } def ensure_activated enable unless enabled? end sig { returns(T::Boolean) } def enable return false if enabled?($stdout) || enabled?($stderr) activate($stdout, :stdout) activate($stderr, :stderr) true end sig { params(stream: IOLike).returns(T::Boolean) } def enabled?(stream = $stdout) stream.respond_to?(WRITE_WITHOUT_CLI_UI) end sig { returns(T::Boolean) } def disable return false unless enabled?($stdout) && enabled?($stderr) deactivate($stdout) deactivate($stderr) true end private sig { params(stream: IOLike).void } def deactivate(stream) sc = stream.singleton_class sc.send(:remove_method, :write) sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI) end sig { params(stream: IOLike, streamname: Symbol).void } def activate(stream, streamname) writer = StdoutRouter::Writer.new(stream, streamname) raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI) stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write) stream.define_singleton_method(:write) do |*args| writer.write(*args) end end end end end end