summary refs log tree commit diff stats
path: root/ranger
diff options
context:
space:
mode:
authorWojciech Siewierski <wojciech.siewierski@onet.pl>2019-12-28 01:07:49 +0100
committerGitHub <noreply@github.com>2019-12-28 01:07:49 +0100
commit814ba418732267d13bdbe01342d14b1c85171133 (patch)
treefc476dc0e5ebda3b09caa9ecd859f973ef3e7b49 /ranger
parent3417dc99a39f860b2a91fcc43e165c7b1707fb7a (diff)
parent45f58365d6b0ef475407571c77cb98cd7ae1717e (diff)
downloadranger-814ba418732267d13bdbe01342d14b1c85171133.tar.gz
Merge branch 'master' into chain-macros
Diffstat (limited to 'ranger')
-rwxr-xr-xranger/config/commands.py150
-rw-r--r--ranger/config/rc.conf12
-rw-r--r--ranger/config/rifle.conf26
-rw-r--r--ranger/container/fsobject.py6
-rw-r--r--ranger/container/settings.py2
-rw-r--r--ranger/core/actions.py15
-rw-r--r--ranger/core/fm.py25
-rw-r--r--ranger/core/loader.py12
-rw-r--r--ranger/core/main.py3
-rw-r--r--ranger/core/runner.py4
-rw-r--r--ranger/data/mime.types27
-rwxr-xr-xranger/data/scope.sh11
-rw-r--r--ranger/ext/img_display.py37
-rwxr-xr-xranger/ext/rifle.py7
-rw-r--r--ranger/ext/safe_path.py22
-rw-r--r--ranger/ext/shutil_generatorized.py42
-rw-r--r--ranger/gui/displayable.py6
-rw-r--r--ranger/gui/ui.py25
-rw-r--r--ranger/gui/widgets/browsercolumn.py3
-rw-r--r--ranger/gui/widgets/pager.py17
-rw-r--r--ranger/gui/widgets/statusbar.py6
21 files changed, 350 insertions, 108 deletions
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index b7f48eb2..17a0fa9c 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -243,8 +243,9 @@ class cd(Command):
 
         paths = self._tab_fuzzy_match(basepath, tokens)
         if not os.path.isabs(dest):
-            paths_rel = basepath
-            paths = [os.path.relpath(path, paths_rel) for path in paths]
+            paths_rel = self.fm.thisdir.path
+            paths = [os.path.relpath(os.path.join(basepath, path), paths_rel)
+                     for path in paths]
         else:
             paths_rel = ''
         return paths, paths_rel
@@ -700,6 +701,64 @@ class delete(Command):
             self.fm.delete(files)
 
 
+class trash(Command):
+    """:trash
+
+    Tries to move the selection or the files passed in arguments (if any) to
+    the trash, using rifle rules with label "trash".
+    The arguments use a shell-like escaping.
+
+    "Selection" is defined as all the "marked files" (by default, you
+    can mark files with space or v). If there are no marked files,
+    use the "current file" (where the cursor is)
+
+    When attempting to trash non-empty directories or multiple
+    marked files, it will require a confirmation.
+    """
+
+    allow_abbrev = False
+    escape_macros_for_shell = True
+
+    def execute(self):
+        import shlex
+        from functools import partial
+
+        def is_directory_with_files(path):
+            return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
+
+        if self.rest(1):
+            files = shlex.split(self.rest(1))
+            many_files = (len(files) > 1 or is_directory_with_files(files[0]))
+        else:
+            cwd = self.fm.thisdir
+            tfile = self.fm.thisfile
+            if not cwd or not tfile:
+                self.fm.notify("Error: no file selected for deletion!", bad=True)
+                return
+
+            # relative_path used for a user-friendly output in the confirmation.
+            files = [f.relative_path for f in self.fm.thistab.get_selection()]
+            many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
+
+        confirm = self.fm.settings.confirm_on_delete
+        if confirm != 'never' and (confirm != 'multiple' or many_files):
+            self.fm.ui.console.ask(
+                "Confirm deletion of: %s (y/N)" % ', '.join(files),
+                partial(self._question_callback, files),
+                ('n', 'N', 'y', 'Y'),
+            )
+        else:
+            # no need for a confirmation, just delete
+            self.fm.execute_file(files, label='trash')
+
+    def tab(self, tabnum):
+        return self._tab_directory_content()
+
+    def _question_callback(self, files, answer):
+        if answer == 'y' or answer == 'Y':
+            self.fm.execute_file(files, label='trash')
+
+
 class jump_non(Command):
     """:jump_non [-FLAGS...]
 
@@ -1264,30 +1323,69 @@ class unmap(Command):
             self.fm.ui.keymaps.unbind(self.context, arg)
 
 
-class cunmap(unmap):
+class uncmap(unmap):
+    """:uncmap <keys> [<keys2>, ...]
+
+    Remove the given "console" mappings
+    """
+    context = 'console'
+
+
+class cunmap(uncmap):
     """:cunmap <keys> [<keys2>, ...]
 
     Remove the given "console" mappings
