From 41fa04be14857f83d32ea7b55b48292bc8f2393b Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Tue, 13 Feb 2018 03:54:11 +0100 Subject: Kitty image protocol based preview, alpha Displays images by sending kitty Application Programming Command. Requires PIL/pillow to work, possible enhancment to add a Imagemagik based version. Support for two modes, one slower that is network aware, possible enhancment to add sutomatic mode selection. Known BUGS: scrolling too fast will breakthe response catcher, leaking escape seq into stdin and corrupting the display. FIX? Ask kovidgoyal@kitty to implement a control sequence to disable responses --- ranger/config/rc.conf | 4 +- ranger/core/fm.py | 8 ++- ranger/ext/img_display.py | 172 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 3 deletions(-) diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 1296f1ca..bf858a50 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 false +set preview_images true # Set the preview image method. Supported methods: # @@ -93,7 +93,7 @@ set preview_images false # * urxvt-full: # The same as urxvt but utilizing not only the preview pane but the # whole terminal window. -set preview_images_method w3m +set preview_images_method kitty # Default iTerm2 font size (see: preview_images_method: iterm2) set iterm2_font_width 8 diff --git a/ranger/core/fm.py b/ranger/core/fm.py index d85dd48c..26feb948 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -24,7 +24,9 @@ from ranger.container.bookmarks import Bookmarks from ranger.core.runner import Runner from ranger.ext.img_display import (W3MImageDisplayer, ITerm2ImageDisplayer, TerminologyImageDisplayer, - URXVTImageDisplayer, URXVTImageFSDisplayer, ImageDisplayer) + URXVTImageDisplayer, URXVTImageFSDisplayer, + KittyImageDisplayer, + ImageDisplayer) from ranger.core.metadata import MetadataManager from ranger.ext.rifle import Rifle from ranger.container.directory import Directory @@ -234,6 +236,10 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes return URXVTImageDisplayer() elif self.settings.preview_images_method == "urxvt-full": return URXVTImageFSDisplayer() + elif self.settings.preview_images_method == "kitty": + return KittyImageDisplayer() + elif self.settings.preview_images_method == "kitty-network": + return KittyImageDisplayer(stream=True, resize_height=480) return ImageDisplayer() def _get_thisfile(self): diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 78d71cb2..f34315e3 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -22,6 +22,10 @@ import sys from subprocess import Popen, PIPE import termios +import select +from contextlib import contextmanager +import tty +import fcntl from ranger.core.shared import FileManagerAware @@ -465,3 +469,171 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer): def _get_offsets(self): """Center the image.""" return self._get_centered_offsets() + + +class KittyImageDisplayer(ImageDisplayer): + """TODO: Document here the relevant parts of the protocol""" + 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 + # in the command body, or save it to a temporary file + # the former being default since it is network aware + self.stream = stream + self.max_height = resize_height + if "screen" in os.environ['TERM']: + # TODO: probably need to modify the preamble + pass + # TODO: implement check if protocol terminal supports protocol + + 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.filter = PIL.Image.BILINEAR + except ImportError: + sys.stderr.write("PIL not Found, trying ImageMagick") + # TODO: check for ImageMagick + pass + + 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 + + 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) + else: + from tempfile import NamedTemporaryFile + try: + fsenc = sys.getfilesystemencoding() or 'utf-8' + codecs.lookup(fsenc) + except Exception: + fsenc = 'utf-8' + 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) + self.temp_paths.append(tmpf.name) + payload = base64.standard_b64encode(os.path.abspath(tmpf.name).encode(fsenc)) + + self.image_id += 1 + 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) + + #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 + # Restore cursor + curses.putp(curses.tigetstr("rc")) + sys.stdout.flush() + + 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: + 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 + \ + self.protocol_end + else: + yield self.protocol_start + central_blk + b';' + self.protocol_end + + @staticmethod + @contextmanager + def non_blocking_read(src=sys.stdin): + # not entirely sure what's going on here + fd = 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 + if src.isatty(): + termios.tcsetattr(fd, termios.TCSADRAIN, old) + fcntl.fcntl(fd, 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 + 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 + 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: + try: + os.remove(self.temp_paths.pop()) + except FileNotFoundError: + continue + -- cgit 1.4.1-2-gfad0 From b512e309a0a09ee88c6866535ed23ec80f7aaef9 Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Wed, 14 Feb 2018 01:16:38 +0100 Subject: 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 --- doc/ranger.1 | 17 ++++ doc/ranger.pod | 15 ++++ ranger/config/rc.conf | 19 ++++- ranger/container/settings.py | 3 +- ranger/ext/img_display.py | 182 +++++++++++++++++++++++-------------------- 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 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 command. + +To enable this feature, set the option C 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 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 - -- cgit 1.4.1-2-gfad0 From c7528ea081cd621829dd23c4e47d67d8d2da6a9b Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Fri, 16 Feb 2018 03:13:29 +0100 Subject: Bugfixes & Improvements Fixed support for terminology terminal emulator Fixed the annoying bug affecting fast scrolling (tnkx@kovidgoyal) Refactored some code repeated for the whole module Eliminated some dependencies --- doc/ranger.1 | 6 ++ doc/ranger.pod | 5 ++ ranger/config/rc.conf | 4 + ranger/core/fm.py | 2 +- ranger/ext/img_display.py | 219 ++++++++++++++++++++++------------------------ 5 files changed, 120 insertions(+), 116 deletions(-) diff --git a/doc/ranger.1 b/doc/ranger.1 index f8f0bd8c..a0fc79a1 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -307,6 +307,12 @@ This feature relies on the dimensions of the terminal's font. By default, a width of 8 and height of 11 are used. To use other values, set the options \&\f(CW\*(C`iterm2_font_width\*(C'\fR and \f(CW\*(C`iterm2_font_height\*(C'\fR to the desired values. .PP +\fIterminology\fR +.IX Subsection "terminology" +.PP +This only works in terminology. It can render vectors graphics, but works only locally. +To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to terminology. +.PP \fIurxvt\fR .IX Subsection "urxvt" .PP diff --git a/doc/ranger.pod b/doc/ranger.pod index 79a61a98..fbe20012 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -220,6 +220,11 @@ This feature relies on the dimensions of the terminal's font. By default, a width of 8 and height of 11 are used. To use other values, set the options C and C to the desired values. +=head3 terminology + +This only works in terminology. It can render vectors graphics, but works only locally. +To enable this feature, set the option C to terminology. + =head3 urxvt This only works in urxvt compiled with pixbuf support. Does not work over ssh. diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index b8bc00d8..cf27cbb5 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -86,6 +86,10 @@ set preview_images false # width of 8 and height of 11 are used. To use other values, set the options # iterm2_font_width and iterm2_font_height to the desired values. # +# * terminology: +# Previews images in full color in the terminology terminal emulator. +# Supports a wide variety of formats, even vector graphics like svg +# # * urxvt: # Preview images in full color using urxvt image backgrounds. This # requires using urxvt compiled with pixbuf support. diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 26feb948..226b1461 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -225,7 +225,7 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes for line in entry.splitlines(): yield line - def _get_image_displayer(self): + def _get_image_displayer(self): # pylint: disable=too-many-return-statements if self.settings.preview_images_method == "w3m": return W3MImageDisplayer() elif self.settings.preview_images_method == "iterm2": diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 122e3a15..09d3429c 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -22,9 +22,7 @@ import sys from subprocess import Popen, PIPE import termios -import select from contextlib import contextmanager -import tty import codecs from tempfile import NamedTemporaryFile @@ -40,6 +38,29 @@ W3MIMGDISPLAY_PATHS = [ '/usr/local/libexec/w3m/w3mimgdisplay', ] +# Helper functions shared between the previewers (make them static methods of the base class?) + + +@contextmanager +def temporarly_moved_cursor(to_y, to_x): + """Common boilerplate code to move the cursor to a drawing area. Use it as: + with temporarly_moved_cursor(dest_y, dest_x): + your_func_here()""" + curses.putp(curses.tigetstr("sc")) + move_cur(to_y, to_x) + yield + curses.putp(curses.tigetstr("rc")) + sys.stdout.flush() + + +# this is excised since Terminology needs to move the cursor multiple times +def move_cur(to_y, to_x): + tparm = curses.tparm(curses.tigetstr("cup"), to_y, to_x) + if sys.version_info[0] < 3: + sys.stdout.write(tparm) + else: + sys.stdout.buffer.write(tparm) + class ImageDisplayError(Exception): pass @@ -215,15 +236,8 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): """ def draw(self, path, start_x, start_y, width, height): - curses.putp(curses.tigetstr("sc")) - 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) # pylint: disable=no-member - sys.stdout.write(self._generate_iterm2_input(path, width, height)) - curses.putp(curses.tigetstr("rc")) - sys.stdout.flush() + with temporarly_moved_cursor(start_y, start_x): + sys.stdout.write(self._generate_iterm2_input(path, width, height)) def clear(self, start_x, start_y, width, height): self.fm.ui.win.redrawwin() @@ -332,44 +346,23 @@ class TerminologyImageDisplayer(ImageDisplayer, FileManagerAware): self.close_protocol = "\000" def draw(self, path, start_x, start_y, width, height): - # Save cursor - curses.putp(curses.tigetstr("sc")) - - y = start_y - # Move to drawing zone - self._move_to(start_x, y) - - # Write intent - sys.stdout.write("%s}ic#%d;%d;%s%s" % ( - self.display_protocol, - width, height, - path, - self.close_protocol)) - - # Write Replacement commands ('#') - for _ in range(0, height): - sys.stdout.write("%s}ib%s%s%s}ie%s" % ( - self.display_protocol, - self.close_protocol, - "#" * width, + with temporarly_moved_cursor(start_y, start_x): + # Write intent + sys.stdout.write("%s}ic#%d;%d;%s%s" % ( self.display_protocol, + width, height, + path, self.close_protocol)) - y = y + 1 - self._move_to(start_x, y) - - # Restore cursor - curses.putp(curses.tigetstr("rc")) - - sys.stdout.flush() - @staticmethod - def _move_to(x, y): - # curses.move(y, x) - tparm = curses.tparm(curses.tigetstr("cup"), y, x) - if sys.version_info[0] < 3: - sys.stdout.write(tparm) - else: - sys.stdout.buffer.write(tparm) # pylint: disable=no-member + # Write Replacement commands ('#') + for y in range(0, height): + move_cur(start_y + y, start_x) + sys.stdout.write("%s}ib%s%s%s}ie%s\n" % ( # needs a newline to work + self.display_protocol, + self.close_protocol, + "#" * width, + self.display_protocol, + self.close_protocol)) def clear(self, start_x, start_y, width, height): self.fm.ui.win.redrawwin() @@ -485,20 +478,49 @@ class KittyImageDisplayer(ImageDisplayer): 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.image_id = 0 self.temp_paths = [] # parameter deciding if we're going to send the picture data # in the command body, or save it to a temporary file - # the former being default since it is network aware self.stream = stream self.max_height = resize_height + if "screen" in os.environ['TERM']: # TODO: probably need to modify the preamble pass - # 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? + + # we need to find out the encoding for a path string, ascii won't cut it + try: + self.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(self.fsenc) + except (LookupError, TypeError): + self.fsenc = 'utf-8' + + # automatic check if we share the filesystem using a dummy file + # TODO: this doesn't work somehow, the response from kitty appears on + # the tty, and until a newline is inserted the data won't be sent + # to the stdin we have. This works just fine in draw. Something tells me that since this is + # called early the tubes are not set up correctly yet, but I have no idea how to fix it + # + # with NamedTemporaryFile() as tmpf: + # tmpf.write(bytearray([0xFA]*3)) + # tmpf.flush() + # for cmd in self._format_cmd_str({'i': 1, 'f': 24,'t': 'f', 's': 1, 'v': 1, 'S': 3}, + # payload=base64.standard_b64encode(tmpf.name.encode(self.fsenc))): + # sys.stdout.buffer.write(cmd) + # resp = [b''] + # sys.stdout.flush() + # for _ in range(5): + # while resp[-1] != b'\\': + # resp.append(sys.stdin.buffer.read(1)) + # if b''.join(resp[-4:-2]) == b'OK': + # self.stream = False + # else: + # self.stream = True + self.backend = None try: # pillow is the default since we are not going @@ -511,17 +533,21 @@ class KittyImageDisplayer(ImageDisplayer): # 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): + def draw(self, path, start_x, start_y, width, height): # pylint: disable=too-many-locals + self.image_id += 1 # dictionary to store the command arguments for kitty # a is the display command, with T going for immediate output - cmds = {'a': 'T'} + # i is the id entifier for the image + cmds = {'a': 'T', 'i': self.image_id} # 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 + # first let's reduce the size of the image 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) @@ -538,7 +564,7 @@ class KittyImageDisplayer(ImageDisplayer): # 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': + if image.mode != 'RGB' and image.mode != 'RGBA': image = image.convert('RGB') # t: transmissium medium, 'd' for embedded # f: size of a pixel fragment (8bytes per color) @@ -551,13 +577,6 @@ class KittyImageDisplayer(ImageDisplayer): bytearray().join(map(bytes, image.getdata()))) else: # 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() # 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 (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) @@ -566,33 +585,33 @@ class KittyImageDisplayer(ImageDisplayer): with NamedTemporaryFile(prefix='rgr_thumb_', suffix='.png', delete=False) as tmpf: 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)) + payload = base64.standard_b64encode(tmpf.name.encode(self.fsenc)) + + with temporarly_moved_cursor(start_y, start_x): + for cmd_str in self._format_cmd_str(cmds, payload=payload): + sys.stdout.buffer.write(cmd_str) + # catch kitty answer before the escape codes corrupt the console + resp = [b''] + while resp[-1] != b'\\': + resp.append(sys.stdin.buffer.read(1)) + if b''.join(resp[-4:-2]) == b'OK': + return + else: + raise ImageDisplayError - self.image_id += 1 - # image handle we'll use with kitty - cmds['i'] = self.image_id - # save current cursor position - curses.putp(curses.tigetstr("sc")) - # we then can move the cursor to our desired spot - # 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 - for cmd_str in self._format_cmd_str(cmds, payload=payload): + 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() - # to catch the incoming response (which breaks ranger) - # a simple readline doesn't work, but this seems fine - # 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() + # 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 _format_cmd_str(self, cmd, payload=None, max_l=1024): + def _format_cmd_str(self, cmd, payload=None, max_l=2048): 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 @@ -608,36 +627,6 @@ class KittyImageDisplayer(ImageDisplayer): else: yield self.protocol_start + central_blk + b';' + self.protocol_end - @staticmethod - @contextmanager - def non_blocking_read(src=sys.stdin): - # not entirely sure what's going on here - # but sure it looks like tty black magic - f_handle = src.fileno() - if src.isatty(): - 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(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() - # 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: @@ -645,5 +634,5 @@ class KittyImageDisplayer(ImageDisplayer): while self.temp_paths: try: os.remove(self.temp_paths.pop()) - except FileNotFoundError: + except IOError: continue -- cgit 1.4.1-2-gfad0 From 3c428cad7e752c38a282ba1c83190eb41fc4ff3a Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Sun, 18 Feb 2018 15:06:20 +0100 Subject: Grammar Fixes Fixed grammar horrors in ranger.pod and ranger.conf Improved resize handling --- doc/ranger.pod | 12 ++++-------- ranger/config/rc.conf | 16 +++++++--------- ranger/container/settings.py | 2 +- ranger/core/fm.py | 2 +- ranger/ext/img_display.py | 43 ++++++++++++++++++++++++++----------------- 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/doc/ranger.pod b/doc/ranger.pod index fbe20012..a45ab2e4 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -222,7 +222,7 @@ C and C to the desired values. =head3 terminology -This only works in terminology. It can render vectors graphics, but works only locally. +This only works in terminology. It can render vector graphics, but works only locally. To enable this feature, set the option C to terminology. =head3 urxvt @@ -243,18 +243,14 @@ To enable this feature, set the option C 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 command. +This only works on Kitty. It requires PIL (or pillow) to work. -To enable this feature, set the option C to kitty +To enable this feature, set the option C 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. +for example in an ssh session. However the system is more computationally taxing. =head2 SELECTION diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index cf27cbb5..7adbc239 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -88,7 +88,7 @@ set preview_images false # # * terminology: # Previews images in full color in the terminology terminal emulator. -# Supports a wide variety of formats, even vector graphics like svg +# Supports a wide variety of formats, even vector graphics like svg. # # * urxvt: # Preview images in full color using urxvt image backgrounds. This @@ -99,17 +99,15 @@ set preview_images false # whole terminal window. # # * kitty: -# Preview images in full color using kitty image protocol -# (https://github.com/kovidgoyal/kitty/blob/master/graphics-protocol.asciidoc), +# Preview images in full color using kitty image protocol. # 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. +# Tmux support is untested. # # * 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, +# Similar to base kitty, but instead of local storage it streams the whole image +# over standard input. More error prone, +# and more intensive since it base64 encodes entire images. +# However it makes possible to see previews from ranger 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 diff --git a/ranger/container/settings.py b/ranger/container/settings.py index dcadf8bf..b360cf20 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -100,7 +100,7 @@ ALLOWED_VALUES = { 'confirm_on_delete': ['multiple', 'always', 'never'], 'line_numbers': ['false', 'absolute', 'relative'], 'one_indexed': [False, True], - 'preview_images_method': ['w3m', 'iterm2', 'urxvt', + 'preview_images_method': ['w3m', 'iterm2', 'terminology', 'urxvt', 'urxvt-full', 'kitty', 'kitty-network'], 'vcs_backend_bzr': ['disabled', 'local', 'enabled'], 'vcs_backend_git': ['enabled', 'disabled', 'local'], diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 226b1461..5f4e141d 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -239,7 +239,7 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes elif self.settings.preview_images_method == "kitty": return KittyImageDisplayer() elif self.settings.preview_images_method == "kitty-network": - return KittyImageDisplayer(stream=True, resize_height=480) + return KittyImageDisplayer(stream=True) return ImageDisplayer() def _get_thisfile(self): diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 09d3429c..f444e0f9 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -11,6 +11,7 @@ implementations, which are currently w3m, iTerm2 and urxvt. from __future__ import (absolute_import, division, print_function) +import math import base64 import curses import errno @@ -42,9 +43,9 @@ W3MIMGDISPLAY_PATHS = [ @contextmanager -def temporarly_moved_cursor(to_y, to_x): +def temporarily_moved_cursor(to_y, to_x): """Common boilerplate code to move the cursor to a drawing area. Use it as: - with temporarly_moved_cursor(dest_y, dest_x): + with temporarily_moved_cursor(dest_y, dest_x): your_func_here()""" curses.putp(curses.tigetstr("sc")) move_cur(to_y, to_x) @@ -236,7 +237,7 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware): """ def draw(self, path, start_x, start_y, width, height): - with temporarly_moved_cursor(start_y, start_x): + with temporarily_moved_cursor(start_y, start_x): sys.stdout.write(self._generate_iterm2_input(path, width, height)) def clear(self, start_x, start_y, width, height): @@ -346,7 +347,7 @@ class TerminologyImageDisplayer(ImageDisplayer, FileManagerAware): self.close_protocol = "\000" def draw(self, path, start_x, start_y, width, height): - with temporarly_moved_cursor(start_y, start_x): + with temporarily_moved_cursor(start_y, start_x): # Write intent sys.stdout.write("%s}ic#%d;%d;%s%s" % ( self.display_protocol, @@ -479,13 +480,12 @@ class KittyImageDisplayer(ImageDisplayer): protocol_start = b'\033_G' protocol_end = b'\033\\' - def __init__(self, stream=False, resize_height=720): + def __init__(self, stream=False): self.image_id = 0 self.temp_paths = [] # parameter deciding if we're going to send the picture data # in the command body, or save it to a temporary file self.stream = stream - self.max_height = resize_height if "screen" in os.environ['TERM']: # TODO: probably need to modify the preamble @@ -527,7 +527,7 @@ class KittyImageDisplayer(ImageDisplayer): # to spawn other processes, so it _should_ be faster import PIL.Image self.backend = PIL.Image - self.filter = PIL.Image.BILINEAR + self.filter = PIL.Image.LANCZOS except ImportError: sys.stderr.write("PIL not Found") # TODO: implement a wrapper class for Imagemagick process to @@ -543,24 +543,23 @@ class KittyImageDisplayer(ImageDisplayer): 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 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) + mismatch_ratio = (image.width / image.height) / (width * 0.5 / height) if mismatch_ratio > 1.05: new_h = height / mismatch_ratio - start_y += int((height - new_h) / 2) + 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) + start_x += int((width - new_w) // 2) width = int(new_w) + # resize image to a smaller size. Ideally this should be + # image = self._resize_max_area(image, (480*960), self.filter) + if self.stream: + image = self._resize_max_area(image, (480 * 960), self.filter) # encode the whole image as base64 # TODO: implement z compression # to possibly increase resolution in sent image @@ -581,13 +580,14 @@ class KittyImageDisplayer(ImageDisplayer): # 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}) + cmds.update({'t': 't', 'f': 100, + 'c': width, 'r': height}) with NamedTemporaryFile(prefix='rgr_thumb_', suffix='.png', delete=False) as tmpf: image.save(tmpf, format='png', compress_level=0) self.temp_paths.append(tmpf.name) payload = base64.standard_b64encode(tmpf.name.encode(self.fsenc)) - with temporarly_moved_cursor(start_y, start_x): + with temporarily_moved_cursor(start_y, start_x): for cmd_str in self._format_cmd_str(cmds, payload=payload): sys.stdout.buffer.write(cmd_str) # catch kitty answer before the escape codes corrupt the console @@ -627,6 +627,15 @@ class KittyImageDisplayer(ImageDisplayer): else: yield self.protocol_start + central_blk + b';' + self.protocol_end + @staticmethod + def _resize_max_area(image, max_area, img_filter): + aspect = image.width / image.height + area = image.width * image.height + if area > max_area: + image = image.resize((int(math.sqrt(area * aspect)), + int(math.sqrt(area / aspect))), img_filter) + return image + def quit(self): # clear all remaining images, then check if all files went through or are orphaned while self.image_id >= 1: -- cgit 1.4.1-2-gfad0 From 5bde5d533557968deda1e37db49d9d8650925786 Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Tue, 20 Feb 2018 02:46:42 +0100 Subject: Automatic network detection, python2 and Exception fixes Created late_init method to handle task dependent on late facilities Enabled automatic network detection (required stdin to be properly init) Fixed proper handling of import Errors (required ranger Exception handling) Fixed handling of binary stdio differnces between py2/3 Unified said handling aross the module --- doc/ranger.pod | 7 +-- ranger/config/rc.conf | 12 ++--- ranger/container/settings.py | 4 +- ranger/core/fm.py | 2 - ranger/ext/img_display.py | 110 ++++++++++++++++++++++++------------------- 5 files changed, 70 insertions(+), 65 deletions(-) diff --git a/doc/ranger.pod b/doc/ranger.pod index a45ab2e4..887f704d 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -244,14 +244,11 @@ To enable this feature, set the option C to urxvt-full. =head3 kitty This only works on Kitty. It requires PIL (or pillow) to work. +It is able to automatically work over network, +by switching to a slower transfer method. To enable this feature, set the option C 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 more computationally taxing. - =head2 SELECTION The I is defined as "All marked files IF THERE ARE ANY, otherwise diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 7adbc239..305143c1 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -101,15 +101,11 @@ set preview_images false # * kitty: # Preview images in full color using kitty image protocol. # Requires python PIL or pillow library. +# If ranger does not share the local filesystem with kitty +# the transfer method is switched to encoding the whole image in +# the protocol that, while slower, allows remote previews, +# for example during an ssh session. # Tmux support is untested. -# -# * kitty-network -# Similar to base kitty, but instead of local storage it streams the whole image -# over standard input. More error prone, -# and more intensive since it base64 encodes entire images. -# However it makes possible to see previews from ranger 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) diff --git a/ranger/container/settings.py b/ranger/container/settings.py index b360cf20..b9695301 100644 --- a/ranger/container/settings.py +++ b/ranger/container/settings.py @@ -100,8 +100,8 @@ ALLOWED_VALUES = { 'confirm_on_delete': ['multiple', 'always', 'never'], 'line_numbers': ['false', 'absolute', 'relative'], 'one_indexed': [False, True], - 'preview_images_method': ['w3m', 'iterm2', 'terminology', 'urxvt', - 'urxvt-full', 'kitty', 'kitty-network'], + 'preview_images_method': ['w3m', 'iterm2', 'terminology', + 'urxvt', 'urxvt-full', 'kitty'], 'vcs_backend_bzr': ['disabled', 'local', 'enabled'], 'vcs_backend_git': ['enabled', 'disabled', 'local'], 'vcs_backend_hg': ['disabled', 'local', 'enabled'], diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 5f4e141d..f3a19f0a 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -238,8 +238,6 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes return URXVTImageFSDisplayer() elif self.settings.preview_images_method == "kitty": return KittyImageDisplayer() - elif self.settings.preview_images_method == "kitty-network": - return KittyImageDisplayer(stream=True) return ImageDisplayer() def _get_thisfile(self): diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index f444e0f9..387d61a3 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -57,10 +57,9 @@ def temporarily_moved_cursor(to_y, to_x): # this is excised since Terminology needs to move the cursor multiple times def move_cur(to_y, to_x): tparm = curses.tparm(curses.tigetstr("cup"), to_y, to_x) - if sys.version_info[0] < 3: - sys.stdout.write(tparm) - else: - sys.stdout.buffer.write(tparm) + # on python2 stdout is already in binary mode, in python3 is accessed via buffer + bin_stdout = getattr(sys.stdout, 'buffer', sys.stdout) + bin_stdout.write(tparm) class ImageDisplayError(Exception): @@ -479,60 +478,72 @@ class KittyImageDisplayer(ImageDisplayer): https://github.com/kovidgoyal/kitty/blob/master/graphics-protocol.asciidoc""" protocol_start = b'\033_G' protocol_end = b'\033\\' + # we are going to use stdio in binary mode a lot, so due to py2 -> py3 + # differnces is worth to do this: + stdbout = getattr(sys.stdout, 'buffer', sys.stdout) + stdbin = getattr(sys.stdin, 'buffer', sys.stdin) + # counter for image ids on kitty's end + image_id = 0 + # we need to find out the encoding for a path string, ascii won't cut it + try: + 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 (LookupError, TypeError): + fsenc = 'utf-8' - def __init__(self, stream=False): - self.image_id = 0 + def __init__(self): self.temp_paths = [] - # parameter deciding if we're going to send the picture data - # in the command body, or save it to a temporary file - self.stream = stream if "screen" in os.environ['TERM']: # TODO: probably need to modify the preamble pass - # we need to find out the encoding for a path string, ascii won't cut it - try: - self.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(self.fsenc) - except (LookupError, TypeError): - self.fsenc = 'utf-8' + # the rest of the initializations that require reading stdio or raising exceptions + # are delayed to the first draw call, since curses + # and ranger exception handler are not online at __init__() time + self.needs_late_init = True + # to init in _late_init() + self.backend = None + self.stream = None + def _late_init(self): # automatic check if we share the filesystem using a dummy file - # TODO: this doesn't work somehow, the response from kitty appears on - # the tty, and until a newline is inserted the data won't be sent - # to the stdin we have. This works just fine in draw. Something tells me that since this is - # called early the tubes are not set up correctly yet, but I have no idea how to fix it - # - # with NamedTemporaryFile() as tmpf: - # tmpf.write(bytearray([0xFA]*3)) - # tmpf.flush() - # for cmd in self._format_cmd_str({'i': 1, 'f': 24,'t': 'f', 's': 1, 'v': 1, 'S': 3}, - # payload=base64.standard_b64encode(tmpf.name.encode(self.fsenc))): - # sys.stdout.buffer.write(cmd) - # resp = [b''] - # sys.stdout.flush() - # for _ in range(5): - # while resp[-1] != b'\\': - # resp.append(sys.stdin.buffer.read(1)) - # if b''.join(resp[-4:-2]) == b'OK': - # self.stream = False - # else: - # self.stream = True + with NamedTemporaryFile() as tmpf: + tmpf.write(bytearray([0xFF] * 3)) + tmpf.flush() + for cmd in self._format_cmd_str( + {'a': 'q', 'i': 1, 'f': 24, 't': 'f', 's': 1, 'v': 1, 'S': 3}, + payload=base64.standard_b64encode(tmpf.name.encode(self.fsenc))): + self.stdbout.write(cmd) + sys.stdout.flush() + resp = [b''] + while resp[-1] != b'\\': + resp.append(self.stdbin.read(1)) + # set the transfer method based on the response + resp = str(b''.join(resp)) + if resp.find('OK') != -1: + self.stream = False + elif resp.find('EBADF') != -1: + self.stream = True + else: + raise ImgDisplayUnsupportedException( + 'kitty replied an unexpected response: {}' + .format(resp)) - self.backend = None + # get the image manipulation backend 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.filter = PIL.Image.LANCZOS except ImportError: - sys.stderr.write("PIL not Found") + raise ImageDisplayError("Images previews in kitty require PIL (pillow)") # TODO: implement a wrapper class for Imagemagick process to # replicate the functionality we use from im + self.needs_late_init = False + def draw(self, path, start_x, start_y, width, height): # pylint: disable=too-many-locals self.image_id += 1 # dictionary to store the command arguments for kitty @@ -541,7 +552,10 @@ class KittyImageDisplayer(ImageDisplayer): cmds = {'a': 'T', 'i': self.image_id} # 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 + # finish initialization if it is the first call + if self.needs_late_init: + self._late_init() + image = self.backend.open(path) # since kitty streches the image to fill the view box # we need to resize the box to not get distortion @@ -556,10 +570,10 @@ class KittyImageDisplayer(ImageDisplayer): width = int(new_w) # resize image to a smaller size. Ideally this should be - # image = self._resize_max_area(image, (480*960), self.filter) + # image = self._resize_max_area(image, (), self.backend.LANCZOS) if self.stream: - image = self._resize_max_area(image, (480 * 960), self.filter) + image = self._resize_max_area(image, (480 * 960), self.backend.LANCZOS) # encode the whole image as base64 # TODO: implement z compression # to possibly increase resolution in sent image @@ -589,15 +603,15 @@ class KittyImageDisplayer(ImageDisplayer): with temporarily_moved_cursor(start_y, start_x): for cmd_str in self._format_cmd_str(cmds, payload=payload): - sys.stdout.buffer.write(cmd_str) + self.stdbout.write(cmd_str) # catch kitty answer before the escape codes corrupt the console resp = [b''] while resp[-1] != b'\\': - resp.append(sys.stdin.buffer.read(1)) - if b''.join(resp[-4:-2]) == b'OK': + resp.append(self.stdbin.read(1)) + if str(b''.join(resp)).find('OK'): return else: - raise ImageDisplayError + raise ImageDisplayError('kitty replied "{}"'.format(b''.join(resp))) def clear(self, start_x, start_y, width, height): # let's assume that every time ranger call this @@ -605,8 +619,8 @@ class KittyImageDisplayer(ImageDisplayer): # 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() + self.stdbout.write(cmd_str) + self.stdbout.flush() # 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 -- cgit 1.4.1-2-gfad0 From d33cc0ff06f761a1919c9c7508d8c3ffe88364ad Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Thu, 22 Feb 2018 05:23:46 +0100 Subject: Unstretched Image Previews Implemented a way to obtain unstretched image previews Fixed wording in ranger.pod --- doc/ranger.pod | 3 +-- ranger/config/rc.conf | 2 +- ranger/ext/img_display.py | 39 ++++++++++++++++++--------------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/doc/ranger.pod b/doc/ranger.pod index 887f704d..46ca920e 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -244,8 +244,7 @@ To enable this feature, set the option C to urxvt-full. =head3 kitty This only works on Kitty. It requires PIL (or pillow) to work. -It is able to automatically work over network, -by switching to a slower transfer method. +Allows remote image previews, for example in an ssh session. To enable this feature, set the option C to kitty. diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 305143c1..165e049b 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -102,7 +102,7 @@ set preview_images false # Preview images in full color using kitty image protocol. # Requires python PIL or pillow library. # If ranger does not share the local filesystem with kitty -# the transfer method is switched to encoding the whole image in +# the transfer method is switched to encode the whole image in # the protocol that, while slower, allows remote previews, # for example during an ssh session. # Tmux support is untested. diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 387d61a3..925a4054 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -506,6 +506,7 @@ class KittyImageDisplayer(ImageDisplayer): # to init in _late_init() self.backend = None self.stream = None + self.pix_row, self.pix_col = (0, 0) def _late_init(self): # automatic check if we share the filesystem using a dummy file @@ -542,9 +543,14 @@ class KittyImageDisplayer(ImageDisplayer): # TODO: implement a wrapper class for Imagemagick process to # replicate the functionality we use from im + # get dimensions of a cell in pixels + ret = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0)) + n_cols, n_rows, x_px_tot, y_px_tot = struct.unpack('HHHH', ret) + self.pix_row, self.pix_col = x_px_tot / n_rows, y_px_tot / n_cols self.needs_late_init = False - def draw(self, path, start_x, start_y, width, height): # pylint: disable=too-many-locals + def draw(self, path, start_x, start_y, width, height): self.image_id += 1 # dictionary to store the command arguments for kitty # a is the display command, with T going for immediate output @@ -557,23 +563,16 @@ class KittyImageDisplayer(ImageDisplayer): self._late_init() image = self.backend.open(path) - # since kitty streches the image to fill the view box - # we need to resize the box to not get distortion - mismatch_ratio = (image.width / image.height) / (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) - - # resize image to a smaller size. Ideally this should be - # image = self._resize_max_area(image, (), self.backend.LANCZOS) + box = (width * self.pix_row, height * self.pix_col) + + if image.width > box[0] or image.height > box[1]: + scale = min(box[0] / image.width, box[1] / image.height) + image = image.resize((int(scale * image.width), int(scale * image.height)), + self.backend.LANCZOS) + start_x += ((box[0] - image.width) // 2) // self.pix_row + start_y += ((box[1] - image.height) // 2) // self.pix_col if self.stream: - image = self._resize_max_area(image, (480 * 960), self.backend.LANCZOS) # encode the whole image as base64 # TODO: implement z compression # to possibly increase resolution in sent image @@ -584,8 +583,7 @@ class KittyImageDisplayer(ImageDisplayer): # 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}) + 's': image.width, 'v': image.height, }) payload = base64.standard_b64encode( bytearray().join(map(bytes, image.getdata()))) else: @@ -594,14 +592,13 @@ class KittyImageDisplayer(ImageDisplayer): # 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}) + cmds.update({'t': 't', 'f': 100, }) with NamedTemporaryFile(prefix='rgr_thumb_', suffix='.png', delete=False) as tmpf: image.save(tmpf, format='png', compress_level=0) self.temp_paths.append(tmpf.name) payload = base64.standard_b64encode(tmpf.name.encode(self.fsenc)) - with temporarily_moved_cursor(start_y, start_x): + with temporarily_moved_cursor(int(start_y), int(start_x)): for cmd_str in self._format_cmd_str(cmds, payload=payload): self.stdbout.write(cmd_str) # catch kitty answer before the escape codes corrupt the console -- cgit 1.4.1-2-gfad0 From 8730f1ec6e20048f04298f866d4ed8b23836db57 Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Fri, 23 Feb 2018 23:55:54 +0100 Subject: Fixed manpages, tmux handling Updated manapages Corrections to rc.conf Silenced the warning from pillow about image sizes Tmux session with kitty raises ImgDisplayUnsupportedError changed pager.py to also notify on ImgDispalyUnsupported Error --- doc/ranger.1 | 20 ++++++-------------- doc/ranger.pod | 1 + doc/rifle.1 | 4 ++-- ranger/config/rc.conf | 6 +++--- ranger/ext/img_display.py | 22 ++++++++++++++++------ ranger/gui/widgets/pager.py | 3 ++- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/doc/ranger.1 b/doc/ranger.1 index a0fc79a1..8d373a64 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -129,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.9.1" "2018-05-14" "ranger manual" +.TH RANGER 1 "ranger-1.9.1" "05/29/2018" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -310,7 +310,8 @@ width of 8 and height of 11 are used. To use other values, set the options \fIterminology\fR .IX Subsection "terminology" .PP -This only works in terminology. It can render vectors graphics, but works only locally. +This only works in terminology. It can render vector graphics, but works only locally. +.PP To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to terminology. .PP \fIurxvt\fR @@ -334,19 +335,10 @@ To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR t \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" +This only works on Kitty. It requires \s-1PIL\s0 (or pillow) to work. +Allows remote image previews, for example in an ssh session. .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. +To enable this feature, set the option \f(CW\*(C`preview_images_method\*(C'\fR to kitty. .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 46ca920e..b262ad44 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -223,6 +223,7 @@ C and C to the desired values. =head3 terminology This only works in terminology. It can render vector graphics, but works only locally. + To enable this feature, set the option C to terminology. =head3 urxvt diff --git a/doc/rifle.1 b/doc/rifle.1 index ad32e4f4..c53a6f39 100644 --- a/doc/rifle.1 +++ b/doc/rifle.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.07 (Pod::Simple 3.32) +.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -129,7 +129,7 @@ .\" ======================================================================== .\" .IX Title "RIFLE 1" -.TH RIFLE 1 "rifle-1.9.1" "05.03.2018" "rifle manual" +.TH RIFLE 1 "rifle-1.9.1" "05/29/2018" "rifle manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 165e049b..5317dff5 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -102,10 +102,10 @@ set preview_images false # Preview images in full color using kitty image protocol. # Requires python PIL or pillow library. # If ranger does not share the local filesystem with kitty -# the transfer method is switched to encode the whole image in -# the protocol that, while slower, allows remote previews, +# the transfer method is changed to encode the whole image; +# while slower, this allows remote previews, # for example during an ssh session. -# Tmux support is untested. +# Tmux is unsupported. set preview_images_method w3m # Default iTerm2 font size (see: preview_images_method: iterm2) diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 925a4054..17c730df 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -20,6 +20,7 @@ import imghdr import os import struct import sys +import warnings from subprocess import Popen, PIPE import termios @@ -495,10 +496,6 @@ class KittyImageDisplayer(ImageDisplayer): def __init__(self): self.temp_paths = [] - if "screen" in os.environ['TERM']: - # TODO: probably need to modify the preamble - pass - # the rest of the initializations that require reading stdio or raising exceptions # are delayed to the first draw call, since curses # and ranger exception handler are not online at __init__() time @@ -509,6 +506,14 @@ class KittyImageDisplayer(ImageDisplayer): self.pix_row, self.pix_col = (0, 0) def _late_init(self): + # tmux + if "screen" in os.environ['TERM']: + # this doesn't seem to work, ranger freezes... + # commenting out the response check does nothing + # self.protocol_start = b'\033Ptmux;\033' + self.protocol_start + # self.protocol_end += b'\033\\' + raise ImgDisplayUnsupportedException('tmux support is not implemented with kitty') + # automatic check if we share the filesystem using a dummy file with NamedTemporaryFile() as tmpf: tmpf.write(bytearray([0xFF] * 3)) @@ -561,8 +566,13 @@ class KittyImageDisplayer(ImageDisplayer): # finish initialization if it is the first call if self.needs_late_init: self._late_init() - - image = self.backend.open(path) + with warnings.catch_warnings(record=True): # as warn: + warnings.simplefilter('ignore', self.backend.DecompressionBombWarning) + image = self.backend.open(path) + # TODO: find a way to send a message to the user that + # doesn't stop the image from displaying + # if warn: + # raise ImageDisplayError(str(warn[-1].message)) box = (width * self.pix_row, height * self.pix_col) if image.width > box[0] or image.height > box[1]: diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index 42adf1e9..9afbfd15 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -109,8 +109,9 @@ class Pager(Widget): # pylint: disable=too-many-instance-attributes try: self.fm.image_displayer.draw(self.image, self.x, self.y, self.wid, self.hei) - except ImgDisplayUnsupportedException: + 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: -- cgit 1.4.1-2-gfad0 From 77a65efe3f436c8d46c09a4f84b2d6f38ba228a3 Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Sun, 25 Feb 2018 00:19:03 +0100 Subject: Consistency Fixes Made so images are displayed in the top left corner of the preview pane Fixed incorrect exit on python2 --- ranger/ext/img_display.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 17c730df..49bbc200 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -580,8 +580,8 @@ class KittyImageDisplayer(ImageDisplayer): image = image.resize((int(scale * image.width), int(scale * image.height)), self.backend.LANCZOS) - start_x += ((box[0] - image.width) // 2) // self.pix_row - start_y += ((box[1] - image.height) // 2) // self.pix_col + # start_x += ((box[0] - image.width) // 2) // self.pix_row + # start_y += ((box[1] - image.height) // 2) // self.pix_col if self.stream: # encode the whole image as base64 # TODO: implement z compression @@ -664,5 +664,5 @@ class KittyImageDisplayer(ImageDisplayer): while self.temp_paths: try: os.remove(self.temp_paths.pop()) - except IOError: + except (OSError, IOError): continue -- cgit 1.4.1-2-gfad0 From 3d699f88b0d4e7bf6bccd27360cc841831c3bd5d Mon Sep 17 00:00:00 2001 From: mark-dawn Date: Sat, 12 May 2018 20:32:22 +0200 Subject: Style Fixes Changed response recieving to use bytestrings Pythonic "if b'XX' in resp:" used in response checks Disabled kitty preview when kitty is not in TERM PEP8 new style fixes Fixed operator newline in core/fm.py Added a new ignore for pylint in core/main.py:374,25: Using deprecated method load_module() (deprecated-method) --- ranger/core/fm.py | 4 +-- ranger/core/main.py | 2 +- ranger/ext/img_display.py | 74 ++++++++++++++++++++--------------------------- 3 files changed, 35 insertions(+), 45 deletions(-) diff --git a/ranger/core/fm.py b/ranger/core/fm.py index f3a19f0a..61b3cb11 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -428,5 +428,5 @@ class FM(Actions, # pylint: disable=too-many-instance-attributes if not ranger.args.clean and self.settings.save_tabs_on_exit and len(self.tabs) > 1: with open(self.datapath('tabs'), 'a') as fobj: # Don't save active tab since launching ranger changes the active tab - fobj.write('\0'.join(v.path for t, v in self.tabs.items()) + - '\0\0') + fobj.write('\0'.join(v.path for t, v in self.tabs.items()) + + '\0\0') diff --git a/ranger/core/main.py b/ranger/core/main.py index b5d0af77..598ce243 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -363,7 +363,7 @@ def load_settings( # pylint: disable=too-many-locals,too-many-branches,too-many # Load custom commands def import_file(name, path): # From https://stackoverflow.com/a/67692 - # pragma pylint: disable=no-name-in-module,import-error,no-member + # pragma pylint: disable=no-name-in-module,import-error,no-member, deprecated-method if sys.version_info >= (3, 5): import importlib.util as util spec = util.spec_from_file_location(name, path) diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py index 49bbc200..ba2f6f4d 100644 --- a/ranger/ext/img_display.py +++ b/ranger/ext/img_display.py @@ -11,7 +11,6 @@ implementations, which are currently w3m, iTerm2 and urxvt. from __future__ import (absolute_import, division, print_function) -import math import base64 import curses import errno @@ -468,7 +467,7 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer): class KittyImageDisplayer(ImageDisplayer): """Implementation of ImageDisplayer for kitty (https://github.com/kovidgoyal/kitty/) - terminal. It uses the built APC to send commands and data to 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\\' | ---------- -------------- | @@ -477,8 +476,8 @@ class KittyImageDisplayer(ImageDisplayer): 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\\' + protocol_start = b'\x1b_G' + protocol_end = b'\x1b\\' # we are going to use stdio in binary mode a lot, so due to py2 -> py3 # differnces is worth to do this: stdbout = getattr(sys.stdout, 'buffer', sys.stdout) @@ -494,8 +493,6 @@ class KittyImageDisplayer(ImageDisplayer): fsenc = 'utf-8' def __init__(self): - self.temp_paths = [] - # the rest of the initializations that require reading stdio or raising exceptions # are delayed to the first draw call, since curses # and ranger exception handler are not online at __init__() time @@ -507,12 +504,15 @@ class KittyImageDisplayer(ImageDisplayer): def _late_init(self): # tmux - if "screen" in os.environ['TERM']: + if 'kitty' not in os.environ['TERM']: # this doesn't seem to work, ranger freezes... # commenting out the response check does nothing # self.protocol_start = b'\033Ptmux;\033' + self.protocol_start # self.protocol_end += b'\033\\' - raise ImgDisplayUnsupportedException('tmux support is not implemented with kitty') + raise ImgDisplayUnsupportedException( + 'kitty previews only work in' + + ' kitty and outside tmux. ' + + 'Make sure your TERM contains the string "kitty"') # automatic check if we share the filesystem using a dummy file with NamedTemporaryFile() as tmpf: @@ -523,19 +523,18 @@ class KittyImageDisplayer(ImageDisplayer): payload=base64.standard_b64encode(tmpf.name.encode(self.fsenc))): self.stdbout.write(cmd) sys.stdout.flush() - resp = [b''] - while resp[-1] != b'\\': - resp.append(self.stdbin.read(1)) + resp = b'' + while resp[-2:] != self.protocol_end: + resp += self.stdbin.read(1) # set the transfer method based on the response - resp = str(b''.join(resp)) - if resp.find('OK') != -1: + # if resp.find(b'OK') != -1: + if b'OK' in resp: self.stream = False - elif resp.find('EBADF') != -1: + elif b'EBADF' in resp: self.stream = True else: raise ImgDisplayUnsupportedException( - 'kitty replied an unexpected response: {}' - .format(resp)) + 'kitty replied an unexpected response: {}'.format(resp)) # get the image manipulation backend try: @@ -544,7 +543,7 @@ class KittyImageDisplayer(ImageDisplayer): import PIL.Image self.backend = PIL.Image except ImportError: - raise ImageDisplayError("Images previews in kitty require PIL (pillow)") + raise ImageDisplayError("Image previews in kitty require PIL (pillow)") # TODO: implement a wrapper class for Imagemagick process to # replicate the functionality we use from im @@ -552,7 +551,7 @@ class KittyImageDisplayer(ImageDisplayer): ret = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)) n_cols, n_rows, x_px_tot, y_px_tot = struct.unpack('HHHH', ret) - self.pix_row, self.pix_col = x_px_tot / n_rows, y_px_tot / n_cols + self.pix_row, self.pix_col = x_px_tot // n_rows, y_px_tot // n_cols self.needs_late_init = False def draw(self, path, start_x, start_y, width, height): @@ -566,6 +565,7 @@ class KittyImageDisplayer(ImageDisplayer): # finish initialization if it is the first call if self.needs_late_init: self._late_init() + with warnings.catch_warnings(record=True): # as warn: warnings.simplefilter('ignore', self.backend.DecompressionBombWarning) image = self.backend.open(path) @@ -603,22 +603,21 @@ class KittyImageDisplayer(ImageDisplayer): # 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, }) - with NamedTemporaryFile(prefix='rgr_thumb_', suffix='.png', delete=False) as tmpf: + with NamedTemporaryFile(prefix='ranger_thumb_', suffix='.png', delete=False) as tmpf: image.save(tmpf, format='png', compress_level=0) - self.temp_paths.append(tmpf.name) payload = base64.standard_b64encode(tmpf.name.encode(self.fsenc)) with temporarily_moved_cursor(int(start_y), int(start_x)): for cmd_str in self._format_cmd_str(cmds, payload=payload): self.stdbout.write(cmd_str) # catch kitty answer before the escape codes corrupt the console - resp = [b''] - while resp[-1] != b'\\': - resp.append(self.stdbin.read(1)) - if str(b''.join(resp)).find('OK'): + resp = b'' + while resp[-2:] != self.protocol_end: + resp += self.stdbin.read(1) + if b'OK' in resp: return else: - raise ImageDisplayError('kitty replied "{}"'.format(b''.join(resp))) + raise ImageDisplayError('kitty replied "{}"'.format(resp)) def clear(self, start_x, start_y, width, height): # let's assume that every time ranger call this @@ -632,13 +631,13 @@ class KittyImageDisplayer(ImageDisplayer): # will slows down scrolling with timeouts from select self.image_id -= 1 - def _format_cmd_str(self, cmd, payload=None, max_l=2048): + def _format_cmd_str(self, cmd, payload=None, max_slice_len=2048): 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:] + while len(payload) > max_slice_len: + payload_blk, payload = payload[:max_slice_len], payload[max_slice_len:] yield self.protocol_start + \ central_blk + b',m=1;' + payload_blk + \ self.protocol_end @@ -648,21 +647,12 @@ class KittyImageDisplayer(ImageDisplayer): else: yield self.protocol_start + central_blk + b';' + self.protocol_end - @staticmethod - def _resize_max_area(image, max_area, img_filter): - aspect = image.width / image.height - area = image.width * image.height - if area > max_area: - image = image.resize((int(math.sqrt(area * aspect)), - int(math.sqrt(area / aspect))), img_filter) - return image - 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 self.temp_paths: - try: - os.remove(self.temp_paths.pop()) - except (OSError, IOError): - continue + # for k in self.temp_paths: + # try: + # os.remove(self.temp_paths[k]) + # except (OSError, IOError): + # continue -- cgit 1.4.1-2-gfad0