summary refs log tree commit diff stats
diff options
context:
space:
mode:
authormark-dawn <albama92@gmail.com>2018-02-14 01:16:38 +0100
committermark-dawn <albama92@gmail.com>2018-05-29 10:06:08 +0200
commitb512e309a0a09ee88c6866535ed23ec80f7aaef9 (patch)
treedd16e298169c17841d9efbce468a6684e1a3b178
parent41fa04be14857f83d32ea7b55b48292bc8f2393b (diff)
downloadranger-b512e309a0a09ee88c6866535ed23ec80f7aaef9.tar.gz
Kitty image display alpha, merge-ready
Added more comments to the code
reverted changes to default values in rc.conf
addes explanations in rc.config about the new options
Couple of small stylistic corrections after running make test
Updated the man pages
-rw-r--r--doc/ranger.117
-rw-r--r--doc/ranger.pod15
-rw-r--r--ranger/config/rc.conf19
-rw-r--r--ranger/container/settings.py3
-rw-r--r--ranger/ext/img_display.py182
5 files changed, 147 insertions, 89 deletions
diff --git a/doc/ranger.1 b/doc/ranger.1
index 1290cb58..f8f0bd8c 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -324,6 +324,23 @@ The same as urxvt but utilizing not only the preview pane but the whole terminal
 window.
 .PP
 To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to urxvt-full.