+
+    DEPRECATED in favor of uncmap.
     """
-    context = 'browser'
 
+    def execute(self):
+        self.fm.notify("cunmap is deprecated in favor of uncmap!")
+        super(cunmap, self).execute()
 
-class punmap(unmap):
-    """:punmap <keys> [<keys2>, ...]
+
+class unpmap(unmap):
+    """:unpmap <keys> [<keys2>, ...]
 
     Remove the given "pager" mappings
     """
     context = 'pager'
 
 
-class tunmap(unmap):
-    """:tunmap <keys> [<keys2>, ...]
+class punmap(unpmap):
+    """:punmap <keys> [<keys2>, ...]
+
+    Remove the given "pager" mappings
+
+    DEPRECATED in favor of unpmap.
+    """
+
+    def execute(self):
+        self.fm.notify("punmap is deprecated in favor of unpmap!")
+        super(punmap, self).execute()
+
+
+class untmap(unmap):
+    """:untmap <keys> [<keys2>, ...]
 
     Remove the given "taskview" mappings
     """
     context = 'taskview'
 
 
+class tunmap(untmap):
+    """:tunmap <keys> [<keys2>, ...]
+
+    Remove the given "taskview" mappings
+
+    DEPRECATED in favor of untmap.
+    """
+
+    def execute(self):
+        self.fm.notify("tunmap is deprecated in favor of untmap!")
+        super(tunmap, self).execute()
+
+
 class map_(Command):
     """:map <keysequence> <command>
 
@@ -1828,11 +1926,14 @@ class yank(Command):
                     ['xsel'],
                     ['xsel', '-b'],
                 ],
+                'wl-copy': [
+                    ['wl-copy'],
+                ],
                 'pbcopy': [
                     ['pbcopy'],
                 ],
             }
-            ordered_managers = ['pbcopy', 'xclip', 'xsel']
+            ordered_managers = ['pbcopy', 'wl-copy', 'xclip', 'xsel']
             executables = get_executables()
             for manager in ordered_managers:
                 if manager in executables:
@@ -1860,3 +1961,34 @@ class yank(Command):
             in sorted(self.modes.keys())
             if mode
         )
+
+
+class paste_ext(Command):
+    """
+    :paste_ext
+
+    Like paste but tries to rename conflicting files so that the
+    file extension stays intact (e.g. file_.ext).
+    """
+
+    @staticmethod
+    def make_safe_path(dst):
+        if not os.path.exists(dst):
+            return dst
+
+        dst_name, dst_ext = os.path.splitext(dst)
+
+        if not dst_name.endswith("_"):
+            dst_name += "_"
+            if not os.path.exists(dst_name + dst_ext):
+                return dst_name + dst_ext
+        n = 0
+        test_dst = dst_name + str(n)
+        while os.path.exists(test_dst + dst_ext):
+            n += 1
+            test_dst = dst_name + str(n)
+
+        return test_dst + dst_ext
+
+    def execute(self):
+        return self.fm.paste(make_safe_path=paste_ext.make_safe_path)
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 2ca32e3c..7226130d 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -67,6 +67,9 @@ set vcs_backend_hg disabled
 set vcs_backend_bzr disabled
 set vcs_backend_svn disabled
 
+# Truncate the long commit messages to this length when shown in the statusbar.
+set vcs_msg_length 50
+
 # Use one of the supported image preview protocols
 set preview_images false
 
@@ -144,6 +147,9 @@ set preview_files true
 set preview_directories true
 set collapse_preview true
 
+# Wrap long lines in plain text previews?
+set wrap_plaintext_previews false
+
 # Save the console history on exit?
 set save_console_history true
 
@@ -176,10 +182,10 @@ set display_free_space_in_status_bar true
 # Display files tags in all columns or only in main column?
 set display_tags_in_all_columns true
 
-# Set a title for the window?
+# Set a title for the window? Updates both `WM_NAME` and `WM_ICON_NAME`
 set update_title false
 
-# Set the title to "ranger" in the tmux program?
+# Set the tmux window-name to "ranger"?
 set update_tmux_title true
 
 # Shorten the title if it gets long?  The number defines how many
@@ -401,6 +407,7 @@ map <F5> copy
 map <F6> cut
 map <F7> console mkdir%space
 map <F8> console delete
+#map <F8> console trash
 map <F10> exit
 
 # In case you work on a keyboard with dvorak layout
