# -*- 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, , Back one page. f, ^F, , , Forward one page. Does not quit if there are no more lines. g, Back to the first page. g Go to lines from the top. G, Forward to the last page. G Go to lines from the bottom. h Print pager command help. j, +, Forward one line. k, -, 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 /, ?, 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 ('', '', 'b', '\x02'): # Previous page. nxt = pos - self._height if nxt < 0: nxt = 0 elif c in ('', '', 'f', '\x06', ' '): # Next page. if nxt >= len(self._lines): continue nxt = pos + self._height if nxt >= len(self._lines): nxt = pos elif c in ('', '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 ('', '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 ('', 'j', '+', '\n', '\r'): # Next line. if nxt >= len(self._lines): continue nxt = pos + 1 if nxt >= len(self._lines): nxt = pos elif c in ('', '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