tty-pager 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ module Pager
5
+ class Abstract
6
+ UndefinedMethodError = Class.new(StandardError)
7
+
8
+ # Paginate content through null, basic or system pager.
9
+ #
10
+ # @param [String] text
11
+ # an optional blob of content
12
+ # @param [String] path
13
+ # a path to a file
14
+ #
15
+ # @api public
16
+ def self.page(text = nil, path: nil, **options, &block)
17
+ validate_arguments(text, path, block)
18
+
19
+ instance = new(**options)
20
+
21
+ begin
22
+ if block_given?
23
+ block.call(instance)
24
+ else
25
+ instance.page(text, path: path)
26
+ end
27
+ rescue PagerClosed
28
+ # do nothing
29
+ ensure
30
+ instance.close
31
+ end
32
+ end
33
+
34
+ # Disallow mixing input arguments
35
+ #
36
+ # @raise [IvalidArgument]
37
+ #
38
+ # @api private
39
+ def self.validate_arguments(text, path, block)
40
+ message = if !text.nil? && !block.nil?
41
+ "Cannot give text argument and block at the same time."
42
+ elsif !text.nil? && !path.nil?
43
+ "Cannot give text and :path arguments at the same time."
44
+ elsif !path.nil? && !block.nil?
45
+ "Cannot give :path argument and block at the same time."
46
+ end
47
+ raise(InvalidArgument, message) if message
48
+ end
49
+ private_class_method :validate_arguments
50
+
51
+ # Create a pager
52
+ #
53
+ # @param [IO] :input
54
+ # the object to send input to
55
+ # @param [IO] :output
56
+ # the object to send output to
57
+ # @param [Boolean] :enabled
58
+ # disable/enable text paging
59
+ #
60
+ # @api public
61
+ def initialize(input: $stdin, output: $stdout, enabled: true, **_options)
62
+ @input = input
63
+ @output = output
64
+ @enabled = enabled
65
+ end
66
+
67
+ # Check if pager is enabled
68
+ #
69
+ # @return [Boolean]
70
+ #
71
+ # @api public
72
+ def enabled?
73
+ !!@enabled
74
+ end
75
+
76
+ # Page text
77
+ #
78
+ # @example
79
+ # page('some long text...')
80
+ #
81
+ # @param [String] text
82
+ # the text to paginate
83
+ #
84
+ # @api public
85
+ def page(text = nil, path: nil)
86
+ if path
87
+ IO.foreach(path) do |line|
88
+ write(line)
89
+ end
90
+ else
91
+ write(text)
92
+ end
93
+ rescue PagerClosed
94
+ # do nothing
95
+ ensure
96
+ close
97
+ end
98
+
99
+ # Try writing to the pager. Return false if the pager was closed.
100
+ #
101
+ # In case of system pager, send text to
102
+ # the pager process. Start a new process if it hasn't been
103
+ # started yet.
104
+ #
105
+ # @param [Array<String>] *args
106
+ # strings to send to the pager
107
+ #
108
+ # @return [Boolean]
109
+ # the success status of writing to the pager process
110
+ #
111
+ # @api public
112
+ def try_write(*args)
113
+ write(*args)
114
+ true
115
+ rescue PagerClosed
116
+ false
117
+ end
118
+
119
+ def write(*)
120
+ raise UndefinedMethodError
121
+ end
122
+
123
+ def puts(*)
124
+ raise UndefinedMethodError
125
+ end
126
+
127
+ def close(*)
128
+ raise UndefinedMethodError
129
+ end
130
+
131
+ protected
132
+
133
+ attr_reader :output
134
+
135
+ attr_reader :input
136
+ end # Abstract
137
+ end # Pager
138
+ end # TTY
@@ -1,91 +1,234 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'strings'
3
+ require "io/console"
4
+ require "strings"
5
+ require "tty-screen"
6
+
7
+ require_relative "abstract"
4
8
 
5
9
  module TTY
6
- class Pager
10
+ module Pager
7
11
  # A basic pager is used to work on systems where
8
12
  # system pager is not supported.
9
13
  #
10
14
  # @api public
11
- class BasicPager < Pager
12
- PAGE_BREAK = "\n--- Page -%s- " \
13
- "Press enter/return to continue " \
15
+ class BasicPager < Abstract
16
+ PAGE_BREAK = "\n--- Page -%<page>s- Press enter/return to continue " \
14
17
  "(or q to quit) ---"
