require 'pry/terminal' # A Pry::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::Pager class StopPaging < StandardError end # @param [String] text # A piece of text to run through a pager. # @param [Symbol?] pager_type # `:simple` -- Use the pure ruby pager. # `:system` -- Use the system pager (less) or the environment variable # $PAGER if set. # `nil` -- Infer what pager to use from the environment. What this # really means is that JRuby and systems that do not have # access to 'less' will run through the pure ruby pager. def self.page(text, pager_type = nil) pager = best_available($stdout, pager_type) pager << text ensure pager.close if pager end def self.best_available(output, pager_type = nil) case pager_type when nil no_pager = !SystemPager.available? if no_pager || Pry::Helpers::BaseHelpers.jruby? SimplePager.new(output) else SystemPager.new(output) end when :simple SimplePager.new(output) when :system SystemPager.new(output) else raise "'#{pager}' is not a recognized pager." end end 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 # no-op for base pager, but important for subclasses end private def height @height ||= Pry::Terminal.height! end def width @width ||= Pry::Terminal.width! end class SimplePager < Pry::Pager def initialize(*) super @tracker = PageTracker.new(height - 3, width) end def write(str) str.lines.each do |line| @out.write line @tracker.record line if @tracker.page? @out.puts "\n --- Press enter to continue " \ "( q to break ) --- " raise StopPaging if $stdin.gets.chomp == "q" @tracker.reset end 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 < Pry::Pager def self.default_pager pager = ENV["PAGER"] || "" # Default to less, and make sure less is being passed the correct options if pager.strip.empty? or pager =~ /^less\b/ pager = "less -R -S -F -X" end pager end def self.available? if @system_pager.nil? @system_pager = begin pager_executable = default_pager.split(' ').first `which #{ pager_executable }` rescue false end else @system_pager end end def initialize(*) super @tracker = PageTracker.new(height, width) @buffer = "" end def write(str) if invoked_pager? pager.write str else @tracker.record str @buffer << str if @tracker.page? pager.write @buffer end end rescue Errno::EPIPE end def close if invoked_pager? pager.close else @out.puts @buffer end end private 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, @cols = rows, 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