tty-pager 0.9.0 → 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.
@@ -1,3 +1 @@
1
- # coding: utf-8
2
-
3
- require_relative 'tty/pager'
1
+ require_relative "tty/pager"
@@ -1,6 +1,4 @@
1
- # coding: utf-8
2
-
3
- require 'tty-screen'
1
+ # frozen_string_literal: true
4
2
 
5
3
  require_relative "pager/basic"
6
4
  require_relative "pager/null"
@@ -8,95 +6,96 @@ require_relative "pager/system"
8
6
  require_relative "pager/version"
9
7
 
10
8
  module TTY
11
- class Pager
9
+ module Pager
12
10
  Error = Class.new(StandardError)
13
11
 
14
- # Create a pager
15
- #
16
- # @param [Hash] options
17
- # @option options [Proc] :prompt
18
- # a proc object that accepts page number
19
- # @option options [IO] :input
20
- # the object to send input to
21
- # @option options [IO] :output
22
- # the object to send output to
23
- # @option options [Boolean] :enabled
24
- # disable/enable text paging
25
- #
26
- # @api public
27
- def initialize(options = {})
28
- @input = options.fetch(:input) { $stdin }
29
- @output = options.fetch(:output) { $stdout }
30
- @enabled = options.fetch(:enabled) { true }
12
+ # Raised when pager is closed
13
+ PagerClosed = Class.new(Error)
31
14
 
32
- if self.class == TTY::Pager
33
- @pager = find_available(options)
34
- end
35
- end
15
+ # Raised when user provides unnexpected argument
16
+ InvalidArgument = Class.new(Error)
36
17
 
37
- # Check if pager is enabled
38
- #
39
- # @return [Boolean]
40
- #
41
- # @api public
42
- def enabled?
43
- !!@enabled
44
- end
18
+ module ClassMethods
19
+ # Create a pager
20
+ #
21
+ # @param [Boolean] :enabled
22
+ # disable/enable text paging
23
+ # @param [String] :command
24
+ # the paging command
25
+ # @param [IO] :input
26
+ # the object to send input to
27
+ # @param [IO] :output
28
+ # the object to send output to
29
+ # @param [Proc] :prompt
30
+ # a proc object that accepts page number
31
+ # @param [Integer] :width
32
+ # the terminal width
33
+ # @param [Integer] :height
34
+ # the terminal height
35
+ #
36
+ # @api public
37
+ def new(enabled: true, command: nil, **options)
38
+ select_pager(enabled: enabled, command: command).new(
39
+ enabled: enabled, command: command, **options)
40
+ end
45
41
 
46
- # Page the given text through the available pager
47
- #
48
- # @param [String] text
49
- # the text to run through a pager
50
- #
51
- # @yield [Integer] page number
52
- #
53
- # @return [TTY::Pager]
54
- #
55
- # @api public
56
- def page(text, &callback)
57
- pager.page(text, &callback)
58
- self
59
- end
42
+ # Paginate content through null, basic or system pager.
43
+ #
44
+ # @example
45
+ # TTY::Pager.page do |pager|
46
+ # pager.write "some text"
47
+ # end
48
+ #
49
+ # @param [String] :text
50
+ # an optional blob of content
51
+ # @param [String] :path
52
+ # a path to a file
53
+ # @param [Boolean] :enabled
54
+ # whether or not to use null pager
55
+ # @param [String] :command
56
+ # the paging command
57
+ # @param [IO] :input
58
+ # the object to send input to
59
+ # @param [IO] :output
60
+ # the object to send output to
61
+ #
62
+ # @api public
63
+ def page(text = nil, path: nil, enabled: true, command: nil,
64
+ **options, &block)
65
+ select_pager(enabled: enabled, command: command).
66
+ page(text, path: path, enabled: enabled, command: command,
67
+ **options, &block)
68
+ end
60
69
 
61
- # The terminal height
62
- #
63
- # @api public
64
- def page_height
65
- TTY::Screen.height
66
- end
70
+ # Select an appriopriate pager
71
+ #
72
+ # If the user disabled paging then a NullPager is returned,
73
+ # otherwise a check is performed to find native system
74
+ # command to perform pagination with SystemPager. Finally,
75
+ # if no system command is found, a BasicPager is used which
76
+ # is a pure Ruby implementation known to work on any platform.
77
+ #
78
+ # @param [Boolean] enabled
79
+ # whether or not to allow paging
80
+ # @param [String] command
81
+ # the command to run if available
82
+ #
83
+ # @api private
84
+ def select_pager(enabled: true, command: nil)
85
+ commands = Array(command)
67
86
 