@@ -488,6 +495,7 @@ map p`<any> paste dest=%any_path
 map p'<any> paste dest=%any_path
 
 map dD console delete
+map dT console trash
 
 map dd cut
 map ud uncut
diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf
index a90646e2..86f53fd1 100644
--- a/ranger/config/rifle.conf
+++ b/ranger/config/rifle.conf
@@ -26,7 +26,7 @@
 #   directory      | $1 is a directory
 #   number <n>     | change the number of this command to n
 #   terminal       | stdin, stderr and stdout are connected to a terminal
-#   X              | $DISPLAY is not empty (i.e. Xorg runs)
+#   X              | A graphical environment is available (darwin, Xorg, or Wayland)
 #
 # There are also pseudo-conditions which have a "side effect":
 #   flag <flags>  | Change how the program is run. See below.
@@ -66,13 +66,13 @@ ext x?html?, has uzbl-tabbed,      X, flag f = uzbl-tabbed -- "$@"
 ext x?html?, has uzbl-browser,     X, flag f = uzbl-browser -- "$@"
 ext x?html?, has uzbl-core,        X, flag f = uzbl-core -- "$@"
 ext x?html?, has midori,           X, flag f = midori -- "$@"
-ext x?html?, has chromium-browser, X, flag f = chromium-browser -- "$@"
-ext x?html?, has chromium,         X, flag f = chromium -- "$@"
-ext x?html?, has google-chrome,    X, flag f = google-chrome -- "$@"
 ext x?html?, has opera,            X, flag f = opera -- "$@"
 ext x?html?, has firefox,          X, flag f = firefox -- "$@"
 ext x?html?, has seamonkey,        X, flag f = seamonkey -- "$@"
 ext x?html?, has iceweasel,        X, flag f = iceweasel -- "$@"
+ext x?html?, has chromium-browser, X, flag f = chromium-browser -- "$@"
+ext x?html?, has chromium,         X, flag f = chromium -- "$@"
+ext x?html?, has google-chrome,    X, flag f = google-chrome -- "$@"
 ext x?html?, has epiphany,         X, flag f = epiphany -- "$@"
 ext x?html?, has konqueror,        X, flag f = konqueror -- "$@"
 ext x?html?, has elinks,            terminal = elinks "$@"
@@ -259,10 +259,26 @@ label wallpaper, number 12, mime ^image, has feh, X = feh --bg-tile "$1"
 label wallpaper, number 13, mime ^image, has feh, X = feh --bg-center "$1"
 label wallpaper, number 14, mime ^image, has feh, X = feh --bg-fill "$1"
 
+#-------------------------------------------
+# Generic file openers
+#-------------------------------------------
+label open, has xdg-open = xdg-open -- "$@"
+label open, has open     = open -- "$@"
+
 # Define the editor for non-text files + pager as last action
               !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php  = ask
 label editor, !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php  = ${VISUAL:-$EDITOR} -- "$@"
 label pager,  !mime ^text, !ext xml|json|csv|tex|py|pl|rb|js|sh|php  = "$PAGER" -- "$@"
 
-# The very last action, so that it's never triggered accidentally, is to execute a program:
+
+######################################################################
+# The actions below are left so low down in this file on purpose, so #
+# they are never triggered accidentally.                             #
+######################################################################
+
+# Execute a file as program/script.
 mime application/x-executable = "$1"
+
+# Move the file to trash using trash-cli.
+label trash, has trash-put = trash-put -- "$@"
+label trash = mkdir -p -- ${XDG_DATA_DIR:-$HOME/.ranger}/ranger-trash; mv -- "$@" ${XDG_DATA_DIR:-$HOME/.ranger}/ranger-trash
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index 4fb47354..7de889bf 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -342,10 +342,10 @@ class FileSystemObject(  # pylint: disable=too-many-instance-attributes,too-many
         if self.permissions is not None:
             return self.permissions
 
-        if self.is_directory:
-            perms = ['d']
-        elif self.is_link:
+        if self.is_link:
             perms = ['l']
+        elif self.is_directory:
+            perms = ['d']
         else:
             perms = ['-']
 
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index 82901ac0..7549325a 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -94,9 +94,11 @@ ALLOWED_SETTINGS = {
     'vcs_backend_git': str,
     'vcs_backend_hg': str,
     'vcs_backend_svn': str,
+    'vcs_msg_length': int,
     'viewmode': str,
     'w3m_delay': float,
     'w3m_offset': int,
+    'wrap_plaintext_previews': bool,
     'wrap_scroll': bool,
     'xterm_alt_key': bool,
 }
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 25b01e2c..c3d7de86 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -24,6 +24,7 @@ import ranger
 from ranger.ext.direction import Direction
 from ranger.ext.relative_symlink import relative_symlink
 from ranger.ext.keybinding_parser import key_to_string, construct_keybinding
+from ranger.ext.safe_path import get_safe_path
 from ranger.ext.shell_escape import shell_quote
 from ranger.ext.next_available_filename import next_available_filename
 from ranger.ext.rifle import squash_flags, ASK_COMMAND
@@ -1118,10 +1119,6 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             data['loading'] = False
             return path
 
-        if ranger.args.clean:
-            # Don't access args.cachedir in clean mode
-            return None
-
         if not os.path.exists(ranger.args.cachedir):
             os.makedirs(ranger.args.cachedir)
         cacheimg = os.path.join(ranger.args.cachedir, self.sha1_encode(path))
@@ -1591,19 +1588,21 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 link(source_path,
                      next_available_filename(target_path))
 
-    def paste(self, overwrite=False, append=False, dest=None):
+    def paste(self, overwrite=False, append=False, dest=None, make_safe_path=get_safe_path):
         """:paste
 
         Paste the selected items into the current directory or to dest
         if provided.
         """
-        if dest is None or isdir(dest):
+        if dest is None:
+            dest = self.thistab.path
+        if isdir(dest):
             loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite,
-                                  dest)
+                                  dest, make_safe_path)
             self.loader.add(loadable, append=append)
             self.do_cut = False
         else:
-            self.notify('Failed to paste. The given path is invalid.', bad=True)
+            self.notify('Failed to paste. The destination is invalid.', bad=True)
 
     def delete(self, files=None):
         # XXX: warn when deleting mount points/unseen marked files?
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 43001e8b..7d23c9b6 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -22,11 +22,7 @@ from ranger.container.tags import Tags, TagsDummy
 from ranger.gui.ui import UI
 from ranger.container.bookmarks import Bookmarks
 from ranger.core.runner import Runner
-from ranger.ext.img_display import (W3MImageDisplayer, ITerm2ImageDisplayer,
-                                    TerminologyImageDisplayer,
-                                    URXVTImageDisplayer, URXVTImageFSDisplayer,
-                                    KittyImageDisplayer, UeberzugImageDisplayer,
-                                    ImageDisplayer)
+from ranger.ext.img_display import get_image_displayer
 from ranger.core.metadata import MetadataManager
 from ranger.ext.rifle import Rifle
 from ranger.container.directory import Directory
@@ -104,7 +100,7 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
         def set_image_displayer():
             if self.image_displayer:
                 self.image_displayer.quit()
-            self.image_displayer = self._get_image_displayer()
+            self.image_displayer = get_image_displayer(self.settings.preview_images_method)
         set_image_displayer()
         self.settings.signal_bind('setopt.preview_images_method', set_image_displayer,
                                   priority=settings.SIGNAL_PRIORITY_AFTER_SYNC)
@@ -227,23 +223,6 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
             for line in entry.splitlines():
                 yield line
 
-    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":
-            return ITerm2ImageDisplayer()
-        elif self.settings.preview_images_method == "terminology":
-            return TerminologyImageDisplayer()
-        elif self.settings.preview_images_method == "urxvt":
-            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 == "ueberzug":
-            return UeberzugImageDisplayer()
-        return ImageDisplayer()
-
     def _get_thisfile(self):
         return self.thistab.thisfile
 
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index 26b729b6..7d4b21e8 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -19,6 +19,7 @@ except ImportError:
     HAVE_CHARDET = False
 
 from ranger.core.shared import FileManagerAware
+from ranger.ext.safe_path import get_safe_path
 from ranger.ext.signals import SignalDispatcher
 from ranger.ext.human_readable import human_readable
 
@@ -51,12 +52,14 @@ class Loadable(object):
 class CopyLoader(Loadable, FileManagerAware):  # pylint: disable=too-many-instance-attributes
     progressbar_supported = True
 
-    def __init__(self, copy_buffer, do_cut=False, overwrite=False, dest=None):
+    def __init__(self, copy_buffer, do_cut=False, overwrite=False, dest=None,
+                 make_safe_path=get_safe_path):
         self.copy_buffer = tuple(copy_buffer)
         self.do_cut = do_cut
         self.original_copy_buffer = copy_buffer
         self.original_path = dest if dest is not None else self.fm.thistab.path
         self.overwrite = overwrite
+        self.make_safe_path = make_safe_path
         self.percent = 0
         if self.copy_buffer:
             self.one_file = self.copy_buffer[0]
@@ -108,7 +111,8 @@ class CopyLoader(Loadable, FileManagerAware):  # pylint: disable=too-many-instan
                         self.fm.tags.dump()
                 n = 0
                 for n in shutil_g.move(src=fobj.path, dst=self.original_path,
-                                       overwrite=self.overwrite):
+                                       overwrite=self.overwrite,
+                                       make_safe_path=self.make_safe_path):
                     self.percent = ((done + n) / size) * 100.
                     yield
                 done += n
@@ -125,6 +129,7 @@ class CopyLoader(Loadable, FileManagerAware):  # pylint: disable=too-many-instan
                             dst=os.path.join(self.original_path, fobj.basename),
                             symlinks=True,
                             overwrite=self.overwrite,
+                            make_safe_path=self.make_safe_path,
                     ):
                         self.percent = ((done + n) / size) * 100.
                         yield
@@ -132,7 +137,8 @@ class CopyLoader(Loadable, FileManagerAware):  # pylint: disable=too-many-instan
                 else:
                     n = 0
                     for n in shutil_g.copy2(fobj.path, self.original_path,
-                                            symlinks=True, overwrite=self.overwrite):
+                                            symlinks=True, overwrite=self.overwrite,
+                                            make_safe_path=self.make_safe_path):
                         self.percent = ((done + n) / size) * 100.
                         yield
                     done += n
diff --git a/ranger/core/main.py b/ranger/core/main.py
index aefaacbc..7322a501 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -333,7 +333,8 @@ def parse_arguments():
         return path
 
     if args.clean:
-        args.cachedir = None
+        from tempfile import mkdtemp
+        args.cachedir = mkdtemp(suffix='.ranger-cache')
         args.confdir = None
         args.datadir = None
     else:
diff --git a/ranger/core/runner.py b/ranger/core/runner.py
index f38b026a..d465f070 100644
--- a/ranger/core/runner.py
+++ b/ranger/core/runner.py
@@ -214,7 +214,9 @@ class Runner(object):  # pylint: disable=too-few-public-methods
             toggle_ui = True
             context.wait = True
         if 't' in context.flags:
-            if 'DISPLAY' not in os.environ:
+            if not ('WAYLAND_DISPLAY' in os.environ
+                    or sys.platform == 'darwin'
+                    or 'DISPLAY' in os.environ):
                 return self._log("Can not run with 't' flag, no display found!")
             term = get_term()
             if isinstance(action, str):
diff --git a/ranger/data/mime.types b/ranger/data/mime.types
index 900cb7d9..c20003d3 100644
--- a/ranger/data/mime.types
+++ b/ranger/data/mime.types
@@ -8,22 +8,31 @@
 #  Mimetypes are used for colorschemes and the builtin filetype detection
 #  to execute files with the right program.
 #
+#  You can find the official list of Media Types assigned by IANA here,
+#  https://www.iana.org/assignments/media-types/media-types.xhtml
+#  We deviate from these in certain cases when formats lack an official type
+#  or the type is too generic, application/* for a video format for example.
+#  In such cases we try to adhere to what file(1) returns.
+#
 ###############################################################################
 
-audio/flac              flac
+application/javascript  js
+
 audio/musepack          mpc mpp mp+
-audio/ogg               oga ogg spx
+audio/ogg               oga ogg spx opus
 audio/wavpack           wv wvc
 audio/webm              weba
 audio/x-ape             ape
+audio/x-dsdiff          dsf
+audio/x-flac            flac
 
-video/mkv               mkv
-video/webm              webm
-video/flash             flv
-video/ogg               ogv ogm
-video/divx              div divx
+image/vnd.djvu          djvu
+image/webp              webp
 
 text/x-ruby             rb
-application/javascript  js
 
-application/djvu        djvu
+video/ogg               ogv ogm
+video/webm              webm
+video/x-flv             flv
+video/x-matroska        mkv
+video/x-msvideo         divx
diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh
index eb09b2bf..caa9475f 100755
--- a/ranger/data/scope.sh
+++ b/ranger/data/scope.sh
@@ -87,12 +87,19 @@ handle_extension() {
             w3m -dump "${FILE_PATH}" && exit 5
             lynx -dump -- "${FILE_PATH}" && exit 5
             elinks -dump "${FILE_PATH}" && exit 5
-            ;; # Continue with next handler on failure
+            ;;
+
         ## JSON
         json)
             jq --color-output . "${FILE_PATH}" && exit 5
             python -m json.tool -- "${FILE_PATH}" && exit 5
             ;;
+
+        ## Direct Stream Digital/Transfer (DSDIFF)
+        dsf)
+            mediainfo "${FILE_PATH}" && exit 5
+            exiftool "${FILE_PATH}" && exit 5
+            ;; # Continue with next handler on failure
     esac
 }
 
@@ -237,6 +244,8 @@ handle_mime() {
             env HIGHLIGHT_OPTIONS="${HIGHLIGHT_OPTIONS}" highlight \
                 --out-format="${highlight_format}" \
                 --force -- "${FILE_PATH}" && exit 5
+            env COLORTERM=8bit bat --color=always --style="plain" \
+                -- "${FILE_PATH}" && exit 5
             pygmentize -f "${pygmentize_format}" -O "style=${PYGMENTIZE_STYLE}"\
                 -- "${FILE_PATH}" && exit 5
             exit 2;;
diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py
index 2cce5c7a..8d1d8168 100644
--- a/ranger/ext/img_display.py
+++ b/ranger/ext/img_display.py
@@ -23,6 +23,7 @@ import warnings
 import json
 import threading
 from subprocess import Popen, PIPE
+from collections import defaultdict
 
 import termios
 from contextlib import contextmanager
@@ -72,6 +73,33 @@ class ImgDisplayUnsupportedException(Exception):
     pass
 
 
+def fallback_image_displayer():
+    """Simply makes some noise when chosen. Temporary fallback behavior."""
+
+    raise ImgDisplayUnsupportedException
+
+
+IMAGE_DISPLAYER_REGISTRY = defaultdict(fallback_image_displayer)
+
+
+def register_image_displayer(nickname=None):
+    """Register an ImageDisplayer by nickname if available."""
+
+    def decorator(image_displayer_class):
+        if nickname:
+            registry_key = nickname
+        else:
+            registry_key = image_displayer_class.__name__
+        IMAGE_DISPLAYER_REGISTRY[registry_key] = image_displayer_class
+        return image_displayer_class
+    return decorator
+
+
+def get_image_displayer(registry_key):
+    image_displayer_class = IMAGE_DISPLAYER_REGISTRY[registry_key]
+    return image_displayer_class()
+
+
 class ImageDisplayer(object):
     """Image display provider functions for drawing images in the terminal"""
 
@@ -90,6 +118,7 @@ class ImageDisplayer(object):
         pass
 
 
+@register_image_displayer("w3m")
 class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
     """Implementation of ImageDisplayer using w3mimgdisplay, an utilitary
     program from w3m (a text-based web browser). w3mimgdisplay can display
@@ -243,6 +272,7 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
 # ranger-independent libraries.
 
 
+@register_image_displayer("iterm2")
 class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware):
     """Implementation of ImageDisplayer using iTerm2 image display support
     (http://iterm2.com/images.html).
@@ -308,7 +338,7 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware):
     def _encode_image_content(path):
         """Read and encode the contents of path"""
         with open(path, 'rb') as fobj:
-            return base64.b64encode(fobj.read())
+            return base64.b64encode(fobj.read()).decode('utf-8')
 
     @staticmethod
     def _get_image_dimensions(path):
@@ -351,6 +381,7 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware):
         return width, height
 
 
+@register_image_displayer("terminology")
 class TerminologyImageDisplayer(ImageDisplayer, FileManagerAware):
     """Implementation of ImageDisplayer using terminology image display support
     (https://github.com/billiob/terminology).
@@ -390,6 +421,7 @@ class TerminologyImageDisplayer(ImageDisplayer, FileManagerAware):
         self.clear(0, 0, 0, 0)
 
 
+@register_image_displayer("urxvt")
 class URXVTImageDisplayer(ImageDisplayer, FileManagerAware):
     """Implementation of ImageDisplayer working by setting the urxvt
     background image "under" the preview pane.
@@ -474,6 +506,7 @@ class URXVTImageDisplayer(ImageDisplayer, FileManagerAware):
         self.clear(0, 0, 0, 0)  # dummy assignments
 
 
+@register_image_displayer("urxvt-full")
 class URXVTImageFSDisplayer(URXVTImageDisplayer):
     """URXVTImageDisplayer that utilizes the whole terminal."""
 
@@ -486,6 +519,7 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer):
         return self._get_centered_offsets()
 
 
+@register_image_displayer("kitty")
 class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
     """Implementation of ImageDisplayer for kitty (https://github.com/kovidgoyal/kitty/)
     terminal. It uses the built APC to send commands and data to kitty,
@@ -681,6 +715,7 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
         #         continue
 
 
+@register_image_displayer("ueberzug")
 class UeberzugImageDisplayer(ImageDisplayer):
     """Implementation of ImageDisplayer using ueberzug.
     Ueberzug can display images in a Xorg session.
diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py
index a55e14c7..a73a188b 100755
--- a/ranger/ext/rifle.py
+++ b/ranger/ext/rifle.py
@@ -237,7 +237,9 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
             self._app_flags = argument
             return True
         elif function == 'X':
-            return sys.platform == 'darwin' or 'DISPLAY' in os.environ
+            return ('WAYLAND_DISPLAY' in os.environ
+                    or sys.platform == 'darwin'
+                    or 'DISPLAY' in os.environ)
         elif function == 'env':
             return bool(os.environ.get(argument))
         elif function == 'else':
@@ -293,6 +295,9 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
             self._app_flags = ''
             self._app_label = None
             if skip_ask and cmd == ASK_COMMAND:
+                # TODO(vifon): Fix properly, see
+                # https://github.com/ranger/ranger/pull/1341#issuecomment-537264495
+                count += 1
                 continue
             for test in tests:
                 if not self._eval_condition(test, files, None):
diff --git a/ranger/ext/safe_path.py b/ranger/ext/safe_path.py
new file mode 100644
index 00000000..b172b577
--- /dev/null
+++ b/ranger/ext/safe_path.py
@@ -0,0 +1,22 @@
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+
+import os
+
+SUFFIX = '_'
+
+
+def get_safe_path(dst):
+    if not os.path.exists(dst):
+        return dst
+    if not dst.endswith(SUFFIX):
+        dst += SUFFIX
+        if not os.path.exists(dst):
+            return dst
+    n = 0
+    test_dst = dst + str(n)
+    while os.path.exists(test_dst):
+        n += 1
+        test_dst = dst + str(n)
+
+    return test_dst
diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py
index fe3cf068..a97a4d2d 100644
--- a/ranger/ext/shutil_generatorized.py
+++ b/ranger/ext/shutil_generatorized.py
@@ -7,11 +7,11 @@ import os
 import stat
 import sys
 from shutil import (_samefile, rmtree, _basename, _destinsrc, Error, SpecialFileError)
+from ranger.ext.safe_path import get_safe_path
 
 __all__ = ["copyfileobj", "copyfile", "copystat", "copy2", "BLOCK_SIZE",
            "copytree", "move", "rmtree", "Error", "SpecialFileError"]
 
-APPENDIX = '_'
 BLOCK_SIZE = 16 * 1024
 
 
@@ -103,22 +103,6 @@ else:
             pass
 
 
-def get_safe_path(dst):
-    if not os.path.exists(dst):
-        return dst
-    if not dst.endswith(APPENDIX):
-        dst += APPENDIX
-        if not os.path.exists(dst):
-            return dst
-    n = 0
-    test_dst = dst + str(n)
-    while os.path.exists(test_dst):
-        n += 1
-        test_dst = dst + str(n)
-
-    return test_dst
-
-
 def copyfileobj(fsrc, fdst, length=BLOCK_SIZE):
     """copy data from file-like object fsrc to file-like object fdst"""
     done = 0
@@ -153,7 +137,7 @@ def copyfile(src, dst):
                 yield done
 
 
-def copy2(src, dst, overwrite=False, symlinks=False):
+def copy2(src, dst, overwrite=False, symlinks=False, make_safe_path=get_safe_path):
     """Copy data and all stat info ("cp -p src dst").
 
     The destination may be a directory.
@@ -162,7 +146,7 @@ def copy2(src, dst, overwrite=False, symlinks=False):
     if os.path.isdir(dst):
         dst = os.path.join(dst, os.path.basename(src))
     if not overwrite:
-        dst = get_safe_path(dst)
+        dst = make_safe_path(dst)
     if symlinks and os.path.islink(src):
         linkto = os.readlink(src)
         if overwrite and os.path.lexists(dst):
@@ -175,7 +159,7 @@ def copy2(src, dst, overwrite=False, symlinks=False):
 
 
 def copytree(src, dst,  # pylint: disable=too-many-locals,too-many-branches
-             symlinks=False, ignore=None, overwrite=False):
+             symlinks=False, ignore=None, overwrite=False, make_safe_path=get_safe_path):
     """Recursively copy a directory tree using copy2().
 
     The destination directory must not already exist.
@@ -211,7 +195,7 @@ def copytree(src, dst,  # pylint: disable=too-many-locals,too-many-branches
         os.makedirs(dst)
     except OSError:
         if not overwrite:
-            dst = get_safe_path(dst)
+            dst = make_safe_path(dst)
             os.makedirs(dst)
     errors = []
     done = 0
@@ -229,13 +213,15 @@ def copytree(src, dst,  # pylint: disable=too-many-locals,too-many-branches
                 copystat(srcname, dstname)
             elif os.path.isdir(srcname):
                 n = 0
-                for n in copytree(srcname, dstname, symlinks, ignore, overwrite):
+                for n in copytree(srcname, dstname, symlinks, ignore, overwrite,
+                                  make_safe_path):
                     yield done + n
                 done += n
             else:
                 # Will raise a SpecialFileError for unsupported file types
                 n = 0
-                for n in copy2(srcname, dstname, overwrite=overwrite, symlinks=symlinks):
+                for n in copy2(srcname, dstname, overwrite=overwrite, symlinks=symlinks,
+                               make_safe_path=make_safe_path):
                     yield done + n
                 done += n
         # catch the Error from the recursive copytree so that we can
@@ -256,7 +242,7 @@ def copytree(src, dst,  # pylint: disable=too-many-locals,too-many-branches
         raise Error(errors)
 
 
-def move(src, dst, overwrite=False):
+def move(src, dst, overwrite=False, make_safe_path=get_safe_path):
     """Recursively move a file or directory to another location. This is
     similar to the Unix "mv" command.
 
@@ -283,17 +269,19 @@ def move(src, dst, overwrite=False):
 
         real_dst = os.path.join(dst, _basename(src))
     if not overwrite:
-        real_dst = get_safe_path(real_dst)
+        real_dst = make_safe_path(real_dst)
     try:
         os.rename(src, real_dst)
     except OSError:
         if os.path.isdir(src):
             if _destinsrc(src, dst):
                 raise Error("Cannot move a directory '%s' into itself '%s'." % (src, dst))
-            for done in copytree(src, real_dst, symlinks=True, overwrite=overwrite):
+            for done in copytree(src, real_dst, symlinks=True, overwrite=overwrite,
+                                 make_safe_path=make_safe_path):
                 yield done
             rmtree(src)
         else:
-            for done in copy2(src, real_dst, symlinks=True, overwrite=overwrite):
+            for done in copy2(src, real_dst, symlinks=True, overwrite=overwrite,
+                              make_safe_path=make_safe_path):
                 yield done
             os.unlink(src)
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index 16cb275f..1c3fb3e4 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -197,7 +197,11 @@ class Displayable(  # pylint: disable=too-many-instance-attributes
             try:
                 self.win.mvderwin(y, x)
             except curses.error:
-                pass
+                try:
+                    self.win.resize(hei, wid)
+                    self.win.mvderwin(y, x)
+                except curses.error:
+                    pass
 
             self.paryx = self.win.getparyx()
             self.y, self.x = self.paryx
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index 2874ee97..b6ea0886 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -20,6 +20,12 @@ from .mouse_event import MouseEvent
 
 MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION
 
+# This escape is not available with a capname from terminfo unlike
+# tsl (to_status_line), so it's hardcoded here. It's used just like tsl,
+# but it sets the icon title (WM_ICON_NAME) instead of the window title
+# (WM_NAME).
+ESCAPE_ICON_TITLE = '\033]1;'
+
 _ASCII = ''.join(chr(c) for c in range(32, 127))
 
 
@@ -238,7 +244,7 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
         for key in keys:
             self.handle_key(key)
 
-    def handle_input(self):
+    def handle_input(self):  # pylint: disable=too-many-branches
         key = self.win.getch()
         if key == curses.KEY_ENTER:
             key = ord('\n')
@@ -277,6 +283,9 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
                 else:
                     if not self.fm.input_is_blocked():
                         self.handle_key(key)
+            elif key == -1 and not os.isatty(sys.stdin.fileno()):
+                # STDIN has been closed
+                self.fm.exit()
 
     def setup(self):
         """Build up the UI by initializing widgets."""
@@ -381,16 +390,18 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
             try:
                 fixed_cwd = cwd.encode('utf-8', 'surrogateescape'). \
                     decode('utf-8', 'replace')
-                fmt_tup = (
+                escapes = [
                     curses.tigetstr('tsl').decode('latin-1'),
-                    fixed_cwd,
-                    curses.tigetstr('fsl').decode('latin-1'),
-                )
+                    ESCAPE_ICON_TITLE
+                ]
+                bel = curses.tigetstr('fsl').decode('latin-1')
+                fmt_tups = [(e, fixed_cwd, bel) for e in escapes]
             except UnicodeError:
                 pass
             else:
-                sys.stdout.write("%sranger:%s%s" % fmt_tup)
-                sys.stdout.flush()
+                for fmt_tup in fmt_tups:
+                    sys.stdout.write("%sranger:%s%s" % fmt_tup)
+                    sys.stdout.flush()
 
         self.win.refresh()
 
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index ecc66f44..1412ef6a 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -48,6 +48,9 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
         level 0 => current file/directory
         level <0 => parent directories
         """
+        self.need_redraw = False
+        self.image = None
+        self.need_clear_image = True
         Pager.__init__(self, win)
         Widget.__init__(self, win)  # pylint: disable=non-parent-init-called
         self.level = level
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index c1aa2765..064f28ca 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -246,11 +246,18 @@ class Pager(Widget):  # pylint: disable=too-many-instance-attributes
         while True:
             try:
                 line = self._get_line(i).expandtabs(4)
-                if self.markup == 'ansi':
-                    line = ansi.char_slice(line, startx, self.wid) + ansi.reset
-                else:
-                    line = line[startx:self.wid + startx]
-                yield line.rstrip().replace('\r\n', '\n')
+                for part in ((0,) if not
+                             self.fm.settings.wrap_plaintext_previews else
+                             range(max(1, ((len(line) - 1) // self.wid) + 1))):
+                    shift = part * self.wid
+                    if self.markup == 'ansi':
+                        line_bit = (ansi.char_slice(line, startx + shift,
+                                                    self.wid + shift)
+                                    + ansi.reset)
+                    else:
+                        line_bit = line[startx + shift:self.wid + startx
+                                        + shift]
+                    yield line_bit.rstrip().replace('\r\n', '\n')
             except IndexError:
                 return
             i += 1
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index 19113012..fd44613e 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -212,7 +212,11 @@ class StatusBar(Widget):  # pylint: disable=too-many-instance-attributes
                 left.add_space()
                 left.add(directory.vcs.rootvcs.head['date'].strftime(self.timeformat), 'vcsdate')
                 left.add_space()
-                left.add(directory.vcs.rootvcs.head['summary'][:50], 'vcscommit')
+                summary_length = self.settings.vcs_msg_length or 50
+                left.add(
+                    directory.vcs.rootvcs.head['summary'][:summary_length],
+                    'vcscommit'
+                )
 
     def _get_owner(self, target):
         uid = target.stat.st_uid