# This file is part of ranger, the console file manager.
# License: GNU GPL version 3, see the file "AUTHORS" for details.
"""The pager displays text and allows you to scroll inside it."""
from __future__ import (absolute_import, division, print_function)
import curses
import logging
from ranger.gui import ansi
from ranger.ext.direction import Direction
from ranger.ext.img_display import ImgDisplayUnsupportedException
from . import Widget
LOG = logging.getLogger(__name__)
# TODO: Scrolling in embedded pager
class Pager(Widget): # pylint: disable=too-many-instance-attributes
source = None
source_is_stream = False
old_source = None
old_scroll_begin = 0
old_startx = 0
need_clear_image = False
need_redraw_image = False
max_width = None
def __init__(self, win, embedded=False):
Widget.__init__(self, win)
self.embedded = embedded
self.scroll_begin = 0
self.scroll_extra = 0
self.startx = 0
self.markup = None
self.lines = []
self.image = None
self.image_drawn = False
def _close_source(self):
if self.source and self.source_is_stream:
try:
self.source.close()
except OSError as ex:
LOG.error('Unable to close pager source')
LOG.exception(ex)
def open(self):
self.scroll_begin = 0
self.markup = None
self.max_width = 0
self.startx = 0
self.need_redraw = True
def clear_image(self, force=False):
if (force or self.need_clear_image) and self.image_drawn:
self.fm.image_displayer.clear(self.x, self.y, self.wid, self.hei)
self.need_clear_image = False
self.image_drawn = False
def close(self):
if self.image:
self.need_clear_image = True
self.clear_image()
self._close_source()
def destroy(self):
self.clear_image(force=True)
Widget.destroy(self)
def finalize(self):
self.fm.ui.win.move(self.y, self.x)
def scrollbit(self, lines):
target_scroll = self.scroll_extra + lines
max_scroll = len(self.lines) - self.hei
self.scroll_extra = max(0, min(target_scroll, max_scroll))
self.need_redraw = True
def draw(self):
if self.need_clear_image:
self.need_redraw = True
if self.old_source != self.source:
self.old_source = self.source
self.need_redraw = True
if self.old_scroll_begin != self.scroll_begin or \
self.old_startx != self.startx:
self.old_startx = self.startx
self.old_scroll_begin = self.scroll_begin
self.need_redraw = True
if self.need_redraw:
self.win.erase()
self.need_redraw_image = True
self.clear_image()
if not self.image:
scroll_pos = self.scroll_begin + self.scroll_extra
line_gen = self._generate_lines(
starty=scroll_pos, startx=self.startx)
for line, i in zip(line_gen, range(self.hei)):
self._draw_line(i, line)
self.need_redraw = False
def draw_image(self):
if self.image and self.need_redraw_image:
self.source = None
self.need_redraw_image = False
try:
self.fm.image_displayer.draw(self.image, self.x, self.y,
self.wid, self.hei)
except ImgDisplayUnsupportedException as ex:
self.fm.settings.preview_images = False
self.fm.notify(ex, bad=True)
except Exception as ex: # pylint: disable=broad-except
self.fm.notify(ex, bad=True)
else:
self.image_drawn = True
def _draw_line(self, i, line):
if self.markup is None:
self.addstr(i, 0, line)
elif self.markup == 'ansi':
try:
self.win.move(i, 0)
except curses.error:
pass
else:
for chunk in ansi.text_with_fg_bg_attr(line):
if isinstance(chunk, tuple):
self.set_fg_bg_attr(*chunk)
else:
self.addstr(chunk)
def move(self, narg=None, **kw):
direction = Direction(kw)
if direction.horizontal():
self.startx = direction.move(
direction=direction.right(),
override=narg,
maximum=self.max_width,
current=self.startx,
pagesize=self.wid,
offset=-self.wid + 1)
if direction.vertical():
movement = dict(
direction=direction.down(),
override=narg,
current=self.scroll_begin,
pagesize=self.hei,
offset=-self.hei + 1)
if self.source_is_stream:
# For streams, we first pretend that the content ends much later,
# in case there are still unread lines.
desired_position = direction.move(
maximum=len(self.lines) + 9999,
**movement)
# Then, read the new lines as needed to produce a more accurate
# maximum for the movement:
self._get_line(desired_position + self.hei)
self.scroll_begin = direction.move(
maximum=len(self.lines),
**movement)
def press(self, key):
self.fm.ui.keymaps.use_keymap('pager')
self.fm.ui.press(key)
def set_image(self, image):
if self.image:
self.need_clear_image = True
self.image = image
self._close_source()
self.source = None
self.source_is_stream = False
def set_source(self, source, strip=False):
if self.image:
self.image = None
self.need_clear_image = True
self._close_source()
self.max_width = 0
if isinstance(source, str):
self.source_is_stream = False
self.lines = source.splitlines()
if self.lines:
self.max_width = max(len(line) for line in self.lines)
elif hasattr(source, '__getitem__'):
self.source_is_stream = False
self.lines = source
if self.lines:
self.max_width = max(len(line) for line in source)
elif hasattr(source, 'readline'):
self.source_is_stream = True
self.lines = []
else:
self.source = None
self.source_is_stream = False
return False
self.markup = 'ansi'
if not self.source_is_stream and strip:
self.lines = [line.strip() for line in self.lines]
self.source = source
return True
def click(self, event):
n = 1 if event.ctrl() else 3
direction = event.mouse_wheel_direction()
if direction:
self.move(down=direction * n)
return True
def _get_line(self, n, attempt_to_read=True):
assert isinstance(n, int), n
try:
return self.lines[n]
except (KeyError, IndexError):
if attempt_to_read and self.source_is_stream:
try:
for line in self.source:
if len(line) > self.max_width:
self.max_width = len(line)
self.lines.append(line)
if len(self.lines) > n:
break
except (UnicodeError, IOError):
pass
return self._get_line(n, attempt_to_read=False)
return ""
def _generate_lines(self, starty, startx):
i = starty
if not self.source:
return
while True:
try:
line = self._get_line(i).expandtabs(4)
for part in ((0,) if not
self.fm.settings.wrap_plaintext_previews else
range(max(1, ((len(line) - 1) // self.wid) + 1))):
shift = part * self.wid
if self.markup == 'ansi':
line_bit = (ansi.char_slice(line, startx + shift,
self.wid + shift)
+ ansi.reset)
else:
line_bit = line[startx + shift:self.wid + startx
+ shift]
yield line_bit.rstrip().replace('\r\n', '\n')
except IndexError:
return
i += 1