+.PP
+\fIkitty\fR
+.IX Subsection "kitty"
+.PP
+This only works on Kitty.
+It requires \s-1PIL\s0 or pillow at the moment to work. A nasty bug that can
+corrupt the terminal window when scrolling quicly many images, that can
+be solved by with the \f(CW\*(C`redraw_window\*(C'\fR command.
+.PP
+To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to kitty
+.PP
+\fIkitty-network\fR
+.IX Subsection "kitty-network"
+.PP
+The same as kitty, but uses a streaming method to allow previews remotely,
+for example in an ssh session. However the system is slighly more computationally taxing
+and the quality of the preview is reduced for bandwidth considerations.
 .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 4cac8ef9..79a61a98 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -236,6 +236,21 @@ window.
 
 To enable this feature, set the option C<preview_images_method> to urxvt-full.
 
+=head3 kitty
+
+This only works on Kitty.
+It requires PIL or pillow at the moment to work. A nasty bug that can
+corrupt the terminal window when scrolling quicly many images, that can
+be solved by with the C<redraw_window> command.
+
+To enable this feature, set the option C<preview_images_method> to kitty
+
+=head3 kitty-network
+
+The same as kitty, but uses a streaming method to allow previews remotely,
+for example in an ssh session. However the system is slighly more computationally taxing
+and the quality of the preview is reduced for bandwidth considerations.
+
 =head2 SELECTION
 
 The I<selection> is defined as "All marked files IF THERE ARE ANY, otherwise
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index bf858a50..b8bc00d8 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -68,7 +68,7 @@ set vcs_backend_bzr disabled
 set vcs_backend_svn disabled
 
 # Use one of the supported image preview protocols
-set preview_images true
+set preview_images false
 
 # Set the preview image method. Supported methods:
 #
@@ -93,7 +93,22 @@ set preview_images true
 # * urxvt-full:
 #   The same as urxvt but utilizing not only the preview pane but the
 #   whole terminal window.
-set preview_images_method kitty
+#
+# * kitty:
+#   Preview images in full color using kitty image protocol
+#   (https://github.com/kovidgoyal/kitty/blob/master/graphics-protocol.asciidoc),
+#   Requires python PIL or pillow library.
+#   In experimental stage: tmux support is untested, and a scrolling too fast a folder with many images may glitch ranger;
+#   Future improvements to kitty will ameliorate this issue, for now call the command 'redraw_window' to get rid of the garbage.
+#
+# * kitty-network
+#   Similar to base kitty, bit instead of local storage it uses kitty's protocol special feature to
+#   stream the whole image over standard input. More error prone, and more intensive since it scales down images,
+#   producing also worse quality previews.
+#   However it makes possible to see previews froma ranger instance over the network,
+#   so it makes sense to enable this on remote machines.
+#   Note that has been untested over an actual network.
+set preview_images_method w3m
 
 # Default iTerm2 font size (see: preview_images_method: iterm2)
 set iterm2_font_width 8
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index 170ace5a..dcadf8bf 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -100,7 +100,8 @@ ALLOWED_VALUES = {
     'confirm_on_delete': ['multiple', 'always', 'never'],
     'line_numbers': ['false', 'absolute', 'relative'],
     'one_indexed': [False, True],
-    'preview_images_method': ['w3m', 'iterm2', 'urxvt', 'urxvt-full'],
+    'preview_images_method': ['w3m', 'iterm2', 'urxvt',
+                              'urxvt-full', 'kitty', 'kitty-network'],
     'vcs_backend_bzr': ['disabled', 'local', 'enabled'],
     'vcs_backend_git': ['enabled', 'disabled', 'local'],
     'vcs_backend_hg': ['disabled', 'local', 'enabled'],
diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py
index f34315e3..122e3a15 100644
--- a/ranger/ext/img_display.py
+++ b/ranger/ext/img_display.py
@@ -25,7 +25,8 @@ import termios
 import select
 from contextlib import contextmanager
 import tty
-import fcntl
+import codecs
+from tempfile import NamedTemporaryFile
 
 from ranger.core.shared import FileManagerAware
 
@@ -472,10 +473,19 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer):
 
 
 class KittyImageDisplayer(ImageDisplayer):
-    """TODO: Document here the relevant parts of the protocol"""
+    """Implementation of ImageDisplayer for kitty (https://github.com/kovidgoyal/kitty/)
+    terminal. It uses the built APC to  send commands and data to kitty,
+    which in turn renders the image. The APC takes the form
+    '\033_Gk=v,k=v...;bbbbbbbbbbbbbb\033\\'
+       |   ---------- --------------  |
+    escape code  |             |    escape code
+                 |  base64 encoded payload
+        key: value pairs as parameters
+    For more info please head over to :
+        https://github.com/kovidgoyal/kitty/blob/master/graphics-protocol.asciidoc"""
+    protocol_start = b'\033_G'
+    protocol_end = b'\033\\'
     def __init__(self, stream=False, resize_height=720):
-        self.protocol_start = b'\033_G'
-        self.protocol_end = b'\033\\'
         self.image_id = 0
         self.temp_paths = []
         # parameter deciding if we're going to send the picture data
@@ -486,97 +496,98 @@ class KittyImageDisplayer(ImageDisplayer):
         if "screen" in os.environ['TERM']:
             # TODO: probably need to modify the preamble
             pass
-        # TODO: implement check if protocol terminal supports protocol
-
+        # TODO: implement check if protocol terminal supports kitty protocol
+        # Poissibbly automatically check if transfer via file is possible,
+        # and if negative switch to streaming mode?
+        self.backend = None
         try:
             # pillow is the default since we are not going
             # to spawn other processes, so it _should_ be faster
             import PIL.Image
-            self.backend=PIL.Image
+            self.backend = PIL.Image
             self.filter = PIL.Image.BILINEAR
         except ImportError:
-            sys.stderr.write("PIL not Found, trying ImageMagick")
-            # TODO: check for ImageMagick
-            pass
+            sys.stderr.write("PIL not Found")
+            # TODO: implement a wrapper class for Imagemagick process to
+            # replicate the functionality we use from im
 
     def draw(self, path, start_x, start_y, width, height):
         # dictionary to store the command arguments for kitty
         # a is the display command, with T going for immediate output
-        cmds = {'a': 'T', 'm': 1}
-
-        # let's open the image
-        if self.backend:
-            im = self.backend.open(path)
-            # first let's reduce the size of the image if we intend to stream it
-            aspect = im.width / im.height
-            if im.height > self.max_height:
-                im = im.resize((int(self.max_height * aspect), self.max_height), self.filter)
-            # since kitty streches the image to fill the view box
-            # we need to resize the box to not get distortion
-            cell_ratio = 0.5
-            dest_aspect = width * cell_ratio / height
-            mismatch_ratio = aspect/dest_aspect
-            if mismatch_ratio > 1.02:
-                new_h = height / mismatch_ratio
-                start_y += int((height - new_h) / 2)
-                height = int(new_h)
-            elif mismatch_ratio < 0.98:
-                new_w = width * mismatch_ratio
-                start_x += int((width - new_w) / 2)
-                width = int(new_w)
-            # encode image or just the filename and save the image
-        elif self.backend == "immgk":
-            pass
+        cmds = {'a': 'T'}
+        # sys.stderr.write('{}-{}@{}x{}\t'.format(start_x, start_y, width, height))
+        assert self.backend is not None  # sanity check if we actually have a backend
+        image = self.backend.open(path)
+        aspect = image.width / image.height
+        # first let's reduce the size of the image if we intend to stream it
+        if image.height > self.max_height:
+            image = image.resize((int(self.max_height * aspect), self.max_height), self.filter)
+        # since kitty streches the image to fill the view box
+        # we need to resize the box to not get distortion
+        mismatch_ratio = aspect / (width * 0.5 / height)
+        if mismatch_ratio > 1.05:
+            new_h = height / mismatch_ratio
+            start_y += int((height - new_h) / 2)
+            height = int(new_h)
+        elif mismatch_ratio < 0.95:
+            new_w = width * mismatch_ratio
+            start_x += int((width - new_w) / 2)
+            width = int(new_w)
 
         if self.stream:
-            if im.mode != 'RGB' or im.mode != 'RGBA':
-                im = im.convert('RGB')
-            cmds.update({'t': 'd', 'f': len(im.getbands()) * 8,
-                's': im.width, 'v': im.height,
-                'c': width, 'r': height})
-            raw = bytearray().join(map(bytes, im.getdata())) # TODO: check speed
-            payload = base64.standard_b64encode(raw)
+            # encode the whole image as base64
+            # TODO: implement z compression
+            # to possibly increase resolution in sent image
+            if image.mode != 'RGB' or image.mode != 'RGBA':
+                image = image.convert('RGB')
+            # t: transmissium medium, 'd' for embedded
+            # f: size of a pixel fragment (8bytes per color)
+            # s, v: size of the image to recompose the flattened data
+            # c, r: size in cells of the viewbox
+            cmds.update({'t': 'd', 'f': len(image.getbands()) * 8,
+                         's': image.width, 'v': image.height,
+                         'c': width, 'r': height})
+            payload = base64.standard_b64encode(
+                bytearray().join(map(bytes, image.getdata())))
         else:
-            from tempfile import NamedTemporaryFile
+            # put the image in a temporary png file
+            # we need to find out the encoding for a path string, ascii won't cut it
             try:
-                fsenc = sys.getfilesystemencoding() or 'utf-8'
+                fsenc = sys.getfilesystemencoding()  # returns None if standard utf-8 is used
+                # throws LookupError if can't find the codec, TypeError if fsenc is None
                 codecs.lookup(fsenc)
-            except Exception:
+            except (LookupError, TypeError):
                 fsenc = 'utf-8'
+            # t: transmissium medium, 't' for temporary file (kitty will delete it for us)
+            # f: size of a pixel fragment (100 just mean that the file is png encoded,
+            #       the only format except raw RGB(A) bitmap that kitty understand)
+            # c, r: size in cells of the viewbox
             cmds.update({'t': 't', 'f': 100, 'c': width, 'r': height})
             with NamedTemporaryFile(prefix='rgr_thumb_', suffix='.png', delete=False) as tmpf:
-                im.save(tmpf, format='png', compress_level=0)
+                image.save(tmpf, format='png', compress_level=0)
                 self.temp_paths.append(tmpf.name)
                 payload = base64.standard_b64encode(os.path.abspath(tmpf.name).encode(fsenc))
 
         self.image_id += 1
+        # image handle we'll use with kitty
         cmds['i'] = self.image_id
-        # now for some good old retrocompatibility C protocols
         # save current cursor position
         curses.putp(curses.tigetstr("sc"))
         # we then can move the cursor to our desired spot
-        # for some reason none is using curses.move(y, x)
-        # but this convoluted method
-        tparm = curses.tparm(curses.tigetstr("cup"), start_y, start_x)
-        if sys.version_info[0] < 3:
-            sys.stdout.write(tparm)
-        else:
-            sys.stdout.buffer.write(tparm)
+        # we can't call window.move(y, x) since we don't have the curses win instance
+        sys.stdout.buffer.write(curses.tparm(curses.tigetstr("cup"), start_y, start_x))
 
-        #finally send the command
+        # finally send the command
         for cmd_str in self._format_cmd_str(cmds, payload=payload):
             sys.stdout.buffer.write(cmd_str)
         sys.stdout.flush()
         # to catch the incoming response (which breaks ranger)
         # a simple readline doesn't work, but this seems fine
-        with self.non_blocking_read() as fd:
-            while True:
-                rd = select.select([fd], [], [], 2 if self.stream else 0.1)[0]
-                if rd:
-                    data = sys.stdin.buffer.read()  #TODO: check if all is well
-                    break
-                else:
-                    break
+        # except when scrolling real fast. If kitty will implemnt a key to suppress
+        # responses this will be omitted
+        with self.non_blocking_read() as f_descr:
+            if select.select([f_descr], [], [], 2 if self.stream else 0.1)[0]:
+                sys.stdin.buffer.read()  # TODO: check if all is well
         # Restore cursor
         curses.putp(curses.tigetstr("rc"))
         sys.stdout.flush()
@@ -584,14 +595,16 @@ class KittyImageDisplayer(ImageDisplayer):
     def _format_cmd_str(self, cmd, payload=None, max_l=1024):
         central_blk = ','.join(["{}={}".format(k, v) for k, v in cmd.items()]).encode('ascii')
         if payload is not None:
+            # we add the m key to signal a multiframe communication
+            # appending the end (m=0) key to a single message has no effect
             while len(payload) > max_l:
                 payload_blk, payload = payload[:max_l], payload[max_l:]
                 yield self.protocol_start + \
-                        central_blk + b',m=1;' + payload_blk + \
-                        self.protocol_end
-            yield self.protocol_start + \
-                    central_blk + b',m=0;' + payload + \
+                    central_blk + b',m=1;' + payload_blk + \
                     self.protocol_end
+            yield self.protocol_start + \
+                central_blk + b',m=0;' + payload + \
+                self.protocol_end
         else:
             yield self.protocol_start + central_blk + b';' + self.protocol_end
 
@@ -599,41 +612,38 @@ class KittyImageDisplayer(ImageDisplayer):
     @contextmanager
     def non_blocking_read(src=sys.stdin):
         # not entirely sure what's going on here
-        fd = src.fileno()
+        # but sure it looks like tty black magic
+        f_handle = src.fileno()
         if src.isatty():
-            old = termios.tcgetattr(fd)
-            tty.setraw(fd)
-        oldfl = fcntl.fcntl(fd, fcntl.F_GETFL)
-        fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK)
-        yield fd
+            old = termios.tcgetattr(f_handle)
+            tty.setraw(f_handle)
+        oldfl = fcntl.fcntl(f_handle, fcntl.F_GETFL)
+        # this seems the juicy part were we actally set the non_blockingness
+        fcntl.fcntl(f_handle, fcntl.F_SETFL, oldfl | os.O_NONBLOCK)
+        yield f_handle
+        # after the with block is done we are resetting back to the old state
         if src.isatty():
-            termios.tcsetattr(fd, termios.TCSADRAIN, old)
-        fcntl.fcntl(fd, fcntl.F_SETFL, oldfl)
+            termios.tcsetattr(f_handle, termios.TCSADRAIN, old)
+        fcntl.fcntl(f_handle, fcntl.F_SETFL, oldfl)
 
     def clear(self, start_x, start_y, width, height):
         # let's assume that every time ranger call this
         # it actually wants just to remove the previous image
+        # TODO: implement this using the actual x, y, since the protocol supports it
         cmds = {'a': 'd', 'i': self.image_id}
         for cmd_str in self._format_cmd_str(cmds):
             sys.stdout.buffer.write(cmd_str)
         sys.stdout.flush()
-#        with self.non_blocking_read() as fd:
-#            while True:
-#                rd = select.select([fd], [], [], 2 if self.stream else 0.3)[0]
-#                if rd:
-#                    data = sys.stdin.buffer.read()  #TODO: check if all is well
-#                    break
-#                else:
-#                    break
+        # kitty doesn't seem to reply on deletes, checking like we do in draw()
+        # will slows down scrolling with timeouts from select
         self.image_id -= 1
 
     def quit(self):
         # clear all remaining images, then check if all files went through or are orphaned
         while self.image_id >= 1:
-            self.clear(0,0,0,0)
-        while len(self.temp_paths) != 0:
+            self.clear(0, 0, 0, 0)
+        while self.temp_paths:
             try:
                 os.remove(self.temp_paths.pop())
             except FileNotFoundError:
                 continue
-