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 default 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
|