1# This file is part of ranger, the console file manager. 2# License: GNU GPL version 3, see the file "AUTHORS" for details. 3 4"""The pager displays text and allows you to scroll inside it.""" 5 6from __future__ import (absolute_import, division, print_function) 7 8import curses 9import logging 10 11from ranger.gui import ansi 12from ranger.ext.direction import Direction 13from ranger.ext.img_display import ImgDisplayUnsupportedException 14 15from . import Widget 16 17 18LOG = logging.getLogger(__name__) 19 20 21# TODO: Scrolling in embedded pager 22class Pager(Widget): # pylint: disable=too-many-instance-attributes 23 source = None 24 source_is_stream = False 25 26 old_source = None 27 old_scroll_begin = 0 28 old_startx = 0 29 need_clear_image = False 30 need_redraw_image = False 31 max_width = None 32 33 def __init__(self, win, embedded=False): 34 Widget.__init__(self, win) 35 self.embedded = embedded 36 self.scroll_begin = 0 37 self.scroll_extra = 0 38 self.startx = 0 39 self.markup = None 40 self.lines = [] 41 self.image = None 42 self.image_drawn = False 43 44 def _close_source(self): 45 if self.source and self.source_is_stream: 46 try: 47 self.source.close() 48 except OSError as ex: 49 LOG.error('Unable to close pager source') 50 LOG.exception(ex) 51 52 def open(self): 53 self.scroll_begin = 0 54 self.markup = None 55 self.max_width = 0 56 self.startx = 0 57 self.need_redraw = True 58 59 def clear_image(self, force=False): 60 if (force or self.need_clear_image) and self.image_drawn: 61 self.fm.image_displayer.clear(self.x, self.y, self.wid, self.hei) 62 self.need_clear_image = False 63 self.image_drawn = False 64 65 def close(self): 66 if self.image: 67 self.need_clear_image = True 68 self.clear_image() 69 self._close_source() 70 71 def destroy(self): 72 self.clear_image(force=True) 73 Widget.destroy(self) 74 75 def finalize(self): 76 self.fm.ui.win.move(self.y, self.x) 77 78 def scrollbit(self, lines): 79 target_scroll = self.scroll_extra + lines 80 max_scroll = len(self.lines) - self.hei 81 self.scroll_extra = max(0, min(target_scroll, max_scroll)) 82 self.need_redraw = True 83 84 def draw(self): 85 if self.need_clear_image: 86 self.need_redraw = True 87 88 if self.old_source != self.source: 89 self.old_source = self.source 90 self.need_redraw = True 91 92 if self.old_scroll_begin != self.scroll_begin or \ 93 self.old_startx != self.startx: 94 self.old_startx = self.startx 95 self.old_scroll_begin = self.scroll_begin 96 self.need_redraw = True 97 98 if self.need_redraw: 99 self.win.erase() 100 self.need_redraw_image = True 101 self.clear_image() 102 103 if not self.image: 104 scroll_pos = self.scroll_begin + self.scroll_extra 105 line_gen = self._generate_lines( 106 starty=scroll_pos, startx=self.startx) 107 108 for line, i in zip(line_gen, range(self.hei)): 109 self._draw_line(i, line) 110 111 self.need_redraw = False 112 113 def draw_image(self): 114 if self.image and self.need_redraw_image: 115 self.source = None 116 self.need_redraw_image = False 117 try: 118 self.fm.image_displayer.draw(self.image, self.x, self.y, 119 self.wid, self.hei) 120 except ImgDisplayUnsupportedException as ex: 121 self.fm.settings.preview_images = False 122 self.fm.notify(ex, bad=True) 123 except Exception as ex: # pylint: disable=broad-except 124 self.fm.notify(ex, bad=True) 125 else: 126 self.image_drawn = True 127 128 def _draw_line(self, i, line): 129 if self.markup is None: 130 self.addstr(i, 0, line) 131 elif self.markup == 'ansi': 132 try: 133 self.win.move(i, 0) 134 except curses.error: 135 pass 136 else: 137 for chunk in ansi.text_with_fg_bg_attr(line): 138 if isinstance(chunk, tuple): 139 self.set_fg_bg_attr(*chunk) 140 else: 141 self.addstr(chunk) 142 143 def move(self, narg=None, **kw): 144 direction = Direction(kw) 145 if direction.horizontal(): 146 self.startx = direction.move( 147 direction=direction.right(), 148 override=narg, 149 maximum=self.max_width, 150 current=self.startx, 151 pagesize=self.wid, 152 offset=-self.wid + 1) 153 if direction.vertical(): 154 movement = dict( 155 direction=direction.down(), 156 override=narg, 157 current=self.scroll_begin, 158 pagesize=self.hei, 159 offset=-self.hei + 1) 160 if self.source_is_stream: 161 # For streams, we first pretend that the content ends much later, 162 # in case there are still unread lines. 163 desired_position = direction.move( 164 maximum=len(self.lines) + 9999, 165 **movement) 166 # Then, read the new lines as needed to produce a more accurate 167 # maximum for the movement: 168 self._get_line(desired_position + self.hei) 169 self.scroll_begin = direction.move( 170 maximum=len(self.lines), 171 **movement) 172 173 def press(self, key): 174 self.fm.ui.keymaps.use_keymap('pager') 175 self.fm.ui.press(key) 176 177 def set_image(self, image): 178 if self.image: 179 self.need_clear_image = True 180 self.image = image 181 self._close_source() 182 self.source = None 183 self.source_is_stream = False 184 185 def set_source(self, source, strip=False): 186 if self.image: 187 self.image = None 188 self.need_clear_image = True 189 self._close_source() 190 191 self.max_width = 0 192 if isinstance(source, str): 193 self.source_is_stream = False 194 self.lines = source.splitlines() 195 if self.lines: 196 self.max_width = max(len(line) for line in self.lines) 197 elif hasattr(source, '__getitem__'): 198 self.source_is_stream = False 199 self.lines = source 200 if self.lines: 201 self.max_width = max(len(line) for line in source) 202 elif hasattr(source, 'readline'): 203 self.source_is_stream = True 204 self.lines = [] 205 else: 206 self.source = None 207 self.source_is_stream = False 208 return False 209 self.markup = 'ansi' 210 211 if not self.source_is_stream and strip: 212 self.lines = [line.strip() for line in self.lines] 213 214 self.source = source 215 return True 216 217 def click(self, event): 218 n = 1 if event.ctrl() else 3 219 direction = event.mouse_wheel_direction() 220 if direction: 221 self.move(down=direction * n) 222 return True 223 224 def _get_line(self, n, attempt_to_read=True): 225 assert isinstance(n, int), n 226 try: 227 return self.lines[n] 228 except (KeyError, IndexError): 229 if attempt_to_read and self.source_is_stream: 230 try: 231 for line in self.source: 232 if len(line) > self.max_width: 233 self.max_width = len(line) 234 self.lines.append(line) 235 if len(self.lines) > n: 236 break 237 except (UnicodeError, IOError): 238 pass 239 return self._get_line(n, attempt_to_read=False) 240 return "" 241 242 def _generate_lines(self, starty, startx): 243 i = starty 244 if not self.source: 245 return 246 while True: 247 try: 248 line = self._get_line(i).expandtabs(4) 249 for part in ((0,) if not 250 self.fm.settings.wrap_plaintext_previews else 251 range(max(1, ((len(line) - 1) // self.wid) + 1))): 252 shift = part * self.wid 253 if self.markup == 'ansi': 254 line_bit = (ansi.char_slice(line, startx + shift, 255 self.wid + shift) 256 + ansi.reset) 257 else: 258 line_bit = line[startx + shift:self.wid + startx 259 + shift] 260 yield line_bit.rstrip().replace('\r\n', '\n') 261 except IndexError: 262 return 263 i += 1 264