summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/ranger.121
-rw-r--r--doc/ranger.pod19
-rw-r--r--ranger/api/__init__.py8
-rw-r--r--ranger/config/commands.py2
-rw-r--r--ranger/config/rc.conf21
-rw-r--r--ranger/container/settings.py1
-rw-r--r--ranger/core/actions.py2
-rw-r--r--ranger/core/fm.py12
-rw-r--r--ranger/core/linemode.py16
-rw-r--r--ranger/ext/img_display.py145
-rw-r--r--ranger/gui/widgets/browsercolumn.py6
11 files changed, 213 insertions, 40 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/api/__init__.py b/ranger/api/__init__.py
index cc0815c2..572cf1a4 100644
--- a/ranger/api/__init__.py
+++ b/ranger/api/__init__.py
@@ -29,8 +29,8 @@ def hook_ready(fm):
 
 from ranger.core.linemode import LinemodeBase
 
-def register_linemode(*linemodes):
-    """Register the linemodes in a dictionary of the available linemodes."""
+def register_linemode(linemode_class):
+    """Add a custom linemode class.  See ranger.core.linemode"""
     from ranger.container.fsobject import FileSystemObject
-    for linemode in linemodes:
-        FileSystemObject.linemode_dict[linemode.name] = linemode()
+    FileSystemObject.linemode_dict[linemode_class.name] = linemode_class()
+    return linemode_class
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 714bc83b..e129aac4 100644
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -1447,7 +1447,7 @@ class linemode(default_linemode):
             mode = DEFAULT_LINEMODE
 
         if mode not in self.fm.thisfile.linemode_dict:
-            self.notify("Unhandled linemode: `%s'" % mode, bad=True)
+            self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
             return
 
         self.fm.thisdir._set_linemode_of_children(mode)
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index c6622845..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
 
@@ -273,7 +284,7 @@ map <END>      move to=-1
 map <PAGEDOWN> move down=1   pages=True
 map <PAGEUP>   move up=1     pages=True
 map <CR>       move right=1
-map <DELETE>   console delete
+#map <DELETE>   console delete
 map <INSERT>   console touch 
 
 # VIM-like
@@ -337,6 +348,8 @@ map pL paste_symlink relative=True
 map phl paste_hardlink
 map pht paste_hardlinked_subtree
 
+map dD console delete
+
 map dd cut
 map ud uncut
 map da cut mode=add
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/actions.py b/ranger/core/actions.py
index 9c620f20..d1e383ba 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -1244,7 +1244,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
     def mkdir(self, name):
         try:
-            os.mkdirs(os.path.join(self.thisdir.path, name))
+            os.makedirs(os.path.join(self.thisdir.path, name))
         except OSError as err:
             self.notify(err)
 
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/core/linemode.py b/ranger/core/linemode.py
index c394bc15..166829cf 100644
--- a/ranger/core/linemode.py
+++ b/ranger/core/linemode.py
@@ -29,9 +29,17 @@ class LinemodeBase(object):
         """The left-aligned part of the line."""
         raise NotImplementedError
 
-    @abstractmethod
     def infostring(self, file, metadata):
-        """The right-aligned part of the line."""
+        """The right-aligned part of the line.
+
+        If `NotImplementedError' is raised (e.g. this method is just
+        not implemented in the actual linemode), the caller should
+        provide its own implementation, which in this case means
+        displaying the hardlink count of the directories. Useful
+        because only the caller (BrowserColumn) possesses the data
+        necessary to display that information.
+
+        """
         raise NotImplementedError
 
 
@@ -41,10 +49,6 @@ class DefaultLinemode(LinemodeBase):
     def filetitle(self, file, metadata):
         return file.relative_path
 
-    def infostring(self, file, metadata):
-        # Should never be called for this linemode, implemented in BrowserColumn
-        raise NotImplementedError
-
 
 class TitleLinemode(LinemodeBase):
     name = "metatitle"
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
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 2e9bf7a2..0340b565 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -301,13 +301,13 @@ class BrowserColumn(Pager):
             # info string
             infostring = []
             infostringlen = 0
-            if current_linemode.name == "filename":
-                infostring = self._draw_infostring_display(drawn, space)
-            else:
+            try:
                 infostringdata = current_linemode.infostring(drawn, metadata)
                 if infostringdata:
                     infostring.append([" " + infostringdata + " ",
                                        ["infostring"]])
+            except NotImplementedError:
+                infostring = self._draw_infostring_display(drawn, space)
             if infostring:
                 infostringlen = self._total_len(infostring)
                 if space - infostringlen > 2: