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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +81 -10
- data/README.md +247 -29
- data/lib/tty-pager.rb +1 -3
- data/lib/tty/pager.rb +82 -83
- data/lib/tty/pager/abstract.rb +138 -0
- data/lib/tty/pager/basic.rb +198 -56
- data/lib/tty/pager/null.rb +22 -4
- data/lib/tty/pager/system.rb +185 -73
- data/lib/tty/pager/version.rb +3 -3
- metadata +31 -89
- data/.gitignore +0 -22
- data/.rspec +0 -3
- data/.travis.yml +0 -25
- data/CODE_OF_CONDUCT.md +0 -49
- data/Gemfile +0 -14
- data/Rakefile +0 -8
- data/appveyor.yml +0 -25
- data/examples/basic_pager.rb +0 -7
- data/examples/system_pager.rb +0 -9
- data/examples/temp.txt +0 -49
- data/spec/spec_helper.rb +0 -45
- data/spec/unit/basic/page_spec.rb +0 -142
- data/spec/unit/null/page_spec.rb +0 -23
- data/spec/unit/page_spec.rb +0 -40
- data/spec/unit/system/available_spec.rb +0 -50
- data/spec/unit/system/command_exists_spec.rb +0 -15
- data/spec/unit/system/new_spec.rb +0 -10
- 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 -28
data/lib/tty-pager.rb
CHANGED
data/lib/tty/pager.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
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
|
-
|
9
|
+
module Pager
|
12
10
|
Error = Class.new(StandardError)
|
13
11
|
|
14
|
-
#
|
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
|
-
|
33
|
-
|
34
|
-
end
|
35
|
-
end
|
15
|
+
# Raised when user provides unnexpected argument
|
16
|
+
InvalidArgument = Class.new(Error)
|
36
17
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
76
|
-
|
77
|
-
attr_reader :output
|
78
|
-
|
79
|
-
attr_reader :input
|
97
|
+
extend ClassMethods
|
80
98
|
|
81
|
-
|
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
|
data/lib/tty/pager/basic.rb
CHANGED
@@ -1,92 +1,234 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
|
-
require
|
3
|
+
require "io/console"
|
4
|
+
require "strings"
|
5
|
+
require "tty-screen"
|
6
|
+
|
7
|
+
require_relative "abstract"
|
5
8
|
|
6
9
|
module TTY
|
7
|
-
|
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 <
|
13
|
-
PAGE_BREAK = "\n--- Page
|
14
|
-
"
|
15
|
-
|
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
|
-
# @
|
28
|
+
# @param [Integer] :height
|
20
29
|
# the terminal height
|
21
|
-
# @
|
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(
|
26
|
-
|
27
|
-
|
28
|
-
@width =
|
29
|
-
@prompt =
|
30
|
-
prompt_height =
|
31
|
-
@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
|
-
#
|
47
|
+
# Write text to the pager, prompting on page end.
|
35
48
|
#
|
36
|
-
# @
|
49
|
+
# @raise [PagerClosed]
|
50
|
+
# if the pager was closed
|
37
51
|
#
|
38
|
-
# @
|
39
|
-
|
40
|
-
|
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
|
-
#
|
73
|
+
# Stop the pager, wait for it to clean up
|
44
74
|
#
|
45
75
|
# @api public
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
output.
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
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
|
88
|
-
|
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
|