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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +233 -27
- data/lib/tty-pager.rb +1 -1
- data/lib/tty/pager.rb +84 -86
- data/lib/tty/pager/abstract.rb +138 -0
- data/lib/tty/pager/basic.rb +197 -54
- data/lib/tty/pager/null.rb +22 -3
- data/lib/tty/pager/system.rb +154 -67
- data/lib/tty/pager/version.rb +2 -2
- metadata +28 -76
- data/Rakefile +0 -8
- data/examples/basic_pager.rb +0 -5
- data/examples/markdown.rb +0 -7
- data/examples/pager.rb +0 -7
- data/examples/system_pager.rb +0 -7
- data/spec/spec_helper.rb +0 -45
- data/spec/unit/basic/page_spec.rb +0 -141
- data/spec/unit/null/page_spec.rb +0 -23
- data/spec/unit/page_spec.rb +0 -40
- data/spec/unit/system/command_exists_spec.rb +0 -15
- data/spec/unit/system/executables_spec.rb +0 -11
- data/spec/unit/system/find_executable_spec.rb +0 -50
- data/spec/unit/system/new_spec.rb +0 -21
- data/spec/unit/system/page_spec.rb +0 -21
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
- data/tty-pager.gemspec +0 -31
@@ -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
|
data/lib/tty/pager/basic.rb
CHANGED
@@ -1,91 +1,234 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "io/console"
|
4
|
+
require "strings"
|
5
|
+
require "tty-screen"
|
6
|
+
|
7
|
+
require_relative "abstract"
|
4
8
|
|
5
9
|
module TTY
|
6
|
-
|
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 <
|
12
|
-
PAGE_BREAK = "\n--- Page
|
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
|
-
# @
|
28
|
+
# @param [Integer] :height
|
19
29
|
# the terminal height
|
20
|
-
# @
|
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(
|
25
|
-
|
26
|
-
|
27
|
-
@width =
|
28
|
-
@prompt =
|
29
|
-
prompt_height =
|
30
|
-
@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
|
-
#
|
47
|
+
# Write text to the pager, prompting on page end.
|
34
48
|
#
|
35
|
-
# @
|
49
|
+
# @raise [PagerClosed]
|
50
|
+
# if the pager was closed
|
36
51
|
#
|
37
|
-
# @
|
38
|
-
|
39
|
-
|
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
|
-
#
|
73
|
+
# Stop the pager, wait for it to clean up
|
43
74
|
#
|
44
75
|
# @api public
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
output.
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
79
|
-
|
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
|
-
|
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
|
87
|
-
|
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
|
data/lib/tty/pager/null.rb
CHANGED
@@ -1,16 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "abstract"
|
4
|
+
|
3
5
|
module TTY
|
4
|
-
|
5
|
-
class NullPager <
|
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
|
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
|