15
18
 
19
+ # Default prompt for paging
20
+ #
21
+ # @return [Proc]
22
+ #
23
+ # @api private
24
+ DEFAULT_PROMPT = ->(page) { format(PAGE_BREAK, page: page) }
25
+
16
26
  # Create a basic pager
17
27
  #
18
- # @option options [Integer] :height
28
+ # @param [Integer] :height
19
29
  # the terminal height
20
- # @option options [Integer] :width
30
+ # @param [Integer] :width
21
31
  # the terminal width
32
+ # @param [Proc] :prompt
33
+ # a proc object that accepts page number
22
34
  #
23
35
  # @api public
24
- def initialize(**options)
25
- super
26
- @height = options.fetch(:height) { page_height }
27
- @width = options.fetch(:width) { page_width }
28
- @prompt = options.fetch(:prompt) { default_prompt }
29
- prompt_height = PAGE_BREAK.lines.to_a.size
30
- @height -= prompt_height
36
+ def initialize(height: TTY::Screen.height, width: TTY::Screen.width,
37
+ prompt: DEFAULT_PROMPT, **options)
38
+ super(**options)
39
+ @width = width
40
+ @prompt = prompt
41
+ prompt_height = Strings.wrap(prompt.call(100).to_s, width).lines.count
42
+ @page_cursor = PageCursor.new(height - prompt_height)
43
+
44
+ reset
31
45
  end
32
46
 
33
- # Default prompt for paging
47
+ # Write text to the pager, prompting on page end.
34
48
  #
35
- # @return [Proc]
49
+ # @raise [PagerClosed]
50
+ # if the pager was closed
36
51
  #
37
- # @api private
38
- def default_prompt
39
- proc { |page_num| output.puts Strings.wrap(PAGE_BREAK % page_num, @width) }
52
+ # @return [TTY::Pager::BasicPager]
53
+ #
54
+ # @api public
55
+ def write(*args)
56
+ args.each do |text|
57
+ send_text(:write, text)
58
+ end
59
+ self
60
+ end
61
+ alias << write
62
+
63
+ # Print a line of text to the pager, prompting on page end.
64
+ #
65
+ # @raise [PagerClosed]
66
+ # if the pager was closed
67
+ #
68
+ # @api public
69
+ def puts(text)
70
+ send_text(:puts, text)
40
71
  end
41
72
 
42
- # Page text
73
+ # Stop the pager, wait for it to clean up
43
74
  #
44
75
  # @api public
45
- def page(text, &callback)
46
- page_num = 1
47
- leftover = []
48
- lines_left = @height
76
+ def close
77
+ reset
78
+ true
79
+ end
80
+
81
+ private
82
+
83
+ # Reset internal state
84
+ #
85
+ # @api private
86
+ def reset
87
+ @page_cursor.reset
88
+ @leftover = []
89
+ end
49
90
 
91
+ # Tracks page cursor
92
+ #
93
+ # @api private
94
+ class PageCursor
95
+ attr_reader :page_num
96
+
97
+ def initialize(height)
98
+ @height = height
99
+ reset
100
+ end
101
+
102
+ def reset
103
+ @page_num = 1
104
+ @lines_left = @height
105
+ end
106
+
107
+ # Move cursor to the next page
108
+ #
109
+ # @api public
110
+ def next_page
111
+ @page_num += 1
112
+ @lines_left = @height
113
+ end
114
+
115
+ # Move coursor down the page by count
116
+ #
117
+ # @param [Integer] count
118
+ #
119
+ # @api public
120
+ def down_by(count)
121
+ @lines_left -= count
122
+ end
123
+
124
+ # Check if time to break a page
125
+ #
126
+ # @return [Boolean]
127
+ #
128
+ # @api private
129
+ def page_break?
130
+ @lines_left.zero?
131
+ end
132
+ end
133
+
134
+ # The lower-level common implementation of printing methods
135
+ #
136
+ # @return [Boolean]
137
+ # the success status of writing to the screen
138
+ #
139
+ # @api private
140
+ def send_text(write_method, text)
50
141
  text.lines.each do |line|
