summaryrefslogtreecommitdiff
path: root/lib/pry/pager.rb
blob: 7296510a598841b0210bb64cb80d1cc0d73c6614 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# 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 ||= Pry::Terminal.height
      end

      def width
        @width ||= Pry::Terminal.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 "<page break> --- Press enter to continue " \
                     "( q<enter> to break ) --- <page 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 = 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