diff options
-rw-r--r-- | doc/ranger.pod | 7 | ||||
-rw-r--r-- | ranger/config/rc.conf | 12 | ||||
-rw-r--r-- | ranger/container/settings.py | 4 | ||||
-rw-r--r-- | ranger/core/fm.py | 2 | ||||
-rw-r--r-- | 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<preview_images_method> 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<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 more computationally taxing. - =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 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 |