51
- chunk = []
52
- if !leftover.empty?
53
- chunk = leftover
54
- leftover = []
55
- end
56
- wrapped_line = Strings.wrap(line, @width)
57
- wrapped_line.lines.each do |line_part|
58
- if lines_left > 0
59
- chunk << line_part
60
- lines_left -= 1
61
- else
62
- leftover << line_part
63
- end
64
- end
65
- output.print(chunk.join)
66
-
67
- if lines_left == 0
68
- break unless continue_paging?(page_num)
69
- lines_left = @height
70
- if leftover.size > 0
71
- lines_left -= leftover.size
72
- end
73
- page_num += 1
74
- return !callback.call(page_num) unless callback.nil?
142
+ chunk = create_chunk_from(line)
143
+
144
+ output.public_send(write_method, chunk)
145
+
146
+ next unless @page_cursor.page_break?
147
+
148
+ output.puts(page_break_prompt)
149
+
150
+ continue_paging?(input)
151
+
152
+ next_page
153
+ end
154
+
155
+ if !remaining_content.empty?
156
+ output.public_send(write_method, remaining_content)
157
+ end
158
+ end
159
+
160
+ # Convert line to a chunk of text to fit display
161
+ #
162
+ # @param [String] line
163
+ #
164
+ # @return [String]
165
+ #
166
+ # @api private
167
+ def create_chunk_from(line)
168
+ chunk = []
169
+
170
+ if !@leftover.empty?
171
+ chunk.concat(@leftover)
172
+ @leftover.clear
173
+ end
174
+
175
+ Strings.wrap(line, @width).lines.each do |line_part|
176
+ if !@page_cursor.page_break?
177
+ chunk << line_part
178
+ @page_cursor.down_by(1)
179
+ else
180
+ @leftover << line_part
75
181
  end
76
182
  end
77
183
 
78
- if leftover.size > 0
79
- output.print(leftover.join)
184
+ chunk.join
185
+ end
186
+
187
+ # Any remaining content
188
+ #
189
+ # @return [String]
190
+ #
191
+ # @api private
192
+ def remaining_content
193
+ @leftover.join
194
+ end
195
+
196
+ # Switch over to the next page
197
+ #
198
+ # @api private
199
+ def next_page
200
+ @page_cursor.next_page
201
+ if @leftover.size > 0
202
+ @page_cursor.down_by(@leftover.size)
80
203
  end
81
204
  end
82
205
 
83
- private
206
+ # Dispaly prompt at page break
207
+ #
208
+ # @api private
209
+ def page_break_prompt
210
+ Strings.wrap(@prompt.call(@page_cursor.page_num), @width)
211
+ end
212
+
213
+ # Check if paging should be continued
214
+ #
215
+ # @param [Integer] page
216
+ # the page number
217
+ #
218
+ # @return [Boolean]
219
+ #
220
+ # @api private
221
+ def continue_paging?(input)
222
+ if getchar.chomp[/q/i]
223
+ raise PagerClosed.new("The pager tool was closed")
224
+ end
225
+ end
84
226
 
227
+ # Find available character reading method
228
+ #
85
229
  # @api private
86
- def continue_paging?(page_num)
87
- instance_exec(page_num, &@prompt)
88
- !@input.gets.chomp[/q/i]
230
+ def getchar
231
+ input.respond_to?(:getch) ? input.getch : input.getc
89
232
  end
90
233
  end # BasicPager
91
234
  end # Pager
@@ -1,16 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "abstract"
4
+
3
5
  module TTY
4
- class Pager
5
- class NullPager < Pager
6
+ module Pager
7
+ class NullPager < Abstract
8
+ # Pass output directly to stdout
9
+ #
10
+ # @api public
11
+ def write(text)
12
+ return text unless output.tty?
13
+
14
+ output.write(text)
15
+ end
16
+ alias << write
17
+
6
18
  # Pass output directly to stdout
7
19
  #
8
20
  # @api public
9
- def page(text, &callback)
21
+ def puts(text)
10
22
  return text unless output.tty?
11
23
 
12
24
  output.puts(text)
13
25
  end
26
+
27
+ # Do nothing, always return success
28
+ #
29
+ # @api public
30
+ def close
31
+ true
32
+ end
14
33
  end
15
34
  end # Pager
16
35
  end # TTY