tty-pager 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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