# frozen_string_literal: true # A pager is an `IO`-like object that accepts text and either prints it # immediately, prints it one page at a time, or streams it to an external # program to print one page at a time. class Pry class Pager class StopPaging < StandardError end attr_reader :pry_instance def initialize(pry_instance) @pry_instance = pry_instance end # Send the given text through the best available pager (if # `Pry.config.pager` is enabled). If you want to send text through in # chunks as you generate it, use `open` to get a writable object # instead. # # @param [String] text # Text to run through a pager. # def page(text) open do |pager| pager << text end end # Yields a pager object (`NullPager`, `SimplePager`, or `SystemPager`). # All pagers accept output with `#puts`, `#print`, `#write`, and `#<<`. def open pager = best_available yield pager rescue StopPaging # rubocop:disable Lint/HandleExceptions ensure pager.close if pager end private def enabled? !!@enabled end attr_reader :output # Return an instance of the "best" available pager class -- # `SystemPager` if possible, `SimplePager` if `SystemPager` isn't # available, and `NullPager` if the user has disabled paging. All # pagers accept output with `#puts`, `#print`, `#write`, and `#<<`. You # must call `#close` when you're done writing output to a pager, and # you must rescue `Pry::Pager::StopPaging`. These requirements can be # avoided by using `.open` instead. def best_available if !pry_instance.config.pager NullPager.new(pry_instance.output) elsif !SystemPager.available? || Helpers::Platform.jruby? SimplePager.new(pry_instance.output) else SystemPager.new(pry_instance.output) end end # `NullPager` is a "pager" that actually just prints all output as it # comes in. Used when `Pry.config.pager` is false. class NullPager def initialize(out) @out = out end def puts(str) print "#{str.chomp}\n" end def print(str) write str end alias << print def write(str) @out.write str end def close; end private def height @height ||= @out.height end def width @width ||= @out.width end end # `SimplePager` is a straightforward pure-Ruby pager. We use it on # JRuby and when we can't find a usable external pager. class SimplePager < NullPager def initialize(*) super @tracker = PageTracker.new(height - 3, width) end def write(str) str.lines.each do |line| @out.print line @tracker.record line next unless @tracker.page? @out.print "\n" @out.print "\e[0m" @out.print " --- Press enter to continue " \ "( q to break ) --- \n" raise StopPaging if Readline.readline("").chomp == "q" @tracker.reset end end end # `SystemPager` buffers output until we're pretty sure it's at least a # page long, then invokes an external pager and starts streaming output # to it. If `#close` is called before then, it just prints out the # buffered content. class SystemPager < NullPager def self.default_pager pager = Pry::Env['PAGER'] || '' # Default to less, and make sure less is being passed the correct # options pager = "less -R -F -X" if pager.strip.empty? || pager =~ /^less\b/ pager end @system_pager = nil def self.available? if @system_pager.nil? @system_pager = begin pager_executable = default_pager.split(' ').first if Helpers::Platform.windows? || Helpers::Platform.windows_ansi? `where /Q #{pager_executable}` else `which #{pager_executable}` end $CHILD_STATUS.success? rescue StandardError false end else @system_pager end end def initialize(*) super @tracker = PageTracker.new(height, width) @buffer = "" @pager = nil end def write(str) if invoked_pager? write_to_pager str else @tracker.record str @buffer += str write_to_pager @buffer if @tracker.page? end rescue Errno::EPIPE raise StopPaging end def close if invoked_pager? pager.close else @out.puts @buffer end end private def write_to_pager(text) pager.write @out.decolorize_maybe(text) end def invoked_pager? @pager end def pager @pager ||= IO.popen(self.class.default_pager, 'w') end end # `PageTracker` tracks output to determine whether it's likely to take # up a whole page. This doesn't need to be super precise, but we can # use it for `SimplePager` and to avoid invoking the system pager # unnecessarily. # # One simplifying assumption is that we don't need `#page?` to return # `true` on the basis of an incomplete line. Long lines should be # counted as multiple lines, but we don't have to transition from # `false` to `true` until we see a newline. class PageTracker def initialize(rows, cols) @rows = rows @cols = cols reset end def record(str) str.lines.each do |line| if line.end_with? "\n" @row += ((@col + line_length(line) - 1) / @cols) + 1 @col = 0 else @col += line_length(line) end end end def page? @row >= @rows end def reset @row = 0 @col = 0 end private # Approximation of the printable length of a given line, without the # newline and without ANSI color codes. def line_length(line) line.chomp.gsub(/\e\[[\d;]*m/, '').length end end end end