68
- # The terminal width
69
- #
70
- # @api public
71
- def page_width
72
- TTY::Screen.width
87
+ if !enabled
88
+ NullPager
89
+ elsif SystemPager.exec_available?(*commands)
90
+ SystemPager
91
+ else
92
+ BasicPager
93
+ end
94
+ end
73
95
  end
74
96
 
75
- protected
76
-
77
- attr_reader :output
78
-
79
- attr_reader :input
97
+ extend ClassMethods
80
98
 
81
- attr_reader :pager
82
-
83
- # Find available pager
84
- #
85
- # If the user disabled paging then a NullPager is returned,
86
- # otherwise a check is performed to find native system
87
- # command to perform pagination with SystemPager. Finally,
88
- # if no system command is found, a BasicPager is used which
89
- # is a pure Ruby implementation known to work on any platform.
90
- #
91
- # @api private
92
- def find_available(options)
93
- if !enabled?
94
- NullPager.new
95
- elsif SystemPager.available?
96
- SystemPager.new(options)
97
- else
98
- BasicPager.new(options)
99
- end
100
- end
99
+ private_class_method :select_pager
101
100
  end # Pager
102
101
  end # TTY
@@ -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,92 +1,234 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- require 'verse'
3
+ require "io/console"
4
+ require "strings"
5
+ require "tty-screen"
6
+
7
+ require_relative "abstract"
5
8
 
6
9
  module TTY
7
- class Pager
10
+ module Pager
8
11
  # A basic pager is used to work on systems where
9
12
  # system pager is not supported.
10
13
  #
11
14
  # @api public
12
- class BasicPager < Pager
13
- PAGE_BREAK = "\n--- Page -%s- " \
14
- "Press enter/return to continue " \
15
- "(or q to quit) ---".freeze
15
+ class BasicPager < Abstract
16
+ PAGE_BREAK = "\n--- Page -%<page>s- Press enter/return to continue " \
17
+ "(or q to quit) ---"
18
+
19
+ # Default prompt for paging
20
+ #
21
+ # @return [Proc]
22
+ #
23
+ # @api private
24
+ DEFAULT_PROMPT = ->(page) { format(PAGE_BREAK, page: page) }
16
25
 
17
26
  # Create a basic pager
18
27
  #
19
- # @option options [Integer] :height
28
+ # @param [Integer] :height
20
29
  # the terminal height
21
- # @option options [Integer] :width
30
+ # @param [Integer] :width
22
31
  # the terminal width
32
+ # @param [Proc] :prompt
33
+ # a proc object that accepts page number
23
34
  #
24
35
  # @api public
25
- def initialize(options = {})
26
- super
27
- @height = options.fetch(:height) { page_height }
28
- @width = options.fetch(:width) { page_width }
29
- @prompt = options.fetch(:prompt) { default_prompt }
30
- prompt_height = PAGE_BREAK.lines.to_a.size
31
- @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
32
45
  end
33
46
 
34
- # Default prompt for paging
47
+ # Write text to the pager, prompting on page end.
35
48
  #
36
- # @return [Proc]
49
+ # @raise [PagerClosed]
50
+ # if the pager was closed
37
51
  #
38
- # @api private
39
- def default_prompt
40
- proc { |page_num| output.puts Verse.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)
41
71
  end
42
72
 
43
- # Page text
73
+ # Stop the pager, wait for it to clean up
44
74
  #
45
75
  # @api public
46
- def page(text, &callback)
47
- page_num = 1
48
- leftover = []
49
- 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
50
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)
51
141
  text.lines.each do |line|
52
- chunk = []
53
- if !leftover.empty?
54
- chunk = leftover
55
- leftover = []
56
- end
57
- wrapped_line = Verse.wrap(line, @width)
58
- wrapped_line.lines.each do |line_part|
59
- if lines_left > 0
60
- chunk << line_part
61
- lines_left -= 1
62
- else
63
- leftover << line_part
64
- end
65
- end
66
- output.print(chunk.join)
67
-
68
- if lines_left == 0
69
- break unless continue_paging?(page_num)
70
- lines_left = @height
71
- if leftover.size > 0
72
- lines_left -= leftover.size
73
- end
74
- page_num += 1
75
- 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
76
181
  end
77
182
  end
78
183
 
79
- if leftover.size > 0
80
- 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)
81
203
  end
82
204
  end
83
205
 
84
- 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
85
226
 
227
+ # Find available character reading method
228
+ #
86
229
  # @api private
87
- def continue_paging?(page_num)
88
- instance_exec(page_num, &@prompt)
89
- !@input.gets.chomp[/q/i]
230
+ def getchar
231
+ input.respond_to?(:getch) ? input.getch : input.getc
90
232
  end
91
233
  end # BasicPager
92
234
  end # Pager