console_pager.py
9.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Simple console pager."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
import sys
from fire.console import console_attr
class Pager(object):
"""A simple console text pager.
This pager requires the entire contents to be available. The contents are
written one page of lines at a time. The prompt is written after each page of
lines. A one character response is expected. See HELP_TEXT below for more
info.
The contents are written as is. For example, ANSI control codes will be in
effect. This is different from pagers like more(1) which is ANSI control code
agnostic and miscalculates line lengths, and less(1) which displays control
character names by default.
Attributes:
_attr: The current ConsoleAttr handle.
_clear: A string that clears the prompt when written to _out.
_contents: The entire contents of the text lines to page.
_height: The terminal height in characters.
_out: The output stream, log.out (effectively) if None.
_prompt: The page break prompt.
_search_direction: The search direction command, n:forward, N:reverse.
_search_pattern: The current forward/reverse search compiled RE.
_width: The termonal width in characters.
"""
HELP_TEXT = """
Simple pager commands:
b, ^B, <PAGE-UP>, <LEFT-ARROW>
Back one page.
f, ^F, <SPACE>, <PAGE-DOWN>, <RIGHT-ARROW>
Forward one page. Does not quit if there are no more lines.
g, <HOME>
Back to the first page.
<number>g
Go to <number> lines from the top.
G, <END>
Forward to the last page.
<number>G
Go to <number> lines from the bottom.
h
Print pager command help.
j, +, <DOWN-ARROW>
Forward one line.
k, -, <UP-ARROW>
Back one line.
/pattern
Forward search for pattern.
?pattern
Backward search for pattern.
n
Repeat current search.
N
Repeat current search in the opposite direction.
q, Q, ^C, ^D, ^Z
Quit return to the caller.
any other character
Prompt again.
Hit any key to continue:"""
PREV_POS_NXT_REPRINT = -1, -1
def __init__(self, contents, out=None, prompt=None):
"""Constructor.
Args:
contents: The entire contents of the text lines to page.
out: The output stream, log.out (effectively) if None.
prompt: The page break prompt, a defalt prompt is used if None..
"""
self._contents = contents
self._out = out or sys.stdout
self._search_pattern = None
self._search_direction = None
# prev_pos, prev_next values to force reprint
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
# Initialize the console attributes.
self._attr = console_attr.GetConsoleAttr()
self._width, self._height = self._attr.GetTermSize()
# Initialize the prompt and the prompt clear string.
if not prompt:
prompt = '{bold}--({{percent}}%)--{normal}'.format(
bold=self._attr.GetFontCode(bold=True),
normal=self._attr.GetFontCode())
self._clear = '\r{0}\r'.format(' ' * (self._attr.DisplayWidth(prompt) - 6))
self._prompt = prompt
# Initialize a list of lines with long lines split into separate display
# lines.
self._lines = []
for line in contents.splitlines():
self._lines += self._attr.SplitLine(line, self._width)
def _Write(self, s):
"""Mockable helper that writes s to self._out."""
self._out.write(s)
def _GetSearchCommand(self, c):
"""Consumes a search command and returns the equivalent pager command.
The search pattern is an RE that is pre-compiled and cached for subsequent
/<newline>, ?<newline>, n, or N commands.
Args:
c: The search command char.
Returns:
The pager command char.
"""
self._Write(c)
buf = ''
while True:
p = self._attr.GetRawKey()
if p in (None, '\n', '\r') or len(p) != 1:
break
self._Write(p)
buf += p
self._Write('\r' + ' ' * len(buf) + '\r')
if buf:
try:
self._search_pattern = re.compile(buf)
except re.error:
# Silently ignore pattern errors.
self._search_pattern = None
return ''
self._search_direction = 'n' if c == '/' else 'N'
return 'n'
def _Help(self):
"""Print command help and wait for any character to continue."""
clear = self._height - (len(self.HELP_TEXT) -
len(self.HELP_TEXT.replace('\n', '')))
if clear > 0:
self._Write('\n' * clear)
self._Write(self.HELP_TEXT)
self._attr.GetRawKey()
self._Write('\n')
def Run(self):
"""Run the pager."""
# No paging if the contents are small enough.
if len(self._lines) <= self._height:
self._Write(self._contents)
return
# We will not always reset previous values.
reset_prev_values = True
# Save room for the prompt at the bottom of the page.
self._height -= 1
# Loop over all the pages.
pos = 0
while pos < len(self._lines):
# Write a page of lines.
nxt = pos + self._height
if nxt > len(self._lines):
nxt = len(self._lines)
pos = nxt - self._height
# Checks if the starting position is in between the current printed lines
# so we don't need to reprint all the lines.
if self.prev_pos < pos < self.prev_nxt:
# we start where the previous page ended.
self._Write('\n'.join(self._lines[self.prev_nxt:nxt]) + '\n')
elif pos != self.prev_pos and nxt != self.prev_nxt:
self._Write('\n'.join(self._lines[pos:nxt]) + '\n')
# Handle the prompt response.
percent = self._prompt.format(percent=100 * nxt // len(self._lines))
digits = ''
while True:
# We want to reset prev values if we just exited out of the while loop
if reset_prev_values:
self.prev_pos, self.prev_nxt = pos, nxt
reset_prev_values = False
self._Write(percent)
c = self._attr.GetRawKey()
self._Write(self._clear)
# Parse the command.
if c in (None, # EOF.
'q', # Quit.
'Q', # Quit.
'\x03', # ^C (unix & windows terminal interrupt)
'\x1b', # ESC.
):
# Quit.
return
elif c in ('/', '?'):
c = self._GetSearchCommand(c)
elif c.isdigit():
# Collect digits for operation count.
digits += c
continue
# Set the optional command count.
if digits:
count = int(digits)
digits = ''
else:
count = 0
# Finally commit to command c.
if c in ('<PAGE-UP>', '<LEFT-ARROW>', 'b', '\x02'):
# Previous page.
nxt = pos - self._height
if nxt < 0:
nxt = 0
elif c in ('<PAGE-DOWN>', '<RIGHT-ARROW>', 'f', '\x06', ' '):
# Next page.
if nxt >= len(self._lines):
continue
nxt = pos + self._height
if nxt >= len(self._lines):
nxt = pos
elif c in ('<HOME>', 'g'):
# First page.
nxt = count - 1
if nxt > len(self._lines) - self._height:
nxt = len(self._lines) - self._height
if nxt < 0:
nxt = 0
elif c in ('<END>', 'G'):
# Last page.
nxt = len(self._lines) - count
if nxt > len(self._lines) - self._height:
nxt = len(self._lines) - self._height
if nxt < 0:
nxt = 0
elif c == 'h':
self._Help()
# Special case when we want to reprint the previous display.
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
nxt = pos
break
elif c in ('<DOWN-ARROW>', 'j', '+', '\n', '\r'):
# Next line.
if nxt >= len(self._lines):
continue
nxt = pos + 1
if nxt >= len(self._lines):
nxt = pos
elif c in ('<UP-ARROW>', 'k', '-'):
# Previous line.
nxt = pos - 1
if nxt < 0:
nxt = 0
elif c in ('n', 'N'):
# Next pattern match search.
if not self._search_pattern:
continue
nxt = pos
i = pos
direction = 1 if c == self._search_direction else -1
while True:
i += direction
if i < 0 or i >= len(self._lines):
break
if self._search_pattern.search(self._lines[i]):
nxt = i
break
else:
# Silently ignore everything else.
continue
if nxt != pos:
# We will exit the while loop because position changed so we can reset
# prev values.
reset_prev_values = True
break
pos = nxt