diff options
-rw-r--r-- | doc/ranger.1 | 21 | ||||
-rw-r--r-- | doc/ranger.pod | 19 | ||||
-rw-r--r-- | ranger/config/rc.conf | 17 | ||||
-rw-r--r-- | ranger/container/settings.py | 1 | ||||
-rw-r--r-- | ranger/core/fm.py | 12 | ||||
-rw-r--r-- | ranger/ext/img_display.py | 145 |
6 files changed, 191 insertions, 24 deletions
diff --git a/doc/ranger.1 b/doc/ranger.1 index 42b32f70..3ba4fe4e 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -263,12 +263,25 @@ Install these programs (just the ones you need) and scope.sh will automatically use them. .PP Independently of the preview script, there is a feature to preview images -by drawing them directly into the terminal. This does not work over ssh, -requires certain terminals (tested on \*(L"xterm\*(R" and \*(L"urxvt\*(R") and is incompatible -with tmux, although it works with screen. +by drawing them directly into the terminal. To enable this feature, set the +option \f(CW\*(C`preview_images\*(C'\fR to true and enable one of the image preview modes: +.PP +\fIw3m\fR +.IX Subsection "w3m" +.PP +This does not work over ssh, requires certain terminals (tested on \*(L"xterm\*(R" and +\&\*(L"urxvt\*(R") and is incompatible with tmux, although it works with screen. .PP To enable this feature, install the program \*(L"w3m\*(R" and set the option -\&\f(CW\*(C`preview_images\*(C'\fR to true. +\&\f(CW\*(C`preview_images_method\*(C'\fR to w3m. +.PP +\fIiTerm2\fR +.IX Subsection "iTerm2" +.PP +This only works in iTerm2 compiled with image preview support, but works over +ssh. +.PP +To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to iterm2. .SS "\s-1SELECTION\s0" .IX Subsection "SELECTION" The \fIselection\fR is defined as \*(L"All marked files \s-1IF THERE ARE ANY,\s0 otherwise diff --git a/doc/ranger.pod b/doc/ranger.pod index 7b3354c4..336a886c 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -163,12 +163,23 @@ Install these programs (just the ones you need) and scope.sh will automatically use them. Independently of the preview script, there is a feature to preview images -by drawing them directly into the terminal. This does not work over ssh, -requires certain terminals (tested on "xterm" and "urxvt") and is incompatible -with tmux, although it works with screen. +by drawing them directly into the terminal. To enable this feature, set the +option C<preview_images> to true and enable one of the image preview modes: + +=head3 w3m + +This does not work over ssh, requires certain terminals (tested on "xterm" and +"urxvt") and is incompatible with tmux, although it works with screen. To enable this feature, install the program "w3m" and set the option -C<preview_images> to true. +C<preview_images_method> to w3m. + +=head3 iTerm2 + +This only works in iTerm2 compiled with image preview support, but works over +ssh. + +To enable this feature, set the option C<preview_images_method> to iterm2. =head2 SELECTION diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 21c685d5..ca67c389 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -58,11 +58,22 @@ set vcs_backend_git enabled set vcs_backend_hg disabled set vcs_backend_bzr disabled -# Preview images in full color with the external command "w3mimgpreview"? -# This requires the console web browser "w3m" and a supported terminal. -# It has been successfully tested with "xterm" and "urxvt" without tmux. +# Use one of the supported image preview protocols set preview_images false +# Set the preview image method. Supported methods: +# +# * w3m (default): +# Preview images in full color with the external command "w3mimgpreview"? +# This requires the console web browser "w3m" and a supported terminal. +# It has been successfully tested with "xterm" and "urxvt" without tmux. +# +# * iterm2: +# Preview images in full color using iTerm2 image previews +# (http://iterm2.com/images.html). This requires using iTerm2 compiled +# with image preview support. +set preview_images_method w3m + # Use a unicode "..." character to mark cut-off filenames? set unicode_ellipsis false diff --git a/ranger/container/settings.py b/ranger/container/settings.py index ddcc7d14..27737eb1 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -35,6 +35,7 @@ ALLOWED_SETTINGS = { 'preview_directories': bool, 'preview_files': bool, 'preview_images': bool, + 'preview_images_method': str, 'preview_max_size': int, 'preview_script': (str, type(None)), 'save_console_history': bool, diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 22908f46..b32a2c33 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -19,7 +19,7 @@ from ranger.container.tags import Tags from ranger.gui.ui import UI from ranger.container.bookmarks import Bookmarks from ranger.core.runner import Runner -from ranger.ext.img_display import ImageDisplayer +from ranger.ext.img_display import * from ranger.core.metadata import MetadataManager from ranger.ext.rifle import Rifle from ranger.container.directory import Directory @@ -49,7 +49,6 @@ class FM(Actions, SignalDispatcher): self.start_paths = paths self.directories = dict() self.log = deque(maxlen=20) - self.image_displayer = ImageDisplayer() self.bookmarks = bookmarks self.current_tab = 1 self.tabs = {} @@ -97,6 +96,7 @@ class FM(Actions, SignalDispatcher): rifleconf = self.relpath('config/rifle.conf') self.rifle = Rifle(rifleconf) self.rifle.reload_config() + self.image_displayer = self._get_image_displayer() if not ranger.arg.clean and self.tags is None: self.tags = Tags(self.confpath('tagged')) @@ -184,6 +184,14 @@ class FM(Actions, SignalDispatcher): if debug: raise + def _get_image_displayer(self): + if self.settings.preview_images_method == "w3m": + return W3MImageDisplayer() + elif self.settings.preview_images_method == "iterm2": + return ITerm2ImageDisplayer() + else: + return ImageDisplayer() + def _get_thisfile(self): return self.thistab.thisfile diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index a70fe366..1f5be2be 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -1,21 +1,20 @@ # This software is distributed under the terms of the GNU GPL version 3. +"""Interface for drawing images into the console -"""Interface for w3mimgdisplay to draw images into the console - -This module provides functions to draw images in the terminal using -w3mimgdisplay, an utilitary program from w3m (a text-based web browser). -w3mimgdisplay can display images either in virtual tty (using linux -framebuffer) or in a Xorg session. - -w3m need to be installed for this to work. +This module provides functions to draw images in the terminal using supported +implementations, which are currently w3m and iTerm2. """ +import base64 +import curses import fcntl +import imghdr import os import select import struct import sys import termios +from ranger.core.shared import FileManagerAware from subprocess import Popen, PIPE W3MIMGDISPLAY_PATH = '/usr/lib/w3m/w3mimgdisplay' @@ -24,8 +23,28 @@ W3MIMGDISPLAY_OPTIONS = [] class ImgDisplayUnsupportedException(Exception): pass - class ImageDisplayer(object): + """Image display provider functions for drawing images in the terminal""" + def draw(self, path, start_x, start_y, width, height): + """Draw an image at the given coordinates.""" + pass + + def clear(self, start_x, start_y, width, height): + """Clear a part of terminal display.""" + pass + + def quit(self): + """Cleanup and close""" + pass + +class W3MImageDisplayer(ImageDisplayer): + """Implementation of ImageDisplayer using w3mimgdisplay, an utilitary + program from w3m (a text-based web browser). w3mimgdisplay can display + images either in virtual tty (using linux framebuffer) or in a Xorg session. + Does not work over ssh. + + w3m need to be installed for this to work. + """ is_initialized = False def initialize(self): @@ -38,7 +57,6 @@ class ImageDisplayer(object): self.is_initialized = True def draw(self, path, start_x, start_y, width, height): - """Draw an image at the given coordinates.""" if not self.is_initialized or self.process.poll() is not None: self.initialize() self.process.stdin.write(self._generate_w3m_input(path, start_x, @@ -47,7 +65,6 @@ class ImageDisplayer(object): self.process.stdout.readline() def clear(self, start_x, start_y, width, height): - """Clear a part of terminal display.""" if not self.is_initialized or self.process.poll() is not None: self.initialize() @@ -108,6 +125,112 @@ class ImageDisplayer(object): if self.is_initialized: self.process.kill() +class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): + """Implementation of ImageDisplayer using iTerm2 image display support + (http://iterm2.com/images.html). + + Ranger must be running in iTerm2 for this to work. + """ + _minimum_font_width = 8 + _minimum_font_height = 11 + + def draw(self, path, start_x, start_y, width, height): + curses.putp(curses.tigetstr("sc")) + sys.stdout.write(curses.tparm(curses.tigetstr("cup"), start_y, start_x)) + sys.stdout.write(self._generate_iterm2_input(path, width, height)) + curses.putp(curses.tigetstr("rc")) + sys.stdout.flush() + + def clear(self, start_x, start_y, width, height): + self.fm.ui.win.redrawwin() + self.fm.ui.win.refresh() + + def _generate_iterm2_input(self, path, max_cols, max_rows): + """Prepare the image content of path for image display in iTerm2""" + image_width, image_height = self._get_image_dimensions(path) + if max_cols == 0 or max_rows == 0 or image_width == 0 or image_height == 0: + return "" + image_width = self._fit_width( + image_width, image_height, max_cols, max_rows) + content = self._encode_image_content(path) + text = "\033]1337;File=inline=1;preserveAspectRatio=0;" + text += "size={0};width={1}px:{2}\a\n".format( + str(len(content)), + str(int(image_width)), + content) + return text + + def _fit_width(self, width, height, max_cols, max_rows): + max_width = self._minimum_font_width * max_cols + max_height = self._minimum_font_height * max_rows + if height > max_height: + if width > max_width: + width_scale = max_width/float(width) + height_scale = max_height/float(height) + min_scale = min(width_scale, height_scale) + max_scale = max(width_scale, height_scale) + if width * max_scale <= max_width and height * max_scale <= max_height: + return (width * max_scale) + else: + return (width * min_scale) + else: + scale = max_height/float(height) + return (width * scale) + elif width > max_width: + scale = max_width/float(width) + return (width * scale) + else: + return width + + + def _encode_image_content(self, path): + """Read and encode the contents of path""" + file = open(path, 'rb') + try: + return base64.b64encode(file.read()) + except: + return "" + finally: + file.close() + + def _get_image_dimensions(self, path): + """Determine image size using imghdr""" + file_handle = open(path, 'rb') + file_header = file_handle.read(24) + image_type = imghdr.what(path) + if len(file_header) != 24: + file_handle.close() + return 0, 0 + if image_type == 'png': + check = struct.unpack('>i', file_header[4:8])[0] + if check != 0x0d0a1a0a: + file_handle.close() + return 0, 0 + width, height = struct.unpack('>ii', file_header[16:24]) + elif image_type == 'gif': + width, height = struct.unpack('<HH', file_header[6:10]) + elif image_type == 'jpeg': + try: + file_handle.seek(0) + size = 2 + ftype = 0 + while not 0xc0 <= ftype <= 0xcf: + file_handle.seek(size, 1) + byte = file_handle.read(1) + while ord(byte) == 0xff: + byte = file_handle.read(1) + ftype = ord(byte) + size = struct.unpack('>H', file_handle.read(2))[0] - 2 + file_handle.seek(1, 1) + height, width = struct.unpack('>HH', file_handle.read(4)) + except: + file_handle.close() + return 0, 0 + else: + file_handle.close() + return 0, 0 + file_handle.close() + return width, height def _get_font_dimensions(): # Get the height and width of a character displayed in the terminal in |