about summary refs log tree commit diff stats
path: root/ranger
diff options
context:
space:
mode:
Diffstat (limited to 'ranger')
-rw-r--r--ranger/__init__.py20
-rw-r--r--ranger/api/commands.py11
-rw-r--r--ranger/colorschemes/default.py3
-rw-r--r--ranger/colorschemes/jungle.py6
-rw-r--r--ranger/colorschemes/snow.py3
-rw-r--r--ranger/config/.pylintrc2
-rwxr-xr-xranger/config/commands.py257
-rw-r--r--ranger/config/commands_sample.py2
-rw-r--r--ranger/config/rc.conf14
-rw-r--r--ranger/config/rifle.conf72
-rw-r--r--ranger/container/bookmarks.py39
-rw-r--r--ranger/container/directory.py5
-rw-r--r--ranger/container/file.py6
-rw-r--r--ranger/container/fsobject.py6
-rw-r--r--ranger/container/history.py10
-rw-r--r--ranger/container/settings.py27
-rw-r--r--ranger/container/tags.py62
-rw-r--r--ranger/core/actions.py225
-rw-r--r--ranger/core/filter_stack.py21
-rw-r--r--ranger/core/fm.py85
-rw-r--r--ranger/core/loader.py34
-rw-r--r--ranger/core/main.py44
-rw-r--r--ranger/core/metadata.py10
-rw-r--r--ranger/core/runner.py15
-rw-r--r--ranger/core/tab.py39
-rw-r--r--ranger/data/mime.types2
-rwxr-xr-xranger/data/scope.sh88
-rw-r--r--ranger/ext/accumulator.py9
-rw-r--r--ranger/ext/img_display.py132
-rw-r--r--ranger/ext/keybinding_parser.py11
-rw-r--r--ranger/ext/logutils.py2
-rw-r--r--ranger/ext/macrodict.py82
-rw-r--r--ranger/ext/popen23.py42
-rw-r--r--ranger/ext/popen_forked.py12
-rwxr-xr-xranger/ext/rifle.py136
-rw-r--r--ranger/ext/safe_path.py1
-rw-r--r--ranger/ext/shell_escape.py2
-rw-r--r--ranger/ext/shutil_generatorized.py12
-rw-r--r--ranger/ext/signals.py4
-rw-r--r--ranger/ext/spawn.py16
-rw-r--r--ranger/ext/vcs/git.py2
-rw-r--r--ranger/ext/vcs/vcs.py21
-rw-r--r--ranger/ext/widestring.py3
-rw-r--r--ranger/gui/ansi.py24
-rw-r--r--ranger/gui/bar.py10
-rw-r--r--ranger/gui/color.py36
-rw-r--r--ranger/gui/colorscheme.py10
-rw-r--r--ranger/gui/context.py3
-rw-r--r--ranger/gui/displayable.py22
-rw-r--r--ranger/gui/ui.py99
-rw-r--r--ranger/gui/widgets/browsercolumn.py69
-rw-r--r--ranger/gui/widgets/console.py136
-rw-r--r--ranger/gui/widgets/statusbar.py8
-rw-r--r--ranger/gui/widgets/titlebar.py6
-rw-r--r--ranger/gui/widgets/view_multipane.py102
55 files changed, 1428 insertions, 692 deletions
diff --git a/ranger/__init__.py b/ranger/__init__.py
index fabaeae9..cc388d60 100644
--- a/ranger/__init__.py
+++ b/ranger/__init__.py
@@ -11,6 +11,7 @@ program you want to use to open your files with.
 from __future__ import (absolute_import, division, print_function)
 
 import os
+from sys import version_info
 
 
 # Version helper
@@ -21,21 +22,23 @@ def version_helper():
         import subprocess
         version_string = 'ranger-master {0}'
         try:
-            git_describe = subprocess.Popen(['git', 'describe'],
-                                            universal_newlines=True,
-                                            cwd=RANGERDIR,
-                                            stdout=subprocess.PIPE,
-                                            stderr=subprocess.PIPE)
-            (git_description, _) = git_describe.communicate()
+            with subprocess.Popen(
+                ["git", "describe"],
+                universal_newlines=True,
+                cwd=RANGERDIR,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+            ) as git_describe:
+                (git_description, _) = git_describe.communicate()
             version_string = version_string.format(git_description.strip('\n'))
-        except (OSError, subprocess.CalledProcessError):
+        except (OSError, subprocess.CalledProcessError, AttributeError):
             version_string = version_string.format(__version__)
     return version_string
 
 
 # Information
 __license__ = 'GPL3'
-__version__ = '1.9.2'
+__version__ = '1.9.3'
 __release__ = False
 __author__ = __maintainer__ = 'Roman Zimbelmann'
 __email__ = 'hut@hut.pm'
@@ -50,6 +53,7 @@ MACRO_DELIMITER_ESC = '%%'
 DEFAULT_PAGER = 'less'
 USAGE = '%prog [options] [path]'
 VERSION = version_helper()
+PY3 = version_info[0] >= 3
 
 # These variables are ignored if the corresponding
 # XDG environment variable is non-empty and absolute
diff --git a/ranger/api/commands.py b/ranger/api/commands.py
index 90a79488..7b76203b 100644
--- a/ranger/api/commands.py
+++ b/ranger/api/commands.py
@@ -287,7 +287,7 @@ class Command(FileManagerAware):
                             if dn.startswith(rel_basename)]
         except (OSError, StopIteration):
             # os.walk found nothing
-            pass
+            return None
         else:
             dirnames.sort()
 
@@ -353,7 +353,7 @@ class Command(FileManagerAware):
                                     if name.startswith(rel_basename)])
         except (OSError, StopIteration):
             # os.walk found nothing
-            pass
+            return None
         else:
             # no results, return None
             if not names:
@@ -395,7 +395,8 @@ def command_function_factory(func):
     class CommandFunction(Command):
         __doc__ = func.__doc__
 
-        def execute(self):  # pylint: disable=too-many-branches
+        def execute(self):
+            # pylint: disable=too-many-branches,too-many-return-statements
             if not func:
                 return None
             if len(self.args) == 1:
@@ -404,7 +405,7 @@ def command_function_factory(func):
                 except TypeError:
                     return func()
 
-            args, kwargs = list(), dict()
+            args, kwargs = [], {}
             for arg in self.args[1:]:
                 equal_sign = arg.find("=")
                 value = arg if equal_sign == -1 else arg[equal_sign + 1:]
@@ -442,6 +443,8 @@ def command_function_factory(func):
                 self.fm.notify("Bad arguments for %s: %s, %s" % (func.__name__, args, kwargs),
                                bad=True)
 
+            return None
+
     CommandFunction.__name__ = func.__name__
     return CommandFunction
 
diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py
index 3d50e2e4..86d2fbba 100644
--- a/ranger/colorschemes/default.py
+++ b/ranger/colorschemes/default.py
@@ -64,6 +64,9 @@ class Default(ColorScheme):
                 else:
                     fg = red
                 fg += BRIGHT
+            if context.line_number and not context.selected:
+                fg = default
+                attr &= ~bold
             if not context.selected and (context.cut or context.copied):
                 attr |= bold
                 fg = black
diff --git a/ranger/colorschemes/jungle.py b/ranger/colorschemes/jungle.py
index a1fd768d..c11ce64a 100644
--- a/ranger/colorschemes/jungle.py
+++ b/ranger/colorschemes/jungle.py
@@ -4,7 +4,7 @@
 from __future__ import (absolute_import, division, print_function)
 
 from ranger.colorschemes.default import Default
-from ranger.gui.color import green, red, blue
+from ranger.gui.color import green, red, blue, bold
 
 
 class Scheme(Default):
@@ -17,6 +17,10 @@ class Scheme(Default):
                 and not context.inactive_pane:
             fg = self.progress_bar_color
 
+        if context.line_number and not context.selected:
+            fg = self.progress_bar_color
+            attr &= ~bold
+
         if context.in_titlebar and context.hostname:
             fg = red if context.bad else blue
 
diff --git a/ranger/colorschemes/snow.py b/ranger/colorschemes/snow.py
index c5f23c1c..354c2a47 100644
--- a/ranger/colorschemes/snow.py
+++ b/ranger/colorschemes/snow.py
@@ -21,6 +21,9 @@ class Snow(ColorScheme):
             if context.directory:
                 attr |= bold
                 fg += BRIGHT
+            if context.line_number and not context.selected:
+                attr |= bold
+                fg += BRIGHT
 
         elif context.highlight:
             attr |= reverse
diff --git a/ranger/config/.pylintrc b/ranger/config/.pylintrc
index 7cac80d1..509c6f31 100644
--- a/ranger/config/.pylintrc
+++ b/ranger/config/.pylintrc
@@ -5,4 +5,4 @@ class-rgx=[a-z][a-z0-9_]{1,30}$
 [FORMAT]
 max-line-length = 99
 max-module-lines=3000
-disable=locally-disabled,locally-enabled,missing-docstring,duplicate-code,fixme
+disable=consider-using-f-string,duplicate-code,fixme,import-outside-toplevel,locally-disabled,locally-enabled,missing-docstring,no-else-return,super-with-arguments
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 5defa677..43c4993f 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -94,7 +94,9 @@ from __future__ import (absolute_import, division, print_function)
 from collections import deque
 import os
 import re
+from io import open
 
+from ranger import PY3
 from ranger.api.commands import Command
 
 
@@ -337,7 +339,7 @@ class open_with(Command):
     def execute(self):
         app, flags, mode = self._get_app_flags_mode(self.rest(1))
         self.fm.execute_file(
-            files=[f for f in self.fm.thistab.get_selection()],
+            files=self.fm.thistab.get_selection(),
             app=app,
             flags=flags,
             mode=mode)
@@ -480,36 +482,106 @@ class set_(Command):
         return None
 
 
-class setlocal(set_):
-    """:setlocal path=<regular expression> <option name>=<python expression>
+class setlocal_(set_):
+    """Shared class for setinpath and setinregex
 
-    Gives an option a new value.
+    By implementing the _arg abstract propery you can affect what the name of
+    the pattern/path/regex argument can be, this should be a regular expression
+    without match groups.
+
+    By implementing the _format_arg abstract method you can affect how the
+    argument is formatted as a regular expression.
     """
-    PATH_RE_DQUOTED = re.compile(r'^setlocal\s+path="(.*?)"')
-    PATH_RE_SQUOTED = re.compile(r"^setlocal\s+path='(.*?)'")
-    PATH_RE_UNQUOTED = re.compile(r'^path=(.*?)$')
+    from abc import (ABCMeta, abstractmethod, abstractproperty)
+
+    __metaclass__ = ABCMeta
+
+    @property
+    @abstractmethod
+    def _arg(self):
+        """The name of the option for the path/regex"""
+        raise NotImplementedError
+
+    def __init__(self, *args, **kwargs):
+        super(setlocal_, self).__init__(*args, **kwargs)
+        # We require quoting of paths with whitespace so we have to take care
+        # not to match escaped quotes.
+        self.path_re_dquoted = re.compile(
+            r'^set.+?\s+{arg}="(.*?[^\\])"'.format(arg=self._arg)
+        )
+        self.path_re_squoted = re.compile(
+            r"^set.+?\s+{arg}='(.*?[^\\])'".format(arg=self._arg)
+        )
+        self.path_re_unquoted = re.compile(
+            r'^{arg}=(.+?)$'.format(arg=self._arg)
+        )
 
     def _re_shift(self, match):
         if not match:
             return None
-        path = os.path.expanduser(match.group(1))
-        for _ in range(len(path.split())):
+        path = match.group(1)
+        # Prepend something that behaves like "path=" in case path starts with
+        # whitespace
+        for _ in "={0}".format(path).split():
             self.shift()
-        return path
+        return os.path.expanduser(path)
+
+    @abstractmethod
+    def _format_arg(self, arg):
+        """How to format the argument as a regular expression"""
+        raise NotImplementedError
 
     def execute(self):
-        path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
-        if path is None:
-            path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
-        if path is None:
-            path = self._re_shift(self.PATH_RE_UNQUOTED.match(self.arg(1)))
-        if path is None and self.fm.thisdir:
-            path = self.fm.thisdir.path
-        if not path:
+        arg = self._re_shift(self.path_re_dquoted.match(self.line))
+        if arg is None:
+            arg = self._re_shift(self.path_re_squoted.match(self.line))
+        if arg is None:
+            arg = self._re_shift(self.path_re_unquoted.match(self.arg(1)))
+        if arg is None and self.fm.thisdir:
+            arg = self.fm.thisdir.path
+        if arg is None:
             return
+        else:
+            arg = self._format_arg(arg)
 
         name, value, _ = self.parse_setting_line()
-        self.fm.set_option_from_string(name, value, localpath=path)
+        self.fm.set_option_from_string(name, value, localpath=arg)
+
+
+class setinpath(setlocal_):
+    """:setinpath path=<path> <option name>=<python expression>
+
+    Sets an option when in a directory that matches <path>, relative paths can
+    match multiple directories, for example, ``path=build`` would match a build
+    directory in any directory. If the <path> contains whitespace it needs to
+    be quoted and nested quotes need to be backslash-escaped. The "path"
+    argument can also be named "pattern" to allow for easier switching with
+    ``setinregex``.
+    """
+    _arg = "(?:path|pattern)"
+
+    def _format_arg(self, arg):
+        return "{0}$".format(re.escape(arg))
+
+
+class setlocal(setinpath):
+    """:setlocal is an alias for :setinpath"""
+
+
+class setinregex(setlocal_):
+    """:setinregex re=<regex> <option name>=<python expression>
+
+    Sets an option when in a specific directory. If the <regex> contains
+    whitespace it needs to be quoted and nested quotes need to be
+    backslash-escaped. Special characters need to be escaped if they are
+    intended to match literally as documented in the ``re`` library
+    documentation. The "re" argument can also be named "regex" or "pattern,"
+    which allows for easier switching with ``setinpath``.
+    """
+    _arg = "(?:re(?:gex)?|pattern)"
+
+    def _format_arg(self, arg):
+        return arg
 
 
 class setintag(set_):
@@ -697,7 +769,7 @@ class delete(Command):
         return self._tab_directory_content()
 
     def _question_callback(self, files, answer):
-        if answer == 'y' or answer == 'Y':
+        if answer.lower() == 'y':
             self.fm.delete(files)
 
 
@@ -727,8 +799,11 @@ class trash(Command):
             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]))
+            file_names = shlex.split(self.rest(1))
+            files = self.fm.get_filesystem_objects(file_names)
+            if files is None:
+                return
+            many_files = (len(files) > 1 or is_directory_with_files(files[0].path))
         else:
             cwd = self.fm.thisdir
             tfile = self.fm.thisfile
@@ -736,27 +811,42 @@ class trash(Command):
                 self.fm.notify("Error: no file selected for deletion!", bad=True)
                 return
 
+            files = self.fm.thistab.get_selection()
             # relative_path used for a user-friendly output in the confirmation.
-            files = [f.relative_path for f in self.fm.thistab.get_selection()]
+            file_names = [f.relative_path for f in files]
             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),
+                "Confirm deletion of: %s (y/N)" % ', '.join(file_names),
                 partial(self._question_callback, files),
                 ('n', 'N', 'y', 'Y'),
             )
         else:
             # no need for a confirmation, just delete
-            self.fm.execute_file(files, label='trash')
+            self._trash_files_catch_arg_list_error(files)
 
     def tab(self, tabnum):
         return self._tab_directory_content()
 
     def _question_callback(self, files, answer):
-        if answer == 'y' or answer == 'Y':
+        if answer.lower() == 'y':
+            self._trash_files_catch_arg_list_error(files)
+
+    def _trash_files_catch_arg_list_error(self, files):
+        """
+        Executes the fm.execute_file method but catches the OSError ("Argument list too long")
+        that occurs when moving too many files to trash (and would otherwise crash ranger).
+        """
+        try:
             self.fm.execute_file(files, label='trash')
+        except OSError as err:
+            if err.errno == 7:
+                self.fm.notify("Error: Command too long (try passing less files at once)",
+                               bad=True)
+            else:
+                raise
 
 
 class jump_non(Command):
@@ -827,21 +917,31 @@ class mark_tag(Command):
 
 
 class console(Command):
-    """:console <command>
+    """:console [-p N | -s sep] <command>
 
+    Flags:
+     -p N   Set position at N index
+     -s sep Set position at separator(any char[s] sequence), example '#'
     Open the console with the given command.
     """
 
     def execute(self):
         position = None
+        command = self.rest(1)
         if self.arg(1)[0:2] == '-p':
+            command = self.rest(2)
             try:
                 position = int(self.arg(1)[2:])
             except ValueError:
                 pass
-            else:
-                self.shift()
-        self.fm.open_console(self.rest(1), position=position)
+        elif self.arg(1)[0:2] == '-s':
+            command = self.rest(3)
+            sentinel = self.arg(2)
+            pos = command.find(sentinel)
+            if pos != -1:
+                command = command.replace(sentinel, '', 1)
+                position = pos
+        self.fm.open_console(command, position=position)
 
 
 class load_copy_buffer(Command):
@@ -852,20 +952,19 @@ class load_copy_buffer(Command):
     copy_buffer_filename = 'copy_buffer'
 
     def execute(self):
-        import sys
-        from ranger.container.file import File
         from os.path import exists
+        from ranger.container.file import File
         fname = self.fm.datapath(self.copy_buffer_filename)
-        unreadable = IOError if sys.version_info[0] < 3 else OSError
+        unreadable = OSError if PY3 else IOError
         try:
-            fobj = open(fname, 'r')
+            with open(fname, "r", encoding="utf-8") as fobj:
+                self.fm.copy_buffer = set(
+                    File(g) for g in fobj.read().split("\n") if exists(g)
+                )
         except unreadable:
             return self.fm.notify(
                 "Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
 
-        self.fm.copy_buffer = set(File(g)
-                                  for g in fobj.read().split("\n") if exists(g))
-        fobj.close()
         self.fm.ui.redraw_main_column()
         return None
 
@@ -878,17 +977,15 @@ class save_copy_buffer(Command):
     copy_buffer_filename = 'copy_buffer'
 
     def execute(self):
-        import sys
         fname = None
         fname = self.fm.datapath(self.copy_buffer_filename)
-        unwritable = IOError if sys.version_info[0] < 3 else OSError
+        unwritable = OSError if PY3 else IOError
         try:
-            fobj = open(fname, 'w')
+            with open(fname, "w", encoding="utf-8") as fobj:
+                fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
         except unwritable:
             return self.fm.notify("Cannot open %s" %
                                   (fname or self.copy_buffer_filename), bad=True)
-        fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
-        fobj.close()
         return None
 
 
@@ -928,11 +1025,16 @@ class touch(Command):
     """
 
     def execute(self):
-        from os.path import join, expanduser, lexists
+        from os.path import join, expanduser, lexists, dirname
+        from os import makedirs
 
         fname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
+        dirname = dirname(fname)
         if not lexists(fname):
-            open(fname, 'a').close()
+            if not lexists(dirname):
+                makedirs(dirname)
+            with open(fname, 'a', encoding="utf-8"):
+                pass  # Just create the file
         else:
             self.fm.notify("file/directory exists!", bad=True)
 
@@ -1124,26 +1226,31 @@ class bulkrename(Command):
     After you close it, it will be executed.
     """
 
+    def __init__(self, *args, **kwargs):
+        super(bulkrename, self).__init__(*args, **kwargs)
+        self.flags, _ = self.parse_flags()
+        if not self.flags:
+            self.flags = "w"
+
     def execute(self):
         # pylint: disable=too-many-locals,too-many-statements,too-many-branches
-        import sys
         import tempfile
         from ranger.container.file import File
         from ranger.ext.shell_escape import shell_escape as esc
-        py3 = sys.version_info[0] >= 3
 
         # Create and edit the file list
         filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
         with tempfile.NamedTemporaryFile(delete=False) as listfile:
             listpath = listfile.name
-            if py3:
+            if PY3:
                 listfile.write("\n".join(filenames).encode(
                     encoding="utf-8", errors="surrogateescape"))
             else:
                 listfile.write("\n".join(filenames))
         self.fm.execute_file([File(listpath)], app='editor')
-        with (open(listpath, 'r', encoding="utf-8", errors="surrogateescape") if
-              py3 else open(listpath, 'r')) as listfile:
+        with open(
+            listpath, "r", encoding="utf-8", errors="surrogateescape"
+        ) as listfile:
             new_filenames = listfile.read().split("\n")
         os.unlink(listpath)
         if all(a == b for a, b in zip(filenames, new_filenames)):
@@ -1170,7 +1277,7 @@ class bulkrename(Command):
                         old=esc(old), new=esc(new)))
             # Make sure not to forget the ending newline
             script_content = "\n".join(script_lines) + "\n"
-            if py3:
+            if PY3:
                 cmdfile.write(script_content.encode(encoding="utf-8",
                                                     errors="surrogateescape"))
             else:
@@ -1184,7 +1291,7 @@ class bulkrename(Command):
             script_was_edited = (script_content != cmdfile.read())
 
             # Do the renaming
-            self.fm.run(['/bin/sh', cmdfile.name], flags='w')
+            self.fm.run(['/bin/sh', cmdfile.name], flags=self.flags)
 
         # Retag the files, but only if the script wasn't changed during review,
         # because only then we know which are the source and destination files.
@@ -1458,22 +1565,21 @@ class scout(Command):
     Multiple flags can be combined.  For example, ":scout -gpt" would create
     a :filter-like command using globbing.
     """
-    # pylint: disable=bad-whitespace
-    AUTO_OPEN     = 'a'
-    OPEN_ON_ENTER = 'e'
-    FILTER        = 'f'
-    SM_GLOB       = 'g'
-    IGNORE_CASE   = 'i'
-    KEEP_OPEN     = 'k'
-    SM_LETTERSKIP = 'l'
-    MARK          = 'm'
-    UNMARK        = 'M'
-    PERM_FILTER   = 'p'
-    SM_REGEX      = 'r'
-    SMART_CASE    = 's'
-    AS_YOU_TYPE   = 't'
-    INVERT        = 'v'
-    # pylint: enable=bad-whitespace
+
+    AUTO_OPEN = "a"
+    OPEN_ON_ENTER = "e"
+    FILTER = "f"
+    SM_GLOB = "g"
+    IGNORE_CASE = "i"
+    KEEP_OPEN = "k"
+    SM_LETTERSKIP = "l"
+    MARK = "m"
+    UNMARK = "M"
+    PERM_FILTER = "p"
+    SM_REGEX = "r"
+    SMART_CASE = "s"
+    AS_YOU_TYPE = "t"
+    INVERT = "v"
 
     def __init__(self, *args, **kwargs):
         super(scout, self).__init__(*args, **kwargs)
@@ -1704,7 +1810,7 @@ class filter_stack(Command):
             return
         else:
             self.fm.notify(
-                "Unknown subcommand: {}".format(subcommand),
+                "Unknown subcommand: {sub}".format(sub=subcommand),
                 bad=True
             )
             return
@@ -1720,7 +1826,7 @@ class grep(Command):
 
     def execute(self):
         if self.rest(1):
-            action = ['grep', '--line-number']
+            action = ['grep', '-n']
             action.extend(['-e', self.rest(1), '-r'])
             action.extend(f.path for f in self.fm.thistab.get_selection())
             self.fm.execute_command(action, flags='p')
@@ -1852,7 +1958,7 @@ class meta(prompt_metadata):
 
     def execute(self):
         key = self.arg(1)
-        update_dict = dict()
+        update_dict = {}
         update_dict[key] = self.rest(2)
         selection = self.fm.thistab.get_selection()
         for fobj in selection:
@@ -1897,7 +2003,7 @@ class linemode(default_linemode):
 
 
 class yank(Command):
-    """:yank [name|dir|path]
+    """:yank [name|dir|path|name_without_extension]
 
     Copies the file's name (default), directory or path into both the primary X
     selection and the clipboard.
@@ -1932,7 +2038,7 @@ class yank(Command):
                     ['pbcopy'],
                 ],
             }
-            ordered_managers = ['pbcopy', 'wl-copy', 'xclip', 'xsel']
+            ordered_managers = ['pbcopy', 'xclip', 'xsel', 'wl-copy']
             executables = get_executables()
             for manager in ordered_managers:
                 if manager in executables:
@@ -1946,9 +2052,10 @@ class yank(Command):
 
         new_clipboard_contents = "\n".join(selection)
         for command in clipboard_commands:
-            process = subprocess.Popen(command, universal_newlines=True,
-                                       stdin=subprocess.PIPE)
-            process.communicate(input=new_clipboard_contents)
+            with subprocess.Popen(
+                command, universal_newlines=True, stdin=subprocess.PIPE
+            ) as process:
+                process.communicate(input=new_clipboard_contents)
 
     def get_selection_attr(self, attr):
         return [getattr(item, attr) for item in
diff --git a/ranger/config/commands_sample.py b/ranger/config/commands_sample.py
index 97b79096..20317566 100644
--- a/ranger/config/commands_sample.py
+++ b/ranger/config/commands_sample.py
@@ -48,7 +48,7 @@ class my_edit(Command):
             self.fm.notify("The given file does not exist!", bad=True)
             return
 
-        # This executes a function from ranger.core.acitons, a module with a
+        # This executes a function from ranger.core.actions, a module with a
         # variety of subroutines that can help you construct commands.
         # Check out the source, or run "pydoc ranger.core.actions" for a list.
         self.fm.edit_file(target_filename)
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 9d08a6a7..e2dfc29f 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -25,7 +25,6 @@
 #     multipane: Midnight-commander like multipane view showing all tabs next
 #                to each other
 set viewmode miller
-#set viewmode multipane
 
 # How many columns are there, and what are their relative widths?
 set column_ratios 1,3,4
@@ -334,6 +333,7 @@ alias qall! quitall!
 alias setl  setlocal
 
 alias filter     scout -prts
+alias hide       scout -prtsv
 alias find       scout -aets
 alias mark       scout -mr
 alias unmark     scout -Mr
@@ -467,8 +467,8 @@ map g? cd /usr/share/doc/ranger
 
 # External Programs
 map E  edit
-map du shell -p du --max-depth=1 -h --apparent-size
-map dU shell -p du --max-depth=1 -h --apparent-size | sort -rh
+map du shell -p (du --max-depth=1 --human-readable --apparent-size || du -d 1 -h) 2>/dev/null
+map dU shell -p (du --max-depth=1 --human-readable --apparent-size || du -d 1 -h) 2>/dev/null | sort -rh
 map yp yank path
 map yd yank dir
 map yn yank name
@@ -620,20 +620,20 @@ map m<any>  set_bookmark %any
 map um<any> unset_bookmark %any
 
 map m<bg>   draw_bookmarks
-copymap m<bg>  um<bg> `<bg> '<bg>
+copymap m<bg>  um<bg> `<bg> '<bg> p`<bg> p'<bg>
 
 # Generate all the chmod bindings with some python help:
 eval for arg in "rwxXst": cmd("map +u{0} shell -f chmod u+{0} %s".format(arg))
 eval for arg in "rwxXst": cmd("map +g{0} shell -f chmod g+{0} %s".format(arg))
 eval for arg in "rwxXst": cmd("map +o{0} shell -f chmod o+{0} %s".format(arg))
 eval for arg in "rwxXst": cmd("map +a{0} shell -f chmod a+{0} %s".format(arg))
-eval for arg in "rwxXst": cmd("map +{0}  shell -f chmod u+{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map +{0}  shell -f chmod +{0} %s".format(arg))
 
 eval for arg in "rwxXst": cmd("map -u{0} shell -f chmod u-{0} %s".format(arg))
 eval for arg in "rwxXst": cmd("map -g{0} shell -f chmod g-{0} %s".format(arg))
 eval for arg in "rwxXst": cmd("map -o{0} shell -f chmod o-{0} %s".format(arg))
 eval for arg in "rwxXst": cmd("map -a{0} shell -f chmod a-{0} %s".format(arg))
-eval for arg in "rwxXst": cmd("map -{0}  shell -f chmod u-{0} %s".format(arg))
+eval for arg in "rwxXst": cmd("map -{0}  shell -f chmod -{0} %s".format(arg))
 
 # ===================================================================
 # == Define keys for the console
@@ -671,6 +671,8 @@ cmap <A-d>        eval fm.ui.console.delete_word(backward=False)
 cmap <C-k>        eval fm.ui.console.delete_rest(1)
 cmap <C-u>        eval fm.ui.console.delete_rest(-1)
 cmap <C-y>        eval fm.ui.console.paste()
+cmap <C-t>        eval fm.ui.console.transpose_chars()
+cmap <A-t>        eval fm.ui.console.transpose_words()
 
 # And of course the emacs way
 copycmap <ESC>       <C-g>
diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf
index 86f53fd1..432b2281 100644
--- a/ranger/config/rifle.conf
+++ b/ranger/config/rifle.conf
@@ -86,15 +86,15 @@ ext x?html?, has w3m,               terminal = w3m "$@"
 #-------------------------------------------
 # Define the "editor" for text files as first action
 mime ^text,  label editor = ${VISUAL:-$EDITOR} -- "$@"
-mime ^text,  label pager  = "$PAGER" -- "$@"
-!mime ^text, label editor, ext xml|json|csv|tex|py|pl|rb|js|sh|php = ${VISUAL:-$EDITOR} -- "$@"
-!mime ^text, label pager,  ext xml|json|csv|tex|py|pl|rb|js|sh|php = "$PAGER" -- "$@"
+mime ^text,  label pager  = $PAGER -- "$@"
+!mime ^text, label editor, ext xml|json|csv|tex|py|pl|rb|rs|js|sh|php|dart = ${VISUAL:-$EDITOR} -- "$@"
+!mime ^text, label pager,  ext xml|json|csv|tex|py|pl|rb|rs|js|sh|php|dart = $PAGER -- "$@"
 
 ext 1                         = man "$1"
 ext s[wmf]c, has zsnes, X     = zsnes "$1"
 ext s[wmf]c, has snes9x-gtk,X = snes9x-gtk "$1"
 ext nes, has fceux, X         = fceux "$1"
-ext exe                       = wine "$1"
+ext exe, has wine             = wine "$1"
 name ^[mM]akefile$            = make
 
 #--------------------------------------------
@@ -106,6 +106,7 @@ ext rb  = ruby -- "$1"
 ext js  = node -- "$1"
 ext sh  = sh -- "$1"
 ext php = php -- "$1"
+ext dart = dart run -- "$1"
 
 #--------------------------------------------
 # Audio without X
@@ -118,17 +119,19 @@ ext midi?,        terminal, has wildmidi = wildmidi -- "$@"
 #--------------------------------------------
 # Video/Audio with a GUI
 #-------------------------------------------
-mime ^video|audio, has gmplayer, X, flag f = gmplayer -- "$@"
-mime ^video|audio, has smplayer, X, flag f = smplayer "$@"
-mime ^video,       has mpv,      X, flag f = mpv -- "$@"
-mime ^video,       has mpv,      X, flag f = mpv --fs -- "$@"
-mime ^video,       has mplayer2, X, flag f = mplayer2 -- "$@"
-mime ^video,       has mplayer2, X, flag f = mplayer2 -fs -- "$@"
-mime ^video,       has mplayer,  X, flag f = mplayer -- "$@"
-mime ^video,       has mplayer,  X, flag f = mplayer -fs -- "$@"
-mime ^video|audio, has vlc,      X, flag f = vlc -- "$@"
-mime ^video|audio, has totem,    X, flag f = totem -- "$@"
-mime ^video|audio, has totem,    X, flag f = totem --fullscreen -- "$@"
+mime ^video|^audio, has gmplayer, X, flag f = gmplayer -- "$@"
+mime ^video|^audio, has smplayer, X, flag f = smplayer "$@"
+mime ^video,        has mpv,      X, flag f = mpv -- "$@"
+mime ^video,        has mpv,      X, flag f = mpv --fs -- "$@"
+mime ^video,        has mplayer2, X, flag f = mplayer2 -- "$@"
+mime ^video,        has mplayer2, X, flag f = mplayer2 -fs -- "$@"
+mime ^video,        has mplayer,  X, flag f = mplayer -- "$@"
+mime ^video,        has mplayer,  X, flag f = mplayer -fs -- "$@"
+mime ^video|^audio, has vlc,      X, flag f = vlc -- "$@"
+mime ^video|^audio, has totem,    X, flag f = totem -- "$@"
+mime ^video|^audio, has totem,    X, flag f = totem --fullscreen -- "$@"
+mime ^audio,        has audacity, X, flag f = audacity -- "$@"
+ext aup,            has audacity, X, flag f = audacity -- "$@"
 
 #--------------------------------------------
 # Video without X
@@ -153,7 +156,8 @@ ext pdf, has epdfview, X, flag f = epdfview -- "$@"
 ext pdf, has qpdfview, X, flag f = qpdfview "$@"
 ext pdf, has open,     X, flag f = open "$@"
 
-ext docx?, has catdoc,       terminal = catdoc -- "$@" | "$PAGER"
+ext sc,    has sc,                    = sc -- "$@"
+ext docx?, has catdoc,       terminal = catdoc -- "$@" | $PAGER
 
 ext                        sxc|xlsx?|xlt|xlw|gnm|gnumeric, has gnumeric,    X, flag f = gnumeric -- "$@"
 ext                        sxc|xlsx?|xlt|xlw|gnm|gnumeric, has kspread,     X, flag f = kspread -- "$@"
@@ -171,19 +175,25 @@ ext epub, has zathura,      X, flag f = zathura -- "$@"
 ext epub, has mupdf,        X, flag f = mupdf -- "$@"
 ext mobi, has ebook-viewer, X, flag f = ebook-viewer -- "$@"
 
-ext cbr,  has zathura,      X, flag f = zathura -- "$@"
-ext cbz,  has zathura,      X, flag f = zathura -- "$@"
+ext cb[rz], has qcomicbook, X, flag f = qcomicbook "$@"
+ext cb[rz], has mcomix,     X, flag f = mcomix -- "$@"
+ext cb[rz], has zathura,    X, flag f = zathura -- "$@"
+ext cb[rz], has atril,      X, flag f = atril -- "$@"
+
+ext sla,  has scribus,      X, flag f = scribus -- "$@"
 
 #-------------------------------------------
 # Images
 #-------------------------------------------
+mime ^image, has viewnior,  X, flag f = viewnior -- "$@"
+
 mime ^image/svg, has inkscape, X, flag f = inkscape -- "$@"
 mime ^image/svg, has display,  X, flag f = display -- "$@"
 
 mime ^image, has imv,       X, flag f = imv -- "$@"
 mime ^image, has pqiv,      X, flag f = pqiv -- "$@"
 mime ^image, has sxiv,      X, flag f = sxiv -- "$@"
-mime ^image, has feh,       X, flag f = feh -- "$@"
+mime ^image, has feh,       X, flag f, !ext gif = feh -- "$@"
 mime ^image, has mirage,    X, flag f = mirage -- "$@"
 mime ^image, has ristretto, X, flag f = ristretto "$@"
 mime ^image, has eog,       X, flag f = eog -- "$@"
@@ -192,7 +202,10 @@ mime ^image, has nomacs,    X, flag f = nomacs -- "$@"
 mime ^image, has geeqie,    X, flag f = geeqie -- "$@"
 mime ^image, has gpicview,  X, flag f = gpicview -- "$@"
 mime ^image, has gwenview,  X, flag f = gwenview -- "$@"
+mime ^image, has mcomix,    X, flag f = mcomix -- "$@"
 mime ^image, has gimp,      X, flag f = gimp -- "$@"
+mime ^image, has krita,     X, flag f = krita -- "$@"
+ext kra,     has krita,     X, flag f = krita -- "$@"
 ext xcf,                    X, flag f = gimp -- "$@"
 
 #-------------------------------------------
@@ -200,15 +213,15 @@ ext xcf,                    X, flag f = gimp -- "$@"
 #-------------------------------------------
 
 # avoid password prompt by providing empty password
-ext 7z, has 7z = 7z -p l "$@" | "$PAGER"
+ext 7z, has 7z = 7z -p l "$@" | $PAGER
 # This requires atool
-ext ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz,     has atool = atool --list --each -- "$@" | "$PAGER"
-ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has atool = atool --list --each -- "$@" | "$PAGER"
+ext ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz,     has atool = atool --list --each -- "$@" | $PAGER
+ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has atool = atool --list --each -- "$@" | $PAGER
 ext 7z|ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz,  has atool = atool --extract --each -- "$@"
 ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has atool = atool --extract --each -- "$@"
 
 # Listing and extracting archives without atool:
-ext tar|gz|bz2|xz, has tar = tar vvtf "$1" | "$PAGER"
+ext tar|gz|bz2|xz, has tar = tar vvtf "$1" | $PAGER
 ext tar|gz|bz2|xz, has tar = for file in "$@"; do tar vvxf "$file"; done
 ext bz2, has bzip2 = for file in "$@"; do bzip2 -dk "$file"; done
 ext zip, has unzip = unzip -l "$1" | less
@@ -217,6 +230,9 @@ ext ace, has unace = unace l "$1" | less
 ext ace, has unace = for file in "$@"; do unace e "$file"; done
 ext rar, has unrar = unrar l "$1" | less
 ext rar, has unrar = for file in "$@"; do unrar x "$file"; done
+ext rar|zip, has qcomicbook, X, flag f = qcomicbook "$@"
+ext rar|zip, has mcomix,     X, flag f = mcomix -- "$@"
+ext rar|zip, has zathura,    X, flag f = zathura -- "$@"
 
 #-------------------------------------------
 # Fonts
@@ -262,13 +278,13 @@ 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 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" -- "$@"
+              !mime ^text, !ext xml|json|csv|tex|py|pl|rb|rs|js|sh|php|dart  = ask
+label editor, !mime ^text, !ext xml|json|csv|tex|py|pl|rb|rs|js|sh|php|dart  = ${VISUAL:-$EDITOR} -- "$@"
+label pager,  !mime ^text, !ext xml|json|csv|tex|py|pl|rb|rs|js|sh|php|dart  = $PAGER -- "$@"
 
 
 ######################################################################
@@ -281,4 +297,4 @@ 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
+label trash = mkdir -p -- "${XDG_DATA_HOME:-$HOME/.local/share}/ranger/trash"; mv -- "$@" "${XDG_DATA_HOME:-$HOME/.local/share}/ranger/trash"
diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py
index 7b5e56b7..293bb01a 100644
--- a/ranger/container/bookmarks.py
+++ b/ranger/container/bookmarks.py
@@ -6,7 +6,9 @@ from __future__ import (absolute_import, division, print_function)
 import string
 import re
 import os
+from io import open
 
+from ranger import PY3
 from ranger.core.shared import FileManagerAware
 
 ALLOWED_KEYS = string.ascii_letters + string.digits + "`'"
@@ -94,7 +96,7 @@ class Bookmarks(FileManagerAware):
             else:
                 raise KeyError("Cannot open bookmark: `%s'!" % key)
         else:
-            raise KeyError("Nonexistant Bookmark: `%s'!" % key)
+            raise KeyError("Nonexistent Bookmark: `%s'!" % key)
 
     def __setitem__(self, key, value):
         """Bookmark <value> to the key <key>.
@@ -168,22 +170,25 @@ class Bookmarks(FileManagerAware):
     def save(self):
         """Save the bookmarks to the bookmarkfile.
 
-        This is done automatically after every modification if autosave is True."""
+        This is done automatically after every modification if autosave is True.
+        """
         self.update()
         if self.path is None:
             return
 
         path_new = self.path + '.new'
         try:
-            fobj = open(path_new, 'w')
+            with open(path_new, 'w', encoding="utf-8") as fobj:
+                for key, value in self.dct.items():
+                    if key in ALLOWED_KEYS \
+                            and key not in self.nonpersistent_bookmarks:
+                        key_value = "{0}:{1}\n".format(key, value)
+                        if not PY3 and isinstance(key_value, str):
+                            key_value = key_value.decode("utf-8")
+                        fobj.write(key_value)
         except OSError as ex:
             self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True)
             return
-        for key, value in self.dct.items():
-            if isinstance(key, str) and key in ALLOWED_KEYS \
-                    and key not in self.nonpersistent_bookmarks:
-                fobj.write("{0}:{1}\n".format(str(key), str(value)))
-        fobj.close()
 
         try:
             old_perms = os.stat(self.path)
@@ -218,24 +223,24 @@ class Bookmarks(FileManagerAware):
 
         if not os.path.exists(self.path):
             try:
-                with open(self.path, 'w') as fobj:
+                with open(self.path, 'w', encoding="utf-8") as fobj:
                     pass
             except OSError as ex:
                 self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True)
                 return None
 
         try:
-            fobj = open(self.path, 'r')
+            with open(self.path, 'r', encoding="utf-8") as fobj:
+                dct = {}
+                for line in fobj:
+                    if self.load_pattern.match(line):
+                        key, value = line[0], line[2:-1]
+                        if key in ALLOWED_KEYS:
+                            dct[key] = self.bookmarktype(value)
         except OSError as ex:
             self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True)
             return None
-        dct = {}
-        for line in fobj:
-            if self.load_pattern.match(line):
-                key, value = line[0], line[2:-1]
-                if key in ALLOWED_KEYS:
-                    dct[key] = self.bookmarktype(value)
-        fobj.close()
+
         return dct
 
     def _set_dict(self, dct, original):
diff --git a/ranger/container/directory.py b/ranger/container/directory.py
index 81dabb24..bcbfc4e6 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -74,7 +74,7 @@ def accept_file(fobj, filters):
 
 def walklevel(some_dir, level):
     some_dir = some_dir.rstrip(os.path.sep)
-    followlinks = True if level > 0 else False
+    followlinks = level > 0
     assert os.path.isdir(some_dir)
     num_sep = some_dir.count(os.path.sep)
     for root, dirs, files in os.walk(some_dir, followlinks=followlinks):
@@ -254,7 +254,7 @@ class Directory(  # pylint: disable=too-many-instance-attributes,too-many-public
 
     def refilter(self):
         if self.files_all is None:
-            return  # propably not loaded yet
+            return  # probably not loaded yet
 
         self.last_update_time = time()
 
@@ -508,6 +508,7 @@ class Directory(  # pylint: disable=too-many-instance-attributes,too-many-public
 
     def sort(self):
         """Sort the contained files"""
+        # pylint: disable=comparison-with-callable
         if self.files_all is None:
             return
 
diff --git a/ranger/container/file.py b/ranger/container/file.py
index 6450cfe6..4cc29887 100644
--- a/ranger/container/file.py
+++ b/ranger/container/file.py
@@ -4,13 +4,13 @@
 from __future__ import (absolute_import, division, print_function)
 
 import re
-import sys
 
+from ranger import PY3
 from ranger.container.fsobject import FileSystemObject
 
 N_FIRST_BYTES = 256
 CONTROL_CHARACTERS = set(list(range(0, 9)) + list(range(14, 32)))
-if sys.version_info[0] < 3:
+if not PY3:
     CONTROL_CHARACTERS = set(chr(n) for n in CONTROL_CHARACTERS)
 
 # Don't even try to preview files which match this regular expression:
@@ -86,7 +86,7 @@ class File(FileSystemObject):
             return True
         if PREVIEW_BLACKLIST.search(self.basename):
             return False
-        if self.path == '/dev/core' or self.path == '/proc/kcore':
+        if self.path in ('/dev/core', '/proc/kcore'):
             return False
         if self.is_binary():
             return False
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index 7de889bf..51eb5376 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -296,7 +296,7 @@ class FileSystemObject(  # pylint: disable=too-many-instance-attributes,too-many
             if self.is_link:
                 new_stat = self.preload[0]
             self.preload = None
-            self.exists = True if new_stat else False
+            self.exists = bool(new_stat)
         else:
             try:
                 new_stat = lstat(path)
@@ -309,11 +309,11 @@ class FileSystemObject(  # pylint: disable=too-many-instance-attributes,too-many
 
         # Set some attributes
 
-        self.accessible = True if new_stat else False
+        self.accessible = bool(new_stat)
         mode = new_stat.st_mode if new_stat else 0
 
         fmt = mode & 0o170000
-        if fmt == 0o020000 or fmt == 0o060000:  # stat.S_IFCHR/BLK
+        if fmt in (0o020000, 0o060000):  # stat.S_IFCHR/BLK
             self.is_device = True
             self.size = 0
             self.infostring = 'dev'
diff --git a/ranger/container/history.py b/ranger/container/history.py
index 9361e373..93a5b1fe 100644
--- a/ranger/container/history.py
+++ b/ranger/container/history.py
@@ -108,17 +108,11 @@ class History(object):
             raise HistoryEmptyException
 
     def back(self):
-        self.index -= 1
-        if self.index < 0:
-            self.index = 0
+        self.index = max(0, self.index - 1)
         return self.current()
 
     def move(self, n):
-        self.index += n
-        if self.index > len(self.history) - 1:
-            self.index = len(self.history) - 1
-        if self.index < 0:
-            self.index = 0
+        self.index = max(0, min(len(self.history) - 1, self.index + n))
         return self.current()
 
     def search(self, string, n):
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index 7549325a..80250d4f 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -14,13 +14,11 @@ from ranger.gui.colorscheme import _colorscheme_name_to_class
 
 # Use these priority constants to trigger events at specific points in time
 # during processing of the signals "setopt" and "setopt.<some_setting_name>"
-# pylint: disable=bad-whitespace
-SIGNAL_PRIORITY_RAW        = 2.0  # signal.value will be raw
-SIGNAL_PRIORITY_SANITIZE   = 1.0  # (Internal) post-processing signal.value
-SIGNAL_PRIORITY_BETWEEN    = 0.6  # sanitized signal.value, old fm.settings.XYZ
-SIGNAL_PRIORITY_SYNC       = 0.2  # (Internal) updating fm.settings.XYZ
+SIGNAL_PRIORITY_RAW = 2.0  # signal.value will be raw
+SIGNAL_PRIORITY_SANITIZE = 1.0  # (Internal) post-processing signal.value
+SIGNAL_PRIORITY_BETWEEN = 0.6  # sanitized signal.value, old fm.settings.XYZ
+SIGNAL_PRIORITY_SYNC = 0.2  # (Internal) updating fm.settings.XYZ
 SIGNAL_PRIORITY_AFTER_SYNC = 0.1  # after fm.settings.XYZ was updated
-# pylint: enable=bad-whitespace
 
 
 ALLOWED_SETTINGS = {
@@ -42,6 +40,7 @@ ALLOWED_SETTINGS = {
     "display_free_space_in_status_bar": bool,
     'display_tags_in_all_columns': bool,
     'draw_borders': str,
+    'draw_borders_multipane': str,
     'draw_progress_bar_in_status_bar': bool,
     'flushinput': bool,
     'freeze_files': bool,
@@ -107,6 +106,8 @@ ALLOWED_VALUES = {
     'cd_tab_case': ['sensitive', 'insensitive', 'smart'],
     'confirm_on_delete': ['multiple', 'always', 'never'],
     'draw_borders': ['none', 'both', 'outline', 'separators'],
+    'draw_borders_multipane': [None, 'none', 'both', 'outline',
+                               'separators', 'active-pane'],
     'line_numbers': ['false', 'absolute', 'relative'],
     'nested_ranger_warning': ['true', 'false', 'error'],
     'one_indexed': [False, True],
@@ -135,10 +136,10 @@ class Settings(SignalDispatcher, FileManagerAware):
 
     def __init__(self):
         SignalDispatcher.__init__(self)
-        self.__dict__['_localsettings'] = dict()
-        self.__dict__['_localregexes'] = dict()
-        self.__dict__['_tagsettings'] = dict()
-        self.__dict__['_settings'] = dict()
+        self.__dict__['_localsettings'] = {}
+        self.__dict__['_localregexes'] = {}
+        self.__dict__['_tagsettings'] = {}
+        self.__dict__['_settings'] = {}
         for name in ALLOWED_SETTINGS:
             self.signal_bind('setopt.' + name, self._sanitize,
                              priority=SIGNAL_PRIORITY_SANITIZE)
@@ -280,11 +281,11 @@ class Settings(SignalDispatcher, FileManagerAware):
         if path:
             if path not in self._localsettings:
                 try:
-                    regex = re.compile(re.escape(path))
+                    regex = re.compile(path)
                 except re.error:  # Bad regular expression
                     return
                 self._localregexes[path] = regex
-                self._localsettings[path] = dict()
+                self._localsettings[path] = {}
             self._localsettings[path][name] = value
 
             # make sure name is in _settings, so __iter__ runs through
@@ -296,7 +297,7 @@ class Settings(SignalDispatcher, FileManagerAware):
         elif tags:
             for tag in tags:
                 if tag not in self._tagsettings:
-                    self._tagsettings[tag] = dict()
+                    self._tagsettings[tag] = {}
                 self._tagsettings[tag][name] = value
         else:
             self._settings[name] = value
diff --git a/ranger/container/tags.py b/ranger/container/tags.py
index dabaf6a8..4d4e6c59 100644
--- a/ranger/container/tags.py
+++ b/ranger/container/tags.py
@@ -5,39 +5,43 @@
 
 from __future__ import (absolute_import, division, print_function)
 
-from os.path import isdir, exists, dirname, abspath, realpath, expanduser, sep
 import string
-import sys
+from io import open
+from os.path import exists, abspath, realpath, expanduser, sep
+
+from ranger.core.shared import FileManagerAware
 
 ALLOWED_KEYS = string.ascii_letters + string.digits + string.punctuation
 
 
-class Tags(object):
+class Tags(FileManagerAware):
     default_tag = '*'
 
     def __init__(self, filename):
 
+        # COMPAT: The intent is to get abspath/normpath's behavior of
+        # collapsing `symlink/..`, abspath is retained for historical reasons
+        # because the documentation states its behavior isn't necessarily in
+        # line with normpath's.
         self._filename = realpath(abspath(expanduser(filename)))
 
-        if isdir(dirname(self._filename)) and not exists(self._filename):
-            open(self._filename, 'w')
-
         self.sync()
 
     def __contains__(self, item):
         return item in self.tags
 
     def add(self, *items, **others):
-        if 'tag' in others:
-            tag = others['tag']
-        else:
-            tag = self.default_tag
+        if len(items) == 0:
+            return
+        tag = others.get('tag', self.default_tag)
         self.sync()
         for item in items:
             self.tags[item] = tag
         self.dump()
 
     def remove(self, *items):
+        if len(items) == 0:
+            return
         self.sync()
         for item in items:
             try:
@@ -47,10 +51,9 @@ class Tags(object):
         self.dump()
 
     def toggle(self, *items, **others):
-        if 'tag' in others:
-            tag = others['tag']
-        else:
-            tag = self.default_tag
+        if len(items) == 0:
+            return
+        tag = others.get('tag', self.default_tag)
         tag = str(tag)
         if tag not in ALLOWED_KEYS:
             return
@@ -72,24 +75,22 @@ class Tags(object):
 
     def sync(self):
         try:
-            if sys.version_info[0] >= 3:
-                fobj = open(self._filename, 'r', errors='replace')
+            with open(
+                self._filename, "r", encoding="utf-8", errors="replace"
+            ) as fobj:
+                self.tags = self._parse(fobj)
+        except (OSError, IOError) as err:
+            if exists(self._filename):
+                self.fm.notify(err, bad=True)
             else:
-                fobj = open(self._filename, 'r')
-        except OSError:
-            pass
-        else:
-            self.tags = self._parse(fobj)
-            fobj.close()
+                self.tags = {}
 
     def dump(self):
         try:
-            fobj = open(self._filename, 'w')
-        except OSError:
-            pass
-        else:
-            self._compile(fobj)
-            fobj.close()
+            with open(self._filename, 'w', encoding="utf-8") as fobj:
+                self._compile(fobj)
+        except OSError as err:
+            self.fm.notify(err, bad=True)
 
     def _compile(self, fobj):
         for path, tag in self.tags.items():
@@ -100,7 +101,7 @@ class Tags(object):
                 fobj.write('{0}:{1}\n'.format(tag, path))
 
     def _parse(self, fobj):
-        result = dict()
+        result = {}
         for line in fobj:
             line = line.rstrip('\n')
             if len(line) > 2 and line[1] == ':':
@@ -122,6 +123,7 @@ class Tags(object):
             elif path.startswith(path_old + sep):
                 pnew = path_new + path[len(path_old):]
             if pnew:
+                # pylint: disable=unnecessary-dict-index-lookup
                 del self.tags[path]
                 self.tags[pnew] = tag
                 changed = True
@@ -140,7 +142,7 @@ class TagsDummy(Tags):
     """
 
     def __init__(self, filename):  # pylint: disable=super-init-not-called
-        self.tags = dict()
+        self.tags = {}
 
     def __contains__(self, item):
         return False
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index c3d7de86..a552e328 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -7,36 +7,36 @@ from __future__ import (absolute_import, division, print_function)
 
 import codecs
 import os
-from os import link, symlink, listdir, stat
-from os.path import join, isdir, realpath, exists
 import re
 import shlex
 import shutil
 import string
 import tempfile
+from hashlib import sha512
 from inspect import cleandoc
-from stat import S_IEXEC
-from hashlib import sha1
-from sys import version_info
+from io import open
 from logging import getLogger
+from os import link, symlink, listdir, stat
+from os.path import join, isdir, realpath, exists
+from stat import S_IEXEC
 
 import ranger
+from ranger import PY3
+from ranger.container.directory import Directory
+from ranger.container.file import File
+from ranger.container.settings import ALLOWED_SETTINGS, ALLOWED_VALUES
+from ranger.core.loader import CommandLoader, CopyLoader
+from ranger.core.shared import FileManagerAware, SettingsAware
+from ranger.core.tab import Tab
 from ranger.ext.direction import Direction
-from ranger.ext.relative_symlink import relative_symlink
+from ranger.ext.get_executables import get_executables
 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.macrodict import MacroDict, MACRO_FAIL, macro_val
 from ranger.ext.next_available_filename import next_available_filename
+from ranger.ext.relative_symlink import relative_symlink
 from ranger.ext.rifle import squash_flags, ASK_COMMAND
-from ranger.core.shared import FileManagerAware, SettingsAware
-from ranger.core.tab import Tab
-from ranger.container.directory import Directory
-from ranger.container.file import File
-from ranger.core.loader import CommandLoader, CopyLoader
-from ranger.container.settings import ALLOWED_SETTINGS, ALLOWED_VALUES
-
-
-MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
+from ranger.ext.safe_path import get_safe_path
+from ranger.ext.shell_escape import shell_quote
 
 LOG = getLogger(__name__)
 
@@ -242,14 +242,14 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
 
         if cmd.resolve_macros and _MacroTemplate.delimiter in cmd.line:
             def any_macro(i, char):
-                return ('any{:d}'.format(i), key_to_string(char))
+                return ('any{0:d}'.format(i), key_to_string(char))
 
             def anypath_macro(i, char):
                 try:
                     val = self.fm.bookmarks[key_to_string(char)]
                 except KeyError:
                     val = MACRO_FAIL
-                return ('any_path{:d}'.format(i), val)
+                return ('any_path{0:d}'.format(i), val)
 
             macros = dict(f(i, char) for f in (any_macro, anypath_macro)
                           for i, char in enumerate(wildcards if wildcards
@@ -295,8 +295,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             raise ValueError("Could not apply macros to `%s'" % string)
         return result
 
-    def get_macros(self):  # pylint: disable=too-many-branches,too-many-statements
-        macros = {}
+    def get_macros(self):
+        macros = MacroDict()
 
         macros['rangerdir'] = ranger.RANGERDIR
         if not ranger.args.clean:
@@ -304,57 +304,39 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             macros['datadir'] = self.fm.datapath()
         macros['space'] = ' '
 
-        if self.fm.thisfile:
-            macros['f'] = self.fm.thisfile.relative_path
-        else:
-            macros['f'] = MACRO_FAIL
+        macros['f'] = lambda: self.fm.thisfile.relative_path
 
-        if self.fm.thistab.get_selection:
-            macros['p'] = [os.path.join(self.fm.thisdir.path, fl.relative_path)
-                           for fl in self.fm.thistab.get_selection()]
-            macros['s'] = [fl.relative_path for fl in self.fm.thistab.get_selection()]
-        else:
-            macros['p'] = MACRO_FAIL
-            macros['s'] = MACRO_FAIL
+        macros['p'] = lambda: [os.path.join(self.fm.thisdir.path,
+                                            fl.relative_path) for fl in
+                               self.fm.thistab.get_selection()]
+        macros['s'] = lambda: [fl.relative_path for fl in
+                               self.fm.thistab.get_selection()]
 
-        if self.fm.copy_buffer:
-            macros['c'] = [fl.path for fl in self.fm.copy_buffer]
-        else:
-            macros['c'] = MACRO_FAIL
+        macros['c'] = lambda: [fl.path for fl in self.fm.copy_buffer]
 
-        if self.fm.thisdir.files:
-            macros['t'] = [fl.relative_path for fl in self.fm.thisdir.files
-                           if fl.realpath in self.fm.tags or []]
-        else:
-            macros['t'] = MACRO_FAIL
+        macros['t'] = lambda: [fl.relative_path for fl in
+                               self.fm.thisdir.files if fl.realpath in
+                               self.fm.tags]
 
-        if self.fm.thisdir:
-            macros['d'] = self.fm.thisdir.path
-        else:
-            macros['d'] = '.'
+        macros['d'] = macro_val(lambda: self.fm.thisdir.path, fallback='.')
 
         # define d/f/p/s macros for each tab
         for i in range(1, 10):
+            # pylint: disable=cell-var-from-loop
             try:
                 tab = self.fm.tabs[i]
-            except KeyError:
+                tabdir = tab.thisdir
+            except (KeyError, AttributeError):
                 continue
-            tabdir = tab.thisdir
             if not tabdir:
                 continue
             i = str(i)
-            macros[i + 'd'] = tabdir.path
-            if tabdir.get_selection():
-                macros[i + 'p'] = [os.path.join(tabdir.path, fl.relative_path)
-                                   for fl in tabdir.get_selection()]
-                macros[i + 's'] = [fl.path for fl in tabdir.get_selection()]
-            else:
-                macros[i + 'p'] = MACRO_FAIL
-                macros[i + 's'] = MACRO_FAIL
-            if tabdir.pointed_obj:
-                macros[i + 'f'] = tabdir.pointed_obj.path
-            else:
-                macros[i + 'f'] = MACRO_FAIL
+            macros[i + 'd'] = lambda: tabdir.path
+            macros[i + 'p'] = lambda: [os.path.join(tabdir.path,
+                                                    fl.relative_path) for fl in
+                                       tabdir.get_selection()]
+            macros[i + 's'] = lambda: [fl.path for fl in tabdir.get_selection()]
+            macros[i + 'f'] = lambda: tabdir.pointed_obj.path
 
         # define D/F/P/S for the next tab
         found_current_tab = False
@@ -370,25 +352,16 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 found_current_tab = True
         if found_current_tab and next_tab is None:
             next_tab = self.fm.tabs[first_tab]
-        next_tab_dir = next_tab.thisdir
+        try:
+            next_tab_dir = next_tab.thisdir
+        except AttributeError:
+            return macros
 
-        if next_tab_dir:
-            macros['D'] = str(next_tab_dir.path)
-            if next_tab.thisfile:
-                macros['F'] = next_tab.thisfile.path
-            else:
-                macros['F'] = MACRO_FAIL
-            if next_tab_dir.get_selection():
-                macros['P'] = [os.path.join(next_tab.path, fl.path)
+        macros['D'] = lambda: str(next_tab_dir.path)
+        macros['F'] = lambda: next_tab.thisfile.path
+        macros['P'] = lambda: [os.path.join(next_tab.path, fl.path)
                                for fl in next_tab.get_selection()]
-                macros['S'] = [fl.path for fl in next_tab.get_selection()]
-            else:
-                macros['P'] = MACRO_FAIL
-                macros['S'] = MACRO_FAIL
-        else:
-            macros['D'] = MACRO_FAIL
-            macros['F'] = MACRO_FAIL
-            macros['S'] = MACRO_FAIL
+        macros['S'] = lambda: [fl.path for fl in next_tab.get_selection()]
 
         return macros
 
@@ -399,12 +372,15 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         """
         filename = os.path.expanduser(filename)
         LOG.debug("Sourcing config file '%s'", filename)
-        with open(filename, 'r') as fobj:
+        # pylint: disable=unspecified-encoding
+        with open(filename, 'r', encoding="utf-8") as fobj:
             for line in fobj:
                 line = line.strip(" \r\n")
                 if line.startswith("#") or not line.strip():
                     continue
                 try:
+                    if not isinstance(line, str):
+                        line = line.encode("ascii")
                     self.execute_console(line)
                 except Exception as ex:  # pylint: disable=broad-except
                     if ranger.args.debug:
@@ -429,7 +405,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         # ranger can act as a file chooser when running with --choosefile=...
         if mode == 0 and 'label' not in kw:
             if ranger.args.choosefile:
-                with open(ranger.args.choosefile, 'w') as fobj:
+                with open(
+                    ranger.args.choosefile, 'w', encoding="utf-8"
+                ) as fobj:
                     fobj.write(self.fm.thisfile.path)
 
             if ranger.args.choosefiles:
@@ -440,7 +418,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                             paths += [fobj.path]
                 paths += [f.path for f in self.fm.thistab.get_selection() if f.path not in paths]
 
-                with open(ranger.args.choosefiles, 'w') as fobj:
+                with open(
+                    ranger.args.choosefiles, 'w', encoding="utf-8"
+                ) as fobj:
                     fobj.write('\n'.join(paths) + '\n')
 
             if ranger.args.choosefile or ranger.args.choosefiles:
@@ -961,8 +941,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         except IndexError:
             self.ui.browser.draw_info = []
             return
-        programs = [program for program in self.rifle.list_commands([target.path], None,
-                                                                    skip_ask=True)]
+        programs = list(self.rifle.list_commands(
+            [target.path], None, skip_ask=True))
         if programs:
             num_digits = max((len(str(program[0])) for program in programs))
             program_info = ['%s | %s' % (str(program[0]).rjust(num_digits), program[1])
@@ -997,14 +977,18 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         pager.set_source(lines)
 
     def display_help(self):
-        manualpath = self.relpath('../doc/ranger.1')
-        if os.path.exists(manualpath):
-            process = self.run(['man', manualpath])
-            if process.poll() != 16:
-                return
-        process = self.run(['man', 'ranger'])
-        if process.poll() == 16:
-            self.notify("Could not find manpage.", bad=True)
+        if 'man' in get_executables():
+            manualpath = self.relpath('../doc/ranger.1')
+            if os.path.exists(manualpath):
+                process = self.run(['man', manualpath])
+                if process.poll() != 16:
+                    return
+            process = self.run(['man', 'ranger'])
+            if process.poll() == 16:
+                self.notify("Could not find manpage.", bad=True)
+        else:
+            self.notify("Failed to show man page, check if man is installed",
+                        bad=True)
 
     def display_log(self):
         logs = list(self.get_log())
@@ -1049,13 +1033,16 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         return True
 
     @staticmethod
-    def sha1_encode(path):
-        if version_info[0] < 3:
-            return os.path.join(ranger.args.cachedir, sha1(path).hexdigest()) + '.jpg'
-        return os.path.join(ranger.args.cachedir,
-                            sha1(path.encode('utf-8', 'backslashreplace')).hexdigest()) + '.jpg'
-
-    def get_preview(self, fobj, width, height):  # pylint: disable=too-many-return-statements
+    def sha512_encode(path, inode=None):
+        if inode is None:
+            inode = stat(path).st_ino
+        inode_path = "{0}{1}".format(str(inode), path)
+        if PY3:
+            inode_path = inode_path.encode('utf-8', 'backslashreplace')
+        return '{0}.jpg'.format(sha512(inode_path).hexdigest())
+
+    def get_preview(self, fobj, width, height):
+        # pylint: disable=too-many-return-statements,too-many-statements
         pager = self.ui.get_pager()
         path = fobj.realpath
 
@@ -1065,6 +1052,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         if not self.settings.preview_script or not self.settings.use_preview_script:
             try:
                 # XXX: properly determine file's encoding
+                # Disable the lint because the preview is read outside the
+                # local scope.
+                # pylint: disable=consider-using-with
                 return codecs.open(path, 'r', errors='ignore')
             # IOError for Python2, OSError for Python3
             except (IOError, OSError):
@@ -1121,10 +1111,13 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
 
         if not os.path.exists(ranger.args.cachedir):
             os.makedirs(ranger.args.cachedir)
-        cacheimg = os.path.join(ranger.args.cachedir, self.sha1_encode(path))
-        if self.settings.preview_images and \
-                os.path.isfile(cacheimg) and \
-                os.path.getmtime(cacheimg) > os.path.getmtime(path):
+        fobj.load_if_outdated()
+        cacheimg = os.path.join(
+            ranger.args.cachedir,
+            self.sha512_encode(path, inode=fobj.stat.st_ino)
+        )
+        if (self.settings.preview_images and os.path.isfile(cacheimg)
+                and fobj.stat.st_mtime <= os.path.getmtime(cacheimg)):
             data['foundpreview'] = True
             data['imagepreview'] = True
             pager.set_image(cacheimg)
@@ -1224,7 +1217,7 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             try:
                 text = codecs.decode(data, encoding, error_scheme)
             except UnicodeDecodeError:
-                pass
+                return None
             else:
                 LOG.debug("Guessed encoding of '%s' as %s", path, encoding)
                 return text
@@ -1260,6 +1253,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             self.signal_emit('tab.change', old=previous_tab, new=self.thistab)
             self.signal_emit('tab.layoutchange')
 
+    def tabopen(self, *args, **kwargs):
+        return self.tab_open(*args, **kwargs)
+
     def tab_close(self, name=None):
         if name is None:
             name = self.current_tab
@@ -1275,6 +1271,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         self.restorable_tabs.append(tab)
         self.signal_emit('tab.layoutchange')
 
+    def tabclose(self, *args, **kwargs):
+        return self.tab_close(*args, **kwargs)
+
     def tab_restore(self):
         # NOTE: The name of the tab is not restored.
         previous_tab = self.thistab
@@ -1290,6 +1289,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                     self.signal_emit('tab.change', old=previous_tab, new=self.thistab)
                     break
 
+    def tabrestore(self, *args, **kwargs):
+        return self.tab_restore(*args, **kwargs)
+
     def tab_move(self, offset, narg=None):
         if narg:
             return self.tab_open(narg)
@@ -1301,6 +1303,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             self.tab_open(newtab)
         return None
 
+    def tabmove(self, *args, **kwargs):
+        return self.tab_move(*args, **kwargs)
+
     def tab_new(self, path=None, narg=None):
         if narg:
             return self.tab_open(narg, path)
@@ -1309,6 +1314,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             i += 1
         return self.tab_open(i, path)
 
+    def tabnew(self, *args, **kwargs):
+        return self.tab_new(*args, **kwargs)
+
     def tab_shift(self, offset=0, to=None):  # pylint: disable=invalid-name
         """Shift the tab left/right
 
@@ -1354,7 +1362,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             self.thistab = oldtab
             self.ui.titlebar.request_redraw()
             self.signal_emit('tab.layoutchange')
-        return None
+
+    def tabshift(self, *args, **kwargs):
+        return self.tab_shift(*args, **kwargs)
 
     def tab_switch(self, path, create_directory=False):
         """Switches to tab of given path, opening a new tab as necessary.
@@ -1400,6 +1410,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         if file_selection:
             self.fm.select_file(file_selection)
 
+    def tabswitch(self, *args, **kwargs):
+        return self.tab_switch(*args, **kwargs)
+
     def get_tab_list(self):
         assert self.tabs, "There must be at least 1 tab at all times"
 
@@ -1426,6 +1439,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         if not contexts:
             contexts = 'browser', 'console', 'pager', 'taskview'
 
+        # Disable lint because TemporaryFiles are removed on close
+        # pylint: disable=consider-using-with
         temporary_file = tempfile.NamedTemporaryFile()
 
         def write(string):  # pylint: disable=redefined-outer-name
@@ -1451,6 +1466,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         self._run_pager(temporary_file.name)
 
     def dump_commands(self):
+        # Disable lint because TemporaryFiles are removed on close
+        # pylint: disable=consider-using-with
         temporary_file = tempfile.NamedTemporaryFile()
 
         def write(string):  # pylint: disable=redefined-outer-name
@@ -1476,6 +1493,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         self._run_pager(temporary_file.name)
 
     def dump_settings(self):
+        # Disable lint because TemporaryFiles are removed on close
+        # pylint: disable=consider-using-with
         temporary_file = tempfile.NamedTemporaryFile()
 
         def write(string):  # pylint: disable=redefined-outer-name
@@ -1609,7 +1628,7 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         # COMPAT: old command.py use fm.delete() without arguments
         if files is None:
             files = (fobj.path for fobj in self.thistab.get_selection())
-        self.notify("Deleting {}!".format(", ".join(files)))
+        self.notify("Deleting {fls}!".format(fls=", ".join(files)))
         files = [os.path.abspath(path) for path in files]
         for path in files:
             # Untag the deleted files.
diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py
index bd63f2fe..66ab95c3 100644
--- a/ranger/core/filter_stack.py
+++ b/ranger/core/filter_stack.py
@@ -6,7 +6,6 @@
 from __future__ import (absolute_import, division, print_function)
 
 import re
-import mimetypes
 # pylint: disable=invalid-name
 try:
     from itertools import izip_longest as zip_longest
@@ -55,23 +54,23 @@ class NameFilter(BaseFilter):
         return self.regex.search(fobj.relative_path)
 
     def __str__(self):
-        return "<Filter: name =~ /{}/>".format(self.pattern)
+        return "<Filter: name =~ /{pat}/>".format(pat=self.pattern)
 
 
 @stack_filter("mime")
-class MimeFilter(BaseFilter):
+class MimeFilter(BaseFilter, FileManagerAware):
     def __init__(self, pattern):
         self.pattern = pattern
         self.regex = re.compile(pattern)
 
     def __call__(self, fobj):
-        mimetype, _ = mimetypes.guess_type(fobj.relative_path)
+        mimetype, _ = self.fm.mimetypes.guess_type(fobj.relative_path)
         if mimetype is None:
             return False
         return self.regex.search(mimetype)
 
     def __str__(self):
-        return "<Filter: mimetype =~ /{}/>".format(self.pattern)
+        return "<Filter: mimetype =~ /{pat}/>".format(pat=self.pattern)
 
 
 @stack_filter("hash")
@@ -97,7 +96,7 @@ class HashFilter(BaseFilter, FileManagerAware):
         return True
 
     def __str__(self):
-        return "<Filter: hash {}>".format(self.filepath)
+        return "<Filter: hash {fp}>".format(fp=self.filepath)
 
 
 def group_by_hash(fsobjects):
@@ -192,7 +191,7 @@ class TypeFilter(BaseFilter):
         return self.type_to_function[self.filetype](fobj)
 
     def __str__(self):
-        return "<Filter: type == '{}'>".format(self.filetype)
+        return "<Filter: type == '{ft}'>".format(ft=self.filetype)
 
 
 @filter_combinator("or")
@@ -216,7 +215,8 @@ class OrFilter(BaseFilter):
         )
 
     def __str__(self):
-        return "<Filter: {}>".format(" or ".join(map(str, self.subfilters)))
+        return "<Filter: {comp}>".format(
+            comp=" or ".join(map(str, self.subfilters)))
 
     def decompose(self):
         return self.subfilters
@@ -236,7 +236,8 @@ class AndFilter(BaseFilter):
         return accept_file(fobj, self.subfilters)
 
     def __str__(self):
-        return "<Filter: {}>".format(" and ".join(map(str, self.subfilters)))
+        return "<Filter: {comp}>".format(
+            comp=" and ".join(map(str, self.subfilters)))
 
     def decompose(self):
         return self.subfilters
@@ -252,7 +253,7 @@ class NotFilter(BaseFilter):
         return not self.subfilter(fobj)
 
     def __str__(self):
-        return "<Filter: not {}>".format(str(self.subfilter))
+        return "<Filter: not {exp}>".format(exp=str(self.subfilter))
 
     def decompose(self):
         return [self.subfilter]
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 7d23c9b6..5b221648 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -5,30 +5,31 @@
 
 from __future__ import (absolute_import, division, print_function)
 
-from time import time
-from collections import deque
 import mimetypes
 import os.path
 import pwd
 import socket
 import stat
 import sys
+from collections import deque
+from io import open
+from time import time
 
 import ranger.api
-from ranger.core.actions import Actions
-from ranger.core.tab import Tab
 from ranger.container import settings
-from ranger.container.tags import Tags, TagsDummy
-from ranger.gui.ui import UI
 from ranger.container.bookmarks import Bookmarks
+from ranger.container.directory import Directory
+from ranger.container.tags import Tags, TagsDummy
+from ranger.core.actions import Actions
+from ranger.core.loader import Loader
+from ranger.core.metadata import MetadataManager
 from ranger.core.runner import Runner
+from ranger.core.tab import Tab
+from ranger.ext import logutils
 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
 from ranger.ext.signals import SignalDispatcher
-from ranger.core.loader import Loader
-from ranger.ext import logutils
+from ranger.gui.ui import UI
 
 
 class FM(Actions,  # pylint: disable=too-many-instance-attributes
@@ -49,13 +50,12 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
         SignalDispatcher.__init__(self)
         self.ui = ui if ui is not None else UI()
         self.start_paths = paths if paths is not None else ['.']
-        self.directories = dict()
+        self.directories = {}
         self.bookmarks = bookmarks
         self.current_tab = 1
         self.tabs = {}
         self.tags = tags
         self.restorable_tabs = deque([], ranger.MAX_RESTORABLE_TABS)
-        self.py3 = sys.version_info >= (3, )
         self.previews = {}
         self.default_linemodes = deque()
         self.loader = Loader()
@@ -74,11 +74,12 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
         self.hostname = socket.gethostname()
         self.home_path = os.path.expanduser('~')
 
-        mimetypes.knownfiles.append(os.path.expanduser('~/.mime.types'))
-        mimetypes.knownfiles.append(self.relpath('data/mime.types'))
-        self.mimetypes = mimetypes.MimeTypes()
+        if not mimetypes.inited:
+            extra_files = [self.relpath('data/mime.types'), os.path.expanduser("~/.mime.types")]
+            mimetypes.init(mimetypes.knownfiles + extra_files)
+        self.mimetypes = mimetypes
 
-    def initialize(self):
+    def initialize(self):  # pylint: disable=too-many-statements
         """If ui/bookmarks are None, they will be initialized here."""
 
         self.tabs = dict((n + 1, Tab(path)) for n, path in enumerate(self.start_paths))
@@ -272,15 +273,15 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
                     shutil.copy(self.relpath(src), self.confpath(dest))
                 except OSError as ex:
                     sys.stderr.write("  ERROR: %s\n" % str(ex))
-        if which == 'rifle' or which == 'all':
+        if which in ('rifle', 'all'):
             copy('config/rifle.conf', 'rifle.conf')
-        if which == 'commands' or which == 'all':
+        if which in ('commands', 'all'):
             copy('config/commands_sample.py', 'commands.py')
-        if which == 'commands_full' or which == 'all':
+        if which in ('commands_full', 'all'):
             copy('config/commands.py', 'commands_full.py')
-        if which == 'rc' or which == 'all':
+        if which in ('rc', 'all'):
             copy('config/rc.conf', 'rc.conf')
-        if which == 'scope' or which == 'all':
+        if which in ('scope', 'all'):
             copy('data/scope.sh', 'scope.sh')
             os.chmod(self.confpath('scope.sh'),
                      os.stat(self.confpath('scope.sh')).st_mode | stat.S_IXUSR)
@@ -329,6 +330,44 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
             self.directories[path] = obj
             return obj
 
+    @staticmethod
+    def group_paths_by_dirname(paths):
+        """
+        Groups the paths into a dictionary with their dirnames as keys and a set of
+        basenames as entries.
+        """
+        groups = {}
+        for path in paths:
+            abspath = os.path.abspath(os.path.expanduser(path))
+            dirname, basename = os.path.split(abspath)
+            groups.setdefault(dirname, set()).add(basename)
+        return groups
+
+    def get_filesystem_objects(self, paths):
+        """
+        Find FileSystemObjects corresponding to the paths if they are already in
+        memory and load those that are not.
+        """
+        result = []
+        # Grouping the files by dirname avoids the potentially quadratic running time of doing
+        # a linear search in the directory for each entry name that is supposed to be deleted.
+        groups = self.group_paths_by_dirname(paths)
+        for dirname, basenames in groups.items():
+            directory = self.fm.get_directory(dirname)
+            directory.load_content_if_outdated()
+            for entry in directory.files_all:
+                if entry.basename in basenames:
+                    result.append(entry)
+                    basenames.remove(entry.basename)
+            if basenames != set():
+                # Abort the operation with an error message if there are entries
+                # that weren't found.
+                names = ', '.join(basenames)
+                self.fm.notify('Error: No such file or directory: {0}'.format(
+                    names), bad=True)
+                return None
+        return result
+
     def garbage_collect(
             self, age,
             tabs=None):  # tabs=None is for COMPATibility pylint: disable=unused-argument
@@ -402,14 +441,14 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
             if ranger.args.choosedir and self.thisdir and self.thisdir.path:
                 # XXX: UnicodeEncodeError: 'utf-8' codec can't encode character
                 # '\udcf6' in position 42: surrogates not allowed
-                with open(ranger.args.choosedir, 'w') as fobj:
+                with open(ranger.args.choosedir, 'w', encoding="utf-8") as fobj:
                     fobj.write(self.thisdir.path)
             self.bookmarks.remember(self.thisdir)
             self.bookmarks.save()
 
             # Save tabs
             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:
+                with open(self.datapath('tabs'), 'a', encoding="utf-8") 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')
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index 7d4b21e8..19611c7b 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -3,14 +3,14 @@
 
 from __future__ import (absolute_import, division, print_function)
 
-from collections import deque
-from subprocess import Popen, PIPE
-from time import time, sleep
+import errno
 import math
 import os.path
 import select
-import sys
-import errno
+from collections import deque
+from io import open
+from subprocess import Popen, PIPE
+from time import time, sleep
 
 try:
     import chardet  # pylint: disable=import-error
@@ -18,10 +18,11 @@ try:
 except ImportError:
     HAVE_CHARDET = False
 
+from ranger import PY3
 from ranger.core.shared import FileManagerAware
+from ranger.ext.human_readable import human_readable
 from ranger.ext.safe_path import get_safe_path
 from ranger.ext.signals import SignalDispatcher
-from ranger.ext.human_readable import human_readable
 
 
 class Loadable(object):
@@ -170,15 +171,20 @@ class CommandLoader(  # pylint: disable=too-many-instance-attributes
         self.kill_on_pause = kill_on_pause
         self.popenArgs = popenArgs  # pylint: disable=invalid-name
 
-    def generate(self):  # pylint: disable=too-many-branches,too-many-statements
-        py3 = sys.version_info[0] >= 3
+    def generate(self):
+        # pylint: disable=too-many-branches,too-many-statements
+        # TODO: Check whether we can afford to wait for processes and use a
+        #       with-statement for Popen.
+        # pylint: disable=consider-using-with
         popenargs = {} if self.popenArgs is None else self.popenArgs
         popenargs['stdout'] = popenargs['stderr'] = PIPE
-        popenargs['stdin'] = PIPE if self.input else open(os.devnull, 'r')
+        popenargs['stdin'] = (
+            PIPE if self.input else open(os.devnull, 'r', encoding="utf-8")
+        )
         self.process = process = Popen(self.args, **popenargs)
         self.signal_emit('before', process=process, loader=self)
         if self.input:
-            if py3:
+            if PY3:
                 import io
                 stdin = io.TextIOWrapper(process.stdin)
             else:
@@ -186,7 +192,7 @@ class CommandLoader(  # pylint: disable=too-many-instance-attributes
             try:
                 stdin.write(self.input)
             except IOError as ex:
-                if ex.errno != errno.EPIPE and ex.errno != errno.EINVAL:
+                if ex.errno not in (errno.EPIPE, errno.EINVAL):
                     raise
             stdin.close()
         if self.silent and not self.read:  # pylint: disable=too-many-nested-blocks
@@ -212,7 +218,7 @@ class CommandLoader(  # pylint: disable=too-many-instance-attributes
                         robjs = robjs[0]
                         if robjs == process.stderr:
                             read = robjs.readline()
-                            if py3:
+                            if PY3:
                                 read = safe_decode(read)
                             if read:
                                 self.fm.notify(read, bad=True)
@@ -227,7 +233,7 @@ class CommandLoader(  # pylint: disable=too-many-instance-attributes
                     sleep(0.03)
             if not self.silent:
                 for line in process.stderr:
-                    if py3:
+                    if PY3:
                         line = safe_decode(line)
                     self.fm.notify(line, bad=True)
             if self.read:
@@ -235,7 +241,7 @@ class CommandLoader(  # pylint: disable=too-many-instance-attributes
                 if read:
                     read_stdout += read
             if read_stdout:
-                if py3:
+                if PY3:
                     read_stdout = safe_decode(read_stdout)
                 self.stdout_buffer += read_stdout
         self.finished = True
diff --git a/ranger/core/main.py b/ranger/core/main.py
index 7322a501..2556b22a 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -5,11 +5,14 @@
 
 from __future__ import (absolute_import, division, print_function)
 
-from logging import getLogger
+import atexit
 import locale
 import os.path
+import shutil
 import sys
 import tempfile
+from io import open
+from logging import getLogger
 
 from ranger import VERSION
 
@@ -72,14 +75,14 @@ def main(
             return 1
         fm = FM()
         try:
-            if sys.version_info[0] >= 3:
-                fobj = open(fm.datapath('tagged'), 'r', errors='replace')
-            else:
-                fobj = open(fm.datapath('tagged'), 'r')
+            with open(
+                fm.datapath('tagged'), 'r', encoding="utf-8", errors='replace'
+            ) as fobj:
+                lines = fobj.readlines()
         except OSError as ex:
             print('Unable to open `tagged` data file: {0}'.format(ex), file=sys.stderr)
             return 1
-        for line in fobj.readlines():
+        for line in lines:
             if len(line) > 2 and line[1] == ':':
                 if line[0] in args.list_tagged_files:
                     sys.stdout.write(line[2:])
@@ -155,11 +158,11 @@ def main(
             tabs_datapath = fm.datapath('tabs')
             if fm.settings.save_tabs_on_exit and os.path.exists(tabs_datapath) and not args.paths:
                 try:
-                    with open(tabs_datapath, 'r') as fobj:
+                    with open(tabs_datapath, 'r', encoding="utf-8") as fobj:
                         tabs_saved = fobj.read().partition('\0\0')
                         fm.start_paths += tabs_saved[0].split('\0')
                     if tabs_saved[-1]:
-                        with open(tabs_datapath, 'w') as fobj:
+                        with open(tabs_datapath, 'w', encoding="utf-8") as fobj:
                             fobj.write(tabs_saved[-1])
                     else:
                         os.remove(tabs_datapath)
@@ -242,7 +245,7 @@ https://github.com/ranger/ranger/issues
 
 def get_paths(args):
     if args.paths:
-        prefix = 'file:///'
+        prefix = 'file://'
         prefix_length = len(prefix)
         paths = [path[prefix_length:] if path.startswith(prefix) else path for path in args.paths]
     else:
@@ -337,6 +340,17 @@ def parse_arguments():
         args.cachedir = mkdtemp(suffix='.ranger-cache')
         args.confdir = None
         args.datadir = None
+
+        @atexit.register
+        def cleanup_cachedir():  # pylint: disable=unused-variable
+            try:
+                shutil.rmtree(args.cachedir)
+            except Exception as ex:  # pylint: disable=broad-except
+                sys.stderr.write(
+                    "Error during the temporary cache directory cleanup:\n"
+                    "{ex}\n".format(ex=ex)
+                )
+
     else:
         args.cachedir = path_init('cachedir')
         args.confdir = path_init('confdir')
@@ -377,15 +391,16 @@ def load_settings(  # pylint: disable=too-many-locals,too-many-branches,too-many
         def import_file(name, path):  # From https://stackoverflow.com/a/67692
             # pragma pylint: disable=no-name-in-module,import-error,no-member, deprecated-method
             if sys.version_info >= (3, 5):
-                import importlib.util as util
+                from importlib import util
                 spec = util.spec_from_file_location(name, path)
                 module = util.module_from_spec(spec)
                 spec.loader.exec_module(module)
             elif (3, 3) <= sys.version_info < (3, 5):
                 from importlib.machinery import SourceFileLoader
+                # pylint: disable=no-value-for-parameter
                 module = SourceFileLoader(name, path).load_module()
             else:
-                import imp
+                import imp  # pylint: disable=deprecated-module
                 module = imp.load_source(name, path)
             # pragma pylint: enable=no-name-in-module,import-error,no-member
             return module
@@ -430,8 +445,11 @@ def load_settings(  # pylint: disable=too-many-locals,too-many-branches,too-many
 
             if not os.path.exists(fm.confpath('plugins', '__init__.py')):
                 LOG.debug("Creating missing '__init__.py' file in plugin folder")
-                fobj = open(fm.confpath('plugins', '__init__.py'), 'w')
-                fobj.close()
+                with open(
+                    fm.confpath('plugins', '__init__.py'), 'w', encoding="utf-8"
+                ):
+                    # Create the file if it doesn't exist.
+                    pass
 
             ranger.fm = fm
             for plugin in sorted(plugins):
diff --git a/ranger/core/metadata.py b/ranger/core/metadata.py
index 75f7ba3c..534c083c 100644
--- a/ranger/core/metadata.py
+++ b/ranger/core/metadata.py
@@ -14,7 +14,9 @@ The database is contained in a local .metadata.json file.
 from __future__ import (absolute_import, division, print_function)
 
 import copy
+from io import open
 from os.path import join, dirname, exists, basename
+
 from ranger.ext.openstruct import DefaultOpenStruct as ostruct
 
 
@@ -26,9 +28,9 @@ class MetadataManager(object):
 
     def __init__(self):
         # metadata_cache maps filenames to dicts containing their metadata
-        self.metadata_cache = dict()
+        self.metadata_cache = {}
         # metafile_cache maps .metadata.json filenames to their entries
-        self.metafile_cache = dict()
+        self.metafile_cache = {}
         self.deep_search = DEEP_SEARCH_DEFAULT
 
     def reset(self):
@@ -84,7 +86,7 @@ class MetadataManager(object):
         self.metadata_cache[filename] = entry
         self.metafile_cache[metafile] = entries
 
-        with open(metafile, "w") as fobj:
+        with open(metafile, "w", encoding="utf-8") as fobj:
             json.dump(entries, fobj, check_circular=True, indent=2)
 
     def _get_entry(self, filename):
@@ -117,7 +119,7 @@ class MetadataManager(object):
             return self.metafile_cache[metafile]
 
         if exists(metafile):
-            with open(metafile, "r") as fobj:
+            with open(metafile, "r", encoding="utf-8") as fobj:
                 try:
                     entries = json.load(fobj)
                 except ValueError:
diff --git a/ranger/core/runner.py b/ranger/core/runner.py
index d465f070..c5ec697b 100644
--- a/ranger/core/runner.py
+++ b/ranger/core/runner.py
@@ -27,7 +27,9 @@ from __future__ import (absolute_import, division, print_function)
 import logging
 import os
 import sys
+from io import open
 from subprocess import Popen, PIPE, STDOUT
+
 from ranger.ext.get_executables import get_executables, get_term
 from ranger.ext.popen_forked import Popen_forked
 
@@ -190,8 +192,10 @@ class Runner(object):  # pylint: disable=too-few-public-methods
             pipe_output = True
             context.wait = False
         if 's' in context.flags:
-            devnull_writable = open(os.devnull, 'w')
-            devnull_readable = open(os.devnull, 'r')
+            # Using a with-statement for these is inconvenient.
+            # pylint: disable=consider-using-with
+            devnull_writable = open(os.devnull, 'w', encoding="utf-8")
+            devnull_readable = open(os.devnull, 'r', encoding="utf-8")
             for key in ('stdout', 'stderr'):
                 popen_kws[key] = devnull_writable
             toggle_ui = False
@@ -231,15 +235,18 @@ class Runner(object):  # pylint: disable=too-few-public-methods
 
         if toggle_ui:
             self._activate_ui(False)
+
+        error = None
+        process = None
+
         try:
-            error = None
-            process = None
             self.fm.signal_emit('runner.execute.before',
                                 popen_kws=popen_kws, context=context)
             try:
                 if 'f' in context.flags and 'r' not in context.flags:
                     # This can fail and return False if os.fork() is not
                     # supported, but we assume it is, since curses is used.
+                    # pylint: disable=consider-using-with
                     Popen_forked(**popen_kws)
                 else:
                     process = Popen(**popen_kws)
diff --git a/ranger/core/tab.py b/ranger/core/tab.py
index 1d5e69d4..3a0fb943 100644
--- a/ranger/core/tab.py
+++ b/ranger/core/tab.py
@@ -4,9 +4,9 @@
 from __future__ import (absolute_import, division, print_function)
 
 import os
-from os.path import abspath, normpath, join, expanduser, isdir
-import sys
+from os.path import abspath, normpath, join, expanduser, isdir, dirname
 
+from ranger import PY3
 from ranger.container import settings
 from ranger.container.history import History
 from ranger.core.shared import FileManagerAware, SettingsAware
@@ -19,7 +19,9 @@ class Tab(FileManagerAware, SettingsAware):  # pylint: disable=too-many-instance
         self._thisfile = None  # Current File
         self.history = History(self.settings.max_history_size, unique=False)
         self.last_search = None
-        self.pointer = 0
+        self._pointer = 0
+        self._pointed_obj = None
+        self.pointed_obj = None
         self.path = abspath(expanduser(path))
         self.pathway = ()
         # NOTE: in the line below, weak=True works only in python3.  In python2,
@@ -27,15 +29,16 @@ class Tab(FileManagerAware, SettingsAware):  # pylint: disable=too-many-instance
         # "==", and this breaks _set_thisfile_from_signal and _on_tab_change.
         self.fm.signal_bind('move', self._set_thisfile_from_signal,
                             priority=settings.SIGNAL_PRIORITY_AFTER_SYNC,
-                            weak=(sys.version_info[0] >= 3))
+                            weak=(PY3))
         self.fm.signal_bind('tab.change', self._on_tab_change,
-                            weak=(sys.version_info[0] >= 3))
+                            weak=(PY3))
 
     def _set_thisfile_from_signal(self, signal):
         if self == signal.tab:
             self._thisfile = signal.new
             if self == self.fm.thistab:
                 self.pointer = self.thisdir.pointer
+                self.pointed_obj = self.thisdir.pointed_obj
 
     def _on_tab_change(self, signal):
         if self == signal.new and self.thisdir:
@@ -53,6 +56,26 @@ class Tab(FileManagerAware, SettingsAware):  # pylint: disable=too-many-instance
 
     thisfile = property(_get_thisfile, _set_thisfile)
 
+    def _get_pointer(self):
+        try:
+            if self.thisdir.files[self._pointer] != self._pointed_obj:
+                try:
+                    self._pointer = self.thisdir.files.index(self._pointed_obj)
+                except ValueError:
+                    self._set_pointer(self._pointer)
+        except (TypeError, IndexError):
+            pass
+        return self._pointer
+
+    def _set_pointer(self, value):
+        self._pointer = value
+        try:
+            self._pointed_obj = self.thisdir.files[self._pointer]
+        except (TypeError, IndexError):
+            pass
+
+    pointer = property(_get_pointer, _set_pointer)
+
     def at_level(self, level):
         """Returns the FileSystemObject at the given level.
 
@@ -123,9 +146,11 @@ class Tab(FileManagerAware, SettingsAware):  # pylint: disable=too-many-instance
 
         # get the absolute path
         path = normpath(join(self.path, expanduser(path)))
+        selectfile = None
 
         if not isdir(path):
-            return False
+            selectfile = path
+            path = dirname(path)
         new_thisdir = self.fm.get_directory(path)
 
         try:
@@ -155,6 +180,8 @@ class Tab(FileManagerAware, SettingsAware):  # pylint: disable=too-many-instance
         self.thisdir.sort_directories_first = self.fm.settings.sort_directories_first
         self.thisdir.sort_reverse = self.fm.settings.sort_reverse
         self.thisdir.sort_if_outdated()
+        if selectfile:
+            self.thisdir.move_to_obj(selectfile)
         if previous and previous.path != path:
             self.thisfile = self.thisdir.pointed_obj
         else:
diff --git a/ranger/data/mime.types b/ranger/data/mime.types
index c0cfdcdb..b8803836 100644
--- a/ranger/data/mime.types
+++ b/ranger/data/mime.types
@@ -29,6 +29,8 @@ audio/x-flac            flac
 image/vnd.djvu          djvu
 image/webp              webp
 
+message/rfc822          eml
+
 text/x-ruby             rb
 
 video/ogg               ogv ogm
diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh
index f403ed83..2e9983ee 100755
--- a/ranger/data/scope.sh
+++ b/ranger/data/scope.sh
@@ -40,12 +40,12 @@ FILE_EXTENSION_LOWER="$(printf "%s" "${FILE_EXTENSION}" | tr '[:upper:]' '[:lowe
 
 ## Settings
 HIGHLIGHT_SIZE_MAX=262143  # 256KiB
-HIGHLIGHT_TABWIDTH=${HIGHLIGHT_TABWIDTH:-8}
-HIGHLIGHT_STYLE=${HIGHLIGHT_STYLE:-pablo}
+HIGHLIGHT_TABWIDTH="${HIGHLIGHT_TABWIDTH:-8}"
+HIGHLIGHT_STYLE="${HIGHLIGHT_STYLE:-pablo}"
 HIGHLIGHT_OPTIONS="--replace-tabs=${HIGHLIGHT_TABWIDTH} --style=${HIGHLIGHT_STYLE} ${HIGHLIGHT_OPTIONS:-}"
-PYGMENTIZE_STYLE=${PYGMENTIZE_STYLE:-autumn}
-OPENSCAD_IMGSIZE=${RNGR_OPENSCAD_IMGSIZE:-1000,1000}
-OPENSCAD_COLORSCHEME=${RNGR_OPENSCAD_COLORSCHEME:-Tomorrow Night}
+PYGMENTIZE_STYLE="${PYGMENTIZE_STYLE:-autumn}"
+OPENSCAD_IMGSIZE="${RNGR_OPENSCAD_IMGSIZE:-1000,1000}"
+OPENSCAD_COLORSCHEME="${RNGR_OPENSCAD_COLORSCHEME:-Tomorrow Night}"
 
 handle_extension() {
     case "${FILE_EXTENSION_LOWER}" in
@@ -80,12 +80,16 @@ handle_extension() {
             exit 1;;
 
         ## OpenDocument
-        odt|ods|odp|sxw)
+        odt|sxw)
             ## Preview as text conversion
             odt2txt "${FILE_PATH}" && exit 5
             ## Preview as markdown conversion
             pandoc -s -t markdown -- "${FILE_PATH}" && exit 5
             exit 1;;
+        ods|odp)
+            ## Preview as text conversion (unsupported by pandoc for markdown)
+            odt2txt "${FILE_PATH}" && exit 5
+            exit 1;;
 
         ## XLSX
         xlsx)
@@ -109,6 +113,14 @@ handle_extension() {
             python -m json.tool -- "${FILE_PATH}" && exit 5
             ;;
 
+        ## Jupyter Notebooks
+        ipynb)
+            jupyter nbconvert --to markdown "${FILE_PATH}" --stdout | env COLORTERM=8bit bat --color=always --style=plain --language=markdown && exit 5
+            jupyter nbconvert --to markdown "${FILE_PATH}" --stdout && exit 5
+            jq --color-output . "${FILE_PATH}" && exit 5
+            python -m json.tool -- "${FILE_PATH}" && exit 5
+            ;;
+
         ## Direct Stream Digital/Transfer (DSDIFF) and wavpack aren't detected
         ## by file(1).
         dff|dsf|wv|wvc)
@@ -128,9 +140,11 @@ handle_image() {
     local mimetype="${1}"
     case "${mimetype}" in
         ## SVG
-        # image/svg+xml|image/svg)
-        #     convert -- "${FILE_PATH}" "${IMAGE_CACHE_PATH}" && exit 6
-        #     exit 1;;
+        image/svg+xml|image/svg)
+            rsvg-convert --keep-aspect-ratio --width "${DEFAULT_SIZE%x*}" "${FILE_PATH}" -o "${IMAGE_CACHE_PATH}.png" \
+                && mv "${IMAGE_CACHE_PATH}.png" "${IMAGE_CACHE_PATH}" \
+                && exit 6
+            exit 1;;
 
         ## DjVu
         # image/vnd.djvu)
@@ -149,16 +163,24 @@ handle_image() {
                 convert -- "${FILE_PATH}" -auto-orient "${IMAGE_CACHE_PATH}" && exit 6
             fi
 
-            ## `w3mimgdisplay` will be called for all images (unless overriden
+            ## `w3mimgdisplay` will be called for all images (unless overridden
             ## as above), but might fail for unsupported types.
             exit 7;;
 
         ## Video
         # video/*)
-        #     # Thumbnail
+        #     # Get embedded thumbnail
+        #     ffmpeg -i "${FILE_PATH}" -map 0:v -map -0:V -c copy "${IMAGE_CACHE_PATH}" && exit 6
+        #     # Get frame 10% into video
         #     ffmpegthumbnailer -i "${FILE_PATH}" -o "${IMAGE_CACHE_PATH}" -s 0 && exit 6
         #     exit 1;;
 
+        ## Audio
+        # audio/*)
+        #     # Get embedded thumbnail
+        #     ffmpeg -i "${FILE_PATH}" -map 0:v -map -0:V -c copy \
+        #       "${IMAGE_CACHE_PATH}" && exit 6;;
+
         ## PDF
         # application/pdf)
         #     pdftoppm -f 1 -l 1 \
@@ -218,7 +240,8 @@ handle_image() {
         #     { [ "$rar" ] && fn=$(unrar lb -p- -- "${FILE_PATH}"); } || \
         #     { [ "$zip" ] && fn=$(zipinfo -1 -- "${FILE_PATH}"); } || return
         #
-        #     fn=$(echo "$fn" | python -c "import sys; import mimetypes as m; \
+        #     fn=$(echo "$fn" | python -c "from __future__ import print_function; \
+        #             import sys; import mimetypes as m; \
         #             [ print(l, end='') for l in sys.stdin if \
         #               (m.guess_type(l[:-1])[0] or '').startswith('image/') ]" |\
         #         sort -V | head -n 1)
@@ -247,19 +270,23 @@ handle_image() {
     #     mv "${TMPPNG}" "${IMAGE_CACHE_PATH}"
     # }
 
-    # case "${FILE_EXTENSION_LOWER}" in
-    #     ## 3D models
-    #     ## OpenSCAD only supports png image output, and ${IMAGE_CACHE_PATH}
-    #     ## is hardcoded as jpeg. So we make a tempfile.png and just
-    #     ## move/rename it to jpg. This works because image libraries are
-    #     ## smart enough to handle it.
-    #     csg|scad)
-    #         openscad_image "${FILE_PATH}" && exit 6
-    #         ;;
-    #     3mf|amf|dxf|off|stl)
-    #         openscad_image <(echo "import(\"${FILE_PATH}\");") && exit 6
-    #         ;;
-    # esac
+    case "${FILE_EXTENSION_LOWER}" in
+       ## 3D models
+       ## OpenSCAD only supports png image output, and ${IMAGE_CACHE_PATH}
+       ## is hardcoded as jpeg. So we make a tempfile.png and just
+       ## move/rename it to jpg. This works because image libraries are
+       ## smart enough to handle it.
+       # csg|scad)
+       #     openscad_image "${FILE_PATH}" && exit 6
+       #     ;;
+       # 3mf|amf|dxf|off|stl)
+       #     openscad_image <(echo "import(\"${FILE_PATH}\");") && exit 6
+       #     ;;
+       drawio)
+           draw.io -x "${FILE_PATH}" -o "${IMAGE_CACHE_PATH}" \
+               --width "${DEFAULT_SIZE%x*}" && exit 6
+           exit 1;;
+    esac
 }
 
 handle_mime() {
@@ -281,6 +308,12 @@ handle_mime() {
             pandoc -s -t markdown -- "${FILE_PATH}" && exit 5
             exit 1;;
 
+	## E-mails
+	message/rfc822)
+	    ## Parsing performed by mu: https://github.com/djcb/mu
+	    mu view -- "${FILE_PATH}" && exit 5
+	    exit 1;;
+
         ## XLS
         *ms-excel)
             ## Preview as csv conversion
@@ -330,6 +363,11 @@ handle_mime() {
             mediainfo "${FILE_PATH}" && exit 5
             exiftool "${FILE_PATH}" && exit 5
             exit 1;;
+            
+        ## ELF files (executables and shared objects)
+        application/x-executable | application/x-pie-executable | application/x-sharedlib)
+            readelf -WCa "${FILE_PATH}" && exit 5
+            exit 1;;
     esac
 }
 
diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py
index c2e33b59..c34370d8 100644
--- a/ranger/ext/accumulator.py
+++ b/ranger/ext/accumulator.py
@@ -3,6 +3,8 @@
 
 from __future__ import (absolute_import, division, print_function)
 
+from abc import abstractmethod
+
 from ranger.ext.direction import Direction
 
 
@@ -75,8 +77,7 @@ class Accumulator(object):
                 i = 0
             if i >= len(lst):
                 i = len(lst) - 1
-            if i < 0:
-                i = 0
+            i = max(0, i)
 
             self.pointer = i
             self.pointed_obj = lst[i]
@@ -91,8 +92,8 @@ class Accumulator(object):
     def sync_index(self, **kw):
         self.move_to_obj(self.pointed_obj, **kw)
 
-    @staticmethod
-    def get_list():
+    @abstractmethod
+    def get_list(self):
         """OVERRIDE THIS"""
         return []
 
diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py
index ffaa4c07..61e10f96 100644
--- a/ranger/ext/img_display.py
+++ b/ranger/ext/img_display.py
@@ -30,7 +30,9 @@ from contextlib import contextmanager
 import codecs
 from tempfile import NamedTemporaryFile
 
-from ranger.core.shared import FileManagerAware
+from ranger import PY3
+from ranger.core.shared import FileManagerAware, SettingsAware
+from ranger.ext.popen23 import Popen23
 
 W3MIMGDISPLAY_ENV = "W3MIMGDISPLAY_PATH"
 W3MIMGDISPLAY_OPTIONS = []
@@ -69,8 +71,14 @@ class ImageDisplayError(Exception):
     pass
 
 
-class ImgDisplayUnsupportedException(Exception):
-    pass
+class ImgDisplayUnsupportedException(Exception, SettingsAware):
+    def __init__(self, message=None):
+        if message is None:
+            message = (
+                '"{0}" does not appear to be a valid setting for'
+                ' preview_images_method.'
+            ).format(self.settings.preview_images_method)
+        super(ImgDisplayUnsupportedException, self).__init__(message)
 
 
 def fallback_image_displayer():
@@ -107,15 +115,12 @@ class ImageDisplayer(object):
 
     def draw(self, path, start_x, start_y, width, height):
         """Draw an image at the given coordinates."""
-        pass
 
     def clear(self, start_x, start_y, width, height):
         """Clear a part of terminal display."""
-        pass
 
     def quit(self):
         """Cleanup and close"""
-        pass
 
 
 @register_image_displayer("w3m")
@@ -137,6 +142,8 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
         """start w3mimgdisplay"""
         self.binary_path = None
         self.binary_path = self._find_w3mimgdisplay_executable()  # may crash
+        # We cannot close the process because that stops the preview.
+        # pylint: disable=consider-using-with
         self.process = Popen([self.binary_path] + W3MIMGDISPLAY_OPTIONS, cwd=self.working_dir,
                              stdin=PIPE, stdout=PIPE, universal_newlines=True)
         self.is_initialized = True
@@ -161,8 +168,12 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
         fretint = fcntl.ioctl(fd_stdout, termios.TIOCGWINSZ, farg)
         rows, cols, xpixels, ypixels = struct.unpack("HHHH", fretint)
         if xpixels == 0 and ypixels == 0:
-            process = Popen([self.binary_path, "-test"], stdout=PIPE, universal_newlines=True)
-            output, _ = process.communicate()
+            with Popen23(
+                [self.binary_path, "-test"],
+                stdout=PIPE,
+                universal_newlines=True,
+            ) as process:
+                output, _ = process.communicate()
             output = output.split()
             xpixels, ypixels = int(output[0]), int(output[1])
             # adjust for misplacement
@@ -174,10 +185,11 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
     def draw(self, path, start_x, start_y, width, height):
         if not self.is_initialized or self.process.poll() is not None:
             self.initialize()
-        try:
-            input_gen = self._generate_w3m_input(path, start_x, start_y, width, height)
-        except ImageDisplayError:
-            raise
+        input_gen = self._generate_w3m_input(path, start_x, start_y, width,
+                                             height)
+        self.process.stdin.write(input_gen)
+        self.process.stdin.flush()
+        self.process.stdout.readline()
 
         # Mitigate the issue with the horizontal black bars when
         # selecting some images on some systems. 2 milliseconds seems
@@ -186,9 +198,7 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
             from time import sleep
             sleep(self.fm.settings.w3m_delay)
 
-        self.process.stdin.write(input_gen)
-        self.process.stdin.flush()
-        self.process.stdout.readline()
+        # HACK workaround for w3mimgdisplay memory leak
         self.quit()
         self.is_initialized = False
 
@@ -233,7 +243,7 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
         # max_height_pixels = (max_height - 1) * fonth - 2
 
         # get image size
-        cmd = "5;{}\n".format(path)
+        cmd = "5;{path}\n".format(path=path)
 
         self.process.stdin.write(cmd)
         self.process.stdin.flush()
@@ -302,7 +312,7 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware):
         content = self._encode_image_content(path)
         display_protocol = "\033"
         close_protocol = "\a"
-        if "screen" in os.environ['TERM']:
+        if os.environ["TERM"].startswith(("screen", "tmux")):
             display_protocol += "Ptmux;\033\033"
             close_protocol += "\033\\"
 
@@ -344,41 +354,37 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware):
     @staticmethod
     def _get_image_dimensions(path):
         """Determine image size using imghdr"""
-        file_handle = open(path, 'rb')
-        file_header = file_handle.read(24)
-        image_type = imghdr.what(path)
-        if len(file_header) != 24:
-            file_handle.close()
-            return 0, 0
-        if image_type == 'png':
-            check = struct.unpack('>i', file_header[4:8])[0]
-            if check != 0x0d0a1a0a:
-                file_handle.close()
+        with open(path, 'rb') as file_handle:
+            file_header = file_handle.read(24)
+            image_type = imghdr.what(path)
+            if len(file_header) != 24:
                 return 0, 0
-            width, height = struct.unpack('>ii', file_header[16:24])
-        elif image_type == 'gif':
-            width, height = struct.unpack('<HH', file_header[6:10])
-        elif image_type == 'jpeg':
-            unreadable = IOError if sys.version_info[0] < 3 else OSError
-            try:
-                file_handle.seek(0)
-                size = 2
-                ftype = 0
-                while not 0xc0 <= ftype <= 0xcf:
-                    file_handle.seek(size, 1)
-                    byte = file_handle.read(1)
-                    while ord(byte) == 0xff:
+            if image_type == 'png':
+                check = struct.unpack('>i', file_header[4:8])[0]
+                if check != 0x0d0a1a0a:
+                    return 0, 0
+                width, height = struct.unpack('>ii', file_header[16:24])
+            elif image_type == 'gif':
+                width, height = struct.unpack('<HH', file_header[6:10])
+            elif image_type == 'jpeg':
+                unreadable = OSError if PY3 else IOError
+                try:
+                    file_handle.seek(0)
+                    size = 2
+                    ftype = 0
+                    while not 0xc0 <= ftype <= 0xcf:
+                        file_handle.seek(size, 1)
                         byte = file_handle.read(1)
-                    ftype = ord(byte)
-                    size = struct.unpack('>H', file_handle.read(2))[0] - 2
-                file_handle.seek(1, 1)
-                height, width = struct.unpack('>HH', file_handle.read(4))
-            except unreadable:
-                height, width = 0, 0
-        else:
-            file_handle.close()
-            return 0, 0
-        file_handle.close()
+                        while ord(byte) == 0xff:
+                            byte = file_handle.read(1)
+                        ftype = ord(byte)
+                        size = struct.unpack('>H', file_handle.read(2))[0] - 2
+                    file_handle.seek(1, 1)
+                    height, width = struct.unpack('>HH', file_handle.read(4))
+                except unreadable:
+                    height, width = 0, 0
+            else:
+                return 0, 0
         return width, height
 
 
@@ -434,7 +440,7 @@ class URXVTImageDisplayer(ImageDisplayer, FileManagerAware):
     def __init__(self):
         self.display_protocol = "\033"
         self.close_protocol = "\a"
-        if "screen" in os.environ['TERM']:
+        if os.environ["TERM"].startswith(("screen", "tmux")):
             self.display_protocol += "Ptmux;\033\033"
             self.close_protocol += "\033\\"
         self.display_protocol += "]20;"
@@ -535,7 +541,7 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
     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:
+    # differences 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
@@ -590,7 +596,7 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
             self.stream = True
         else:
             raise ImgDisplayUnsupportedException(
-                'kitty replied an unexpected response: {}'.format(resp))
+                'kitty replied an unexpected response: {r}'.format(r=resp))
 
         # get the image manipulation backend
         try:
@@ -616,7 +622,8 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
         # a is the display command, with T going for immediate output
         # 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))
+        # sys.stderr.write('{0}-{1}@{2}x{3}\t'.format(
+        #     start_x, start_y, width, height))
 
         # finish initialization if it is the first call
         if self.needs_late_init:
@@ -636,7 +643,7 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
             image = image.resize((int(scale * image.width), int(scale * image.height)),
                                  self.backend.LANCZOS)
 
-        if image.mode != 'RGB' and image.mode != 'RGBA':
+        if image.mode not in ('RGB', 'RGBA'):
             image = image.convert('RGB')
         # start_x += ((box[0] - image.width) // 2) // self.pix_row
         # start_y += ((box[1] - image.height) // 2) // self.pix_col
@@ -673,24 +680,26 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
         if b'OK' in resp:
             return
         else:
-            raise ImageDisplayError('kitty replied "{}"'.format(resp))
+            raise ImageDisplayError('kitty replied "{r}"'.format(r=resp))
 
     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
+        # 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):
             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
+        self.image_id = max(0, self.image_id - 1)
         self.fm.ui.win.redrawwin()
         self.fm.ui.win.refresh()
 
     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')
+        central_blk = ','.join(["{k}={v}".format(k=k, v=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
@@ -706,7 +715,8 @@ class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
             yield self.protocol_start + central_blk + b';' + self.protocol_end
 
     def quit(self):
-        # clear all remaining images, then check if all files went through or are orphaned
+        # 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)
         # for k in self.temp_paths:
@@ -734,6 +744,8 @@ class UeberzugImageDisplayer(ImageDisplayer):
                 and not self.process.stdin.closed):
             return
 
+        # We cannot close the process because that stops the preview.
+        # pylint: disable=consider-using-with
         self.process = Popen(['ueberzug', 'layer', '--silent'], cwd=self.working_dir,
                              stdin=PIPE, universal_newlines=True)
         self.is_initialized = True
diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py
index 8703d458..52e1d506 100644
--- a/ranger/ext/keybinding_parser.py
+++ b/ranger/ext/keybinding_parser.py
@@ -7,7 +7,8 @@ import sys
 import copy
 import curses.ascii
 
-PY3 = sys.version_info[0] >= 3
+from ranger import PY3
+
 digits = set(range(ord('0'), ord('9') + 1))  # pylint: disable=invalid-name
 
 # Arbitrary numbers which are not used with curses.KEY_XYZ
@@ -169,7 +170,7 @@ class KeyMaps(dict):
         self.used_keymap = None
 
     def use_keymap(self, keymap_name):
-        self.keybuffer.keymap = self.get(keymap_name, dict())
+        self.keybuffer.keymap = self.get(keymap_name, {})
         if self.used_keymap != keymap_name:
             self.used_keymap = keymap_name
             self.keybuffer.clear()
@@ -178,7 +179,7 @@ class KeyMaps(dict):
         try:
             pointer = self[context]
         except KeyError:
-            self[context] = pointer = dict()
+            self[context] = pointer = {}
         if PY3:
             keys = keys.encode('utf-8').decode('latin-1')
         return list(parse_keybinding(keys)), pointer
@@ -193,9 +194,9 @@ class KeyMaps(dict):
                 if isinstance(pointer[key], dict):
                     pointer = pointer[key]
                 else:
-                    pointer[key] = pointer = dict()
+                    pointer[key] = pointer = {}
             except KeyError:
-                pointer[key] = pointer = dict()
+                pointer[key] = pointer = {}
         pointer[last_key] = leaf
 
     def copy(self, context, source, target):
diff --git a/ranger/ext/logutils.py b/ranger/ext/logutils.py
index 38fc7cda..12f7886d 100644
--- a/ranger/ext/logutils.py
+++ b/ranger/ext/logutils.py
@@ -43,7 +43,7 @@ def setup_logging(debug=True, logfile=None):
     controlled by the `debug` parameter
 
      - debug=False:
-            a concise log format will be used, debug messsages will be discarded
+            a concise log format will be used, debug messages will be discarded
             and only important message will be passed to the `stderr_handler`
 
      - debug=True:
diff --git a/ranger/ext/macrodict.py b/ranger/ext/macrodict.py
new file mode 100644
index 00000000..924fe5a9
--- /dev/null
+++ b/ranger/ext/macrodict.py
@@ -0,0 +1,82 @@
+from __future__ import absolute_import
+import sys
+
+
+MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
+
+
+def macro_val(thunk, fallback=MACRO_FAIL):
+    try:
+        return thunk()
+    except AttributeError:
+        return fallback
+
+
+try:
+    from collections.abc import MutableMapping  # pylint: disable=no-name-in-module
+except ImportError:
+    from collections import MutableMapping  # pylint: disable=deprecated-class
+
+
+class MacroDict(MutableMapping):
+    """Mapping that returns a fallback value when thunks error
+
+    Macros can be used in scenarios where several attributes aren't
+    initialized yet. To avoid errors in these cases we have to delay the
+    evaluation of these attributes using ``lambda``s. This
+    ``MutableMapping`` evaluates these thunks before returning them
+    replacing them with a fallback value if necessary.
+
+    For convenience it also catches ``TypeError`` so you can store
+    non-callable values without thunking.
+
+    >>> m = MacroDict()
+    >>> o = type("", (object,), {})()
+    >>> o.existing_attribute = "I exist!"
+
+    >>> m['a'] = "plain value"
+    >>> m['b'] = lambda: o.non_existent_attribute
+    >>> m['c'] = lambda: o.existing_attribute
+
+    >>> m['a']
+    'plain value'
+    >>> m['b']
+    '<\\x01\\x01MACRO_HAS_NO_VALUE\\x01\\x01>'
+    >>> m['c']
+    'I exist!'
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(MacroDict, self).__init__()
+        self.__dict__.update(*args, **kwargs)
+
+    def __setitem__(self, key, value):
+        try:
+            real_val = value()
+            if real_val is None:
+                real_val = MACRO_FAIL
+        except AttributeError:
+            real_val = MACRO_FAIL
+        except TypeError:
+            real_val = value
+        self.__dict__[key] = real_val
+
+    def __getitem__(self, key):
+        return self.__dict__[key]
+
+    def __delitem__(self, key):
+        del self.__dict__[key]
+
+    def __iter__(self):
+        return iter(self.__dict__)
+
+    def __len__(self):
+        return len(self.__dict__)
+
+    def __str__(self):
+        return str(self.__dict__)
+
+
+if __name__ == '__main__':
+    import doctest
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/ext/popen23.py b/ranger/ext/popen23.py
new file mode 100644
index 00000000..944ed3dd
--- /dev/null
+++ b/ranger/ext/popen23.py
@@ -0,0 +1,42 @@
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+
+from __future__ import absolute_import
+
+from contextlib import contextmanager
+
+from subprocess import Popen
+
+try:
+    from ranger import PY3
+except ImportError:
+    from sys import version_info
+    PY3 = version_info[0] >= 3
+
+
+# COMPAT: Python 2 (and Python <=3.2) subprocess.Popen objects aren't
+#         context managers. We don't care about early Python 3 but we do want
+#         to wrap Python 2's Popen. There's no harm in always using this Popen
+#         but it is only necessary when used with with-statements. This can be
+#         removed once we ditch Python 2 support.
+@contextmanager
+def Popen23(*args, **kwargs):  # pylint: disable=invalid-name
+    if PY3:
+        yield Popen(*args, **kwargs)
+        return
+    else:
+        popen2 = Popen(*args, **kwargs)
+    try:
+        yield popen2
+    finally:
+        # From Lib/subprocess.py Popen.__exit__:
+        if popen2.stdout:
+            popen2.stdout.close()
+        if popen2.stderr:
+            popen2.stderr.close()
+        try:  # Flushing a BufferedWriter may raise an error
+            if popen2.stdin:
+                popen2.stdin.close()
+        finally:
+            # Wait for the process to terminate, to avoid zombies.
+            popen2.wait()
diff --git a/ranger/ext/popen_forked.py b/ranger/ext/popen_forked.py
index bff1818e..40a5b833 100644
--- a/ranger/ext/popen_forked.py
+++ b/ranger/ext/popen_forked.py
@@ -4,7 +4,8 @@
 from __future__ import (absolute_import, division, print_function)
 
 import os
-import subprocess
+from io import open
+from subprocess import Popen
 
 
 def Popen_forked(*args, **kwargs):  # pylint: disable=invalid-name
@@ -18,9 +19,12 @@ def Popen_forked(*args, **kwargs):  # pylint: disable=invalid-name
         return False
     if pid == 0:
         os.setsid()
-        kwargs['stdin'] = open(os.devnull, 'r')
-        kwargs['stdout'] = kwargs['stderr'] = open(os.devnull, 'w')
-        subprocess.Popen(*args, **kwargs)
+        with open(os.devnull, 'r', encoding="utf-8") as null_r, open(
+            os.devnull, 'w', encoding="utf-8"
+        ) as null_w:
+            kwargs['stdin'] = null_r
+            kwargs['stdout'] = kwargs['stderr'] = null_w
+            Popen(*args, **kwargs)  # pylint: disable=consider-using-with
         os._exit(0)  # pylint: disable=protected-access
     else:
         os.wait()
diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py
index a73a188b..fbfe7c5a 100755
--- a/ranger/ext/rifle.py
+++ b/ranger/ext/rifle.py
@@ -18,10 +18,11 @@ from __future__ import (absolute_import, division, print_function)
 
 import os.path
 import re
-from subprocess import Popen, PIPE
+from subprocess import PIPE
 import sys
 
-__version__ = 'rifle 1.9.2'
+
+__version__ = 'rifle 1.9.3'
 
 # Options and constants that a user might want to change:
 DEFAULT_PAGER = 'less'
@@ -70,6 +71,66 @@ except ImportError:
 
 
 try:
+    from ranger.ext.popen23 import Popen23
+except ImportError:
+    # COMPAT: Python 2 (and Python <=3.2) subprocess.Popen objects aren't
+    #         context managers. We don't care about early Python 3 but we do
+    #         want to wrap Python 2's Popen. There's no harm in always using
+    #         this Popen but it is only necessary when used with
+    #         with-statements. This can be removed once we ditch Python 2
+    #         support.
+    from contextlib import contextmanager
+    # pylint: disable=ungrouped-imports
+    from subprocess import Popen, TimeoutExpired
+
+    try:
+        from ranger import PY3
+    except ImportError:
+        from sys import version_info
+        PY3 = version_info[0] >= 3
+
+    @contextmanager
+    def Popen23(*args, **kwargs):  # pylint: disable=invalid-name
+        if PY3:
+            yield Popen(*args, **kwargs)
+            return
+        else:
+            popen2 = Popen(*args, **kwargs)
+        try:
+            yield popen2
+        finally:
+            # From Lib/subprocess.py Popen.__exit__:
+            if popen2.stdout:
+                popen2.stdout.close()
+            if popen2.stderr:
+                popen2.stderr.close()
+            try:  # Flushing a BufferedWriter may raise an error
+                if popen2.stdin:
+                    popen2.stdin.close()
+            except KeyboardInterrupt:
+                # https://bugs.python.org/issue25942
+                # In the case of a KeyboardInterrupt we assume the SIGINT
+                # was also already sent to our child processes.  We can't
+                # block indefinitely as that is not user friendly.
+                # If we have not already waited a brief amount of time in
+                # an interrupted .wait() or .communicate() call, do so here
+                # for consistency.
+                # pylint: disable=protected-access
+                if popen2._sigint_wait_secs > 0:
+                    try:
+                        # pylint: disable=no-member
+                        popen2._wait(timeout=popen2._sigint_wait_secs)
+                    except TimeoutExpired:
+                        pass
+                popen2._sigint_wait_secs = 0  # Note that this's been done.
+                # pylint: disable=lost-exception
+                return  # resume the KeyboardInterrupt
+            finally:
+                # Wait for the process to terminate, to avoid zombies.
+                popen2.wait()
+
+
+try:
     from ranger.ext.popen_forked import Popen_forked
 except ImportError:
     def Popen_forked(*args, **kwargs):  # pylint: disable=invalid-name
@@ -80,9 +141,13 @@ except ImportError:
             return False
         if pid == 0:
             os.setsid()
-            kwargs['stdin'] = open(os.devnull, 'r')
-            kwargs['stdout'] = kwargs['stderr'] = open(os.devnull, 'w')
-            Popen(*args, **kwargs)
+            # pylint: disable=unspecified-encoding
+            with open(os.devnull, "r") as null_r, open(
+                os.devnull, "w"
+            ) as null_w:
+                kwargs["stdin"] = null_r
+                kwargs["stdout"] = kwargs["stderr"] = null_w
+                Popen(*args, **kwargs)  # pylint: disable=consider-using-with
             os._exit(0)  # pylint: disable=protected-access
         return True
 
@@ -144,7 +209,6 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
         self.config_file = config_file
         self._app_flags = ''
         self._app_label = None
-        self._initialized_mimetypes = False
         self._mimetype = None
         self._skip = None
         self.rules = None
@@ -160,20 +224,20 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
         """Replace the current configuration with the one in config_file"""
         if config_file is None:
             config_file = self.config_file
-        fobj = open(config_file, 'r')
-        self.rules = []
-        for line in fobj:
-            line = line.strip()
-            if line.startswith('#') or line == '':
-                continue
-            if self.delimiter1 not in line:
-                raise ValueError("Line without delimiter")
-            tests, command = line.split(self.delimiter1, 1)
-            tests = tests.split(self.delimiter2)
-            tests = tuple(tuple(f.strip().split(None, 1)) for f in tests)
-            command = command.strip()
-            self.rules.append((command, tests))
-        fobj.close()
+        # pylint: disable=unspecified-encoding
+        with open(config_file, "r") as fobj:
+            self.rules = []
+            for line in fobj:
+                line = line.strip()
+                if line.startswith('#') or line == '':
+                    continue
+                if self.delimiter1 not in line:
+                    raise ValueError("Line without delimiter")
+                tests, command = line.split(self.delimiter1, 1)
+                tests = tests.split(self.delimiter2)
+                tests = tuple(tuple(f.strip().split(None, 1)) for f in tests)
+                command = command.strip()
+                self.rules.append((command, tests))
 
     def _eval_condition(self, condition, files, label):
         # Handle the negation of conditions starting with an exclamation mark,
@@ -252,20 +316,24 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
             return self._mimetype
 
         import mimetypes
-        for path in self._mimetype_known_files:
-            if path not in mimetypes.knownfiles:
-                mimetypes.knownfiles.append(path)
+        if not mimetypes.inited:
+            mimetypes.init(mimetypes.knownfiles + self._mimetype_known_files)
         self._mimetype, _ = mimetypes.guess_type(fname)
 
         if not self._mimetype:
-            process = Popen(["file", "--mime-type", "-Lb", fname], stdout=PIPE, stderr=PIPE)
-            mimetype, _ = process.communicate()
+            with Popen23(
+                ["file", "--mime-type", "-Lb", fname], stdout=PIPE, stderr=PIPE
+            ) as process:
+                mimetype, _ = process.communicate()
             self._mimetype = mimetype.decode(ENCODING).strip()
             if self._mimetype == 'application/octet-stream':
                 try:
-                    process = Popen(["mimetype", "--output-format", "%m", fname],
-                                    stdout=PIPE, stderr=PIPE)
-                    mimetype, _ = process.communicate()
+                    with Popen23(
+                        ["mimetype", "--output-format", "%m", fname],
+                        stdout=PIPE,
+                        stderr=PIPE,
+                    ) as process:
+                        mimetype, _ = process.communicate()
                     self._mimetype = mimetype.decode(ENCODING).strip()
                 except OSError:
                     pass
@@ -439,8 +507,10 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
                 if 'f' in flags or 't' in flags:
                     Popen_forked(cmd, env=self.hook_environment(os.environ))
                 else:
-                    process = Popen(cmd, env=self.hook_environment(os.environ))
-                    process.wait()
+                    with Popen23(
+                        cmd, env=self.hook_environment(os.environ)
+                    ) as process:
+                        process.wait()
             finally:
                 self.hook_after_executing(command, self._mimetype, self._app_flags)
 
@@ -475,7 +545,7 @@ def find_conf_path():
 
 
 def main():  # pylint: disable=too-many-locals
-    """The main function which is run when you start this program direectly."""
+    """The main function which is run when you start this program directly."""
 
     # Evaluate arguments
     from optparse import OptionParser  # pylint: disable=deprecated-module
@@ -520,8 +590,8 @@ def main():  # pylint: disable=too-many-locals
         label = options.p
 
     if options.w is not None and not options.l:
-        process = Popen([options.w] + list(positional))
-        process.wait()
+        with Popen23([options.w] + list(positional)) as process:
+            process.wait()
     else:
         # Start up rifle
         rifle = Rifle(conf_path)
diff --git a/ranger/ext/safe_path.py b/ranger/ext/safe_path.py
index b172b577..b2e360ea 100644
--- a/ranger/ext/safe_path.py
+++ b/ranger/ext/safe_path.py
@@ -1,6 +1,7 @@
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
 
+from __future__ import absolute_import
 import os
 
 SUFFIX = '_'
diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py
index a652fab1..05bff1df 100644
--- a/ranger/ext/shell_escape.py
+++ b/ranger/ext/shell_escape.py
@@ -9,6 +9,8 @@ from __future__ import (absolute_import, division, print_function)
 META_CHARS = (' ', "'", '"', '`', '&', '|', ';', '#',
               '$', '!', '(', ')', '[', ']', '<', '>', '\t')
 UNESCAPABLE = set(map(chr, list(range(9)) + list(range(10, 32)) + list(range(127, 256))))
+# pylint: disable=consider-using-dict-comprehension
+# COMPAT Dictionary comprehensions didn't exist before 2.7
 META_DICT = dict([(mc, '\\' + mc) for mc in META_CHARS])
 
 
diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py
index a97a4d2d..fe367696 100644
--- a/ranger/ext/shutil_generatorized.py
+++ b/ranger/ext/shutil_generatorized.py
@@ -15,12 +15,6 @@ __all__ = ["copyfileobj", "copyfile", "copystat", "copy2", "BLOCK_SIZE",
 BLOCK_SIZE = 16 * 1024
 
 
-try:
-    WindowsError
-except NameError:
-    WindowsError = None  # pylint: disable=invalid-name
-
-
 if sys.version_info < (3, 3):
     def copystat(src, dst):
         """Copy all stat info (mode bits, atime, mtime, flags) from src to dst"""
@@ -233,11 +227,7 @@ def copytree(src, dst,  # pylint: disable=too-many-locals,too-many-branches
     try:
         copystat(src, dst)
     except OSError as why:
-        if WindowsError is not None and isinstance(why, WindowsError):
-            # Copying file access times may fail on Windows
-            pass
-        else:
-            errors.append((src, dst, str(why)))
+        errors.append((src, dst, str(why)))
     if errors:
         raise Error(errors)
 
diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py
index 0973249c..8d3f84c9 100644
--- a/ranger/ext/signals.py
+++ b/ranger/ext/signals.py
@@ -106,14 +106,14 @@ class SignalDispatcher(object):
     """This abstract class handles the binding and emitting of signals."""
 
     def __init__(self):
-        self._signals = dict()
+        self._signals = {}
 
     def signal_clear(self):
         """Remove all signals."""
         for handler_list in self._signals.values():
             for handler in handler_list:
                 handler.function = None
-        self._signals = dict()
+        self._signals = {}
 
     def signal_bind(self, signal_name, function, priority=0.5, weak=False, autosort=True):
         """Bind a function to the signal.
diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py
index dacb3c4a..4bd0b499 100644
--- a/ranger/ext/spawn.py
+++ b/ranger/ext/spawn.py
@@ -3,8 +3,12 @@
 
 from __future__ import (absolute_import, division, print_function)
 
+from io import open
 from os import devnull
-from subprocess import Popen, PIPE, CalledProcessError
+from subprocess import PIPE, CalledProcessError
+
+from ranger.ext.popen23 import Popen23
+
 ENCODING = 'utf-8'
 
 
@@ -28,12 +32,12 @@ def check_output(popenargs, **kwargs):
     kwargs.setdefault('shell', isinstance(popenargs, str))
 
     if 'stderr' in kwargs:
-        process = Popen(popenargs, **kwargs)
-        stdout, _ = process.communicate()
-    else:
-        with open(devnull, mode='w') as fd_devnull:
-            process = Popen(popenargs, stderr=fd_devnull, **kwargs)
+        with Popen23(popenargs, **kwargs) as process:
             stdout, _ = process.communicate()
+    else:
+        with open(devnull, mode='w', encoding="utf-8") as fd_devnull:
+            with Popen23(popenargs, stderr=fd_devnull, **kwargs) as process:
+                stdout, _ = process.communicate()
 
     if process.returncode != 0:
         error = CalledProcessError(process.returncode, popenargs)
diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py
index e5f74ee1..c39ab3eb 100644
--- a/ranger/ext/vcs/git.py
+++ b/ranger/ext/vcs/git.py
@@ -177,7 +177,7 @@ class Git(Vcs):
         if head is None:
             return 'detached'
 
-        match = re.match('refs/heads/([^/]+)', head)
+        match = re.match('refs/heads/(.+)', head)
         return match.group(1) if match else None
 
     def data_info(self, rev=None):
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index e2838f8d..93fb1d0b 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -9,6 +9,7 @@ import os
 import subprocess
 import threading
 import time
+from io import open
 
 from ranger.ext import spawn
 
@@ -21,7 +22,6 @@ except ImportError:
 
 class VcsError(Exception):
     """VCS exception"""
-    pass
 
 
 class Vcs(object):  # pylint: disable=too-many-instance-attributes
@@ -77,8 +77,9 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
         )
 
         self.root, self.repodir, self.repotype, self.links = self._find_root(self.path)
-        self.is_root = True if self.obj.path == self.root else False
-        self.is_root_link = True if self.obj.is_link and self.obj.realpath == self.root else False
+        self.is_root = self.obj.path == self.root
+        self.is_root_link = (
+            self.obj.is_link and self.obj.realpath == self.root)
         self.is_root_pointer = self.is_root or self.is_root_link
         self.in_repodir = False
         self.rootvcs = None
@@ -87,6 +88,7 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
         if self.root:
             if self.is_root:
                 self.rootvcs = self
+                # pylint: disable=invalid-class-object
                 self.__class__ = globals()[self.REPOTYPES[self.repotype]['class'] + 'Root']
 
                 if not os.access(self.repodir, os.R_OK):
@@ -100,6 +102,7 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
                 if self.rootvcs is None or self.rootvcs.root is None:
                     return
                 self.rootvcs.links |= self.links
+                # pylint: disable=invalid-class-object
                 self.__class__ = globals()[self.REPOTYPES[self.repotype]['class']]
                 self.track = self.rootvcs.track
 
@@ -127,7 +130,7 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
                     return output[:-1]
                 return output
             else:
-                with open(os.devnull, mode='w') as fd_devnull:
+                with open(os.devnull, mode='w', encoding="utf-8") as fd_devnull:
                     subprocess.check_call(cmd, cwd=path, stdout=fd_devnull, stderr=fd_devnull)
                 return None
         except (subprocess.CalledProcessError, OSError):
@@ -460,10 +463,10 @@ class VcsThread(threading.Thread):  # pylint: disable=too-many-instance-attribut
             self.paused.set()
             self._advance.wait()
             self._awoken.wait()
-            if self.__stop.isSet():
+            if self.__stop.is_set():
                 self.stopped.set()
                 return
-            if not self._advance.isSet():
+            if not self._advance.is_set():
                 continue
             self._awoken.clear()
             self.paused.clear()
@@ -488,7 +491,7 @@ class VcsThread(threading.Thread):  # pylint: disable=too-many-instance-attribut
         self._advance.set()
         self._awoken.set()
         self.stopped.wait(1)
-        return self.stopped.isSet()
+        return self.stopped.is_set()
 
     def pause(self):
         """Pause thread"""
@@ -513,19 +516,15 @@ from .svn import SVN  # NOQA pylint: disable=wrong-import-position
 
 class BzrRoot(VcsRoot, Bzr):
     """Bzr root"""
-    pass
 
 
 class GitRoot(VcsRoot, Git):
     """Git root"""
-    pass
 
 
 class HgRoot(VcsRoot, Hg):
     """Hg root"""
-    pass
 
 
 class SVNRoot(VcsRoot, SVN):
     """SVN root"""
-    pass
diff --git a/ranger/ext/widestring.py b/ranger/ext/widestring.py
index 390da639..a209e5fe 100644
--- a/ranger/ext/widestring.py
+++ b/ranger/ext/widestring.py
@@ -7,7 +7,8 @@ from __future__ import (absolute_import, division, print_function)
 import sys
 from unicodedata import east_asian_width
 
-PY3 = sys.version_info[0] >= 3
+from ranger import PY3
+
 ASCIIONLY = set(chr(c) for c in range(1, 128))
 NARROW = 1
 WIDE = 2
diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py
index ce735317..3d33688d 100644
--- a/ranger/gui/ansi.py
+++ b/ranger/gui/ansi.py
@@ -42,23 +42,23 @@ def text_with_fg_bg_attr(ansi_text):  # pylint: disable=too-many-branches,too-ma
             for x256fg, x256bg, arg in codesplit_re.findall(attr_args + ';'):
                 # first handle xterm256 codes
                 try:
-                    if x256fg:                    # xterm256 foreground
+                    if x256fg:       # xterm256 foreground
                         fg = int(x256fg)
                         continue
-                    elif x256bg:                  # xterm256 background
+                    elif x256bg:     # xterm256 background
                         bg = int(x256bg)
                         continue
-                    elif arg:                     # usual ansi code
+                    elif arg:        # usual ansi code
                         n = int(arg)
-                    else:                         # empty code means reset
+                    else:            # empty code means reset
                         n = 0
                 except ValueError:
                     continue
 
-                if n == 0:                        # reset colors and attributes
+                if n == 0:           # reset colors and attributes
                     fg, bg, attr = -1, -1, 0
 
-                elif n == 1:                      # enable attribute
+                elif n == 1:         # enable attribute
                     attr |= color.bold
                 elif n == 4:
                     attr |= color.underline
@@ -69,7 +69,7 @@ def text_with_fg_bg_attr(ansi_text):  # pylint: disable=too-many-branches,too-ma
                 elif n == 8:
                     attr |= color.invisible
 
-                elif n == 22:                     # disable attribute
+                elif n == 22:        # disable attribute
                     attr &= not color.bold
                 elif n == 24:
                     attr &= not color.underline
@@ -80,21 +80,21 @@ def text_with_fg_bg_attr(ansi_text):  # pylint: disable=too-many-branches,too-ma
                 elif n == 28:
                     attr &= not color.invisible
 
-                elif n >= 30 and n <= 37:         # 8 ansi foreground and background colors
+                elif 30 <= n <= 37:  # 8 ansi foreground and background colors
                     fg = n - 30
                 elif n == 39:
                     fg = -1
-                elif n >= 40 and n <= 47:
+                elif 40 <= n <= 47:
                     bg = n - 40
                 elif n == 49:
                     bg = -1
 
                 # 8 aixterm high intensity colors (light but not bold)
-                elif n >= 90 and n <= 97:
+                elif 90 <= n <= 97:
                     fg = n - 90 + 8
                 elif n == 99:
                     fg = -1
-                elif n >= 100 and n <= 107:
+                elif 100 <= n <= 107:
                     bg = n - 100 + 8
                 elif n == 109:
                     bg = -1
@@ -159,7 +159,7 @@ def char_slice(ansi_text, start, length):
         pos += len(chunk)
         if pos <= start:
             pass  # seek
-        elif old_pos < start and pos >= start:
+        elif old_pos < start <= pos:
             chunks.append(last_color)
             chunks.append(str(chunk[start - old_pos:start - old_pos + length]))
         elif pos > length + start:
diff --git a/ranger/gui/bar.py b/ranger/gui/bar.py
index 3f4b891c..d65ef4bc 100644
--- a/ranger/gui/bar.py
+++ b/ranger/gui/bar.py
@@ -3,14 +3,10 @@
 
 from __future__ import (absolute_import, division, print_function)
 
-import sys
-
+from ranger import PY3
 from ranger.ext.widestring import WideString, utf_char_width
 
 
-PY3 = sys.version_info[0] >= 3
-
-
 class Bar(object):
     left = None
     right = None
@@ -38,7 +34,7 @@ class Bar(object):
         rightsize = self.right.sumsize()
         sumsize = leftsize + rightsize
 
-        # remove elemets from the left until it fits
+        # remove elements from the left until it fits
         if sumsize > wid:
             while self.left:
                 leftsize -= len(self.left.pop(-1))
@@ -46,7 +42,7 @@ class Bar(object):
                     break
             sumsize = leftsize + rightsize
 
-            # remove elemets from the right until it fits
+            # remove elements from the right until it fits
             if sumsize > wid:
                 while self.right:
                     rightsize -= len(self.right.pop(0))
diff --git a/ranger/gui/color.py b/ranger/gui/color.py
index 8f6439c7..81d8b44b 100644
--- a/ranger/gui/color.py
+++ b/ranger/gui/color.py
@@ -49,27 +49,27 @@ def get_color(fg, bg):
     return COLOR_PAIRS[key]
 
 
-# pylint: disable=invalid-name,bad-whitespace
-black      = curses.COLOR_BLACK
-blue       = curses.COLOR_BLUE
-cyan       = curses.COLOR_CYAN
-green      = curses.COLOR_GREEN
-magenta    = curses.COLOR_MAGENTA
-red        = curses.COLOR_RED
-white      = curses.COLOR_WHITE
-yellow     = curses.COLOR_YELLOW
-default    = -1
-
-normal     = curses.A_NORMAL
-bold       = curses.A_BOLD
-blink      = curses.A_BLINK
-reverse    = curses.A_REVERSE
-underline  = curses.A_UNDERLINE
-invisible  = curses.A_INVIS
+# pylint: disable=invalid-name
+black = curses.COLOR_BLACK
+blue = curses.COLOR_BLUE
+cyan = curses.COLOR_CYAN
+green = curses.COLOR_GREEN
+magenta = curses.COLOR_MAGENTA
+red = curses.COLOR_RED
+white = curses.COLOR_WHITE
+yellow = curses.COLOR_YELLOW
+default = -1
+
+normal = curses.A_NORMAL
+bold = curses.A_BOLD
+blink = curses.A_BLINK
+reverse = curses.A_REVERSE
+underline = curses.A_UNDERLINE
+invisible = curses.A_INVIS
 dim = curses.A_DIM
 
 default_colors = (default, default, normal)
-# pylint: enable=invalid-name,bad-whitespace
+# pylint: enable=invalid-name
 
 curses.setupterm()
 # Adding BRIGHT to a color achieves what `bold` was used for.
diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py
index 01862957..22ff053f 100644
--- a/ranger/gui/colorscheme.py
+++ b/ranger/gui/colorscheme.py
@@ -27,7 +27,9 @@ set colorscheme yourschemename
 from __future__ import (absolute_import, division, print_function)
 
 import os.path
+from abc import abstractmethod
 from curses import color_pair
+from io import open
 
 import ranger
 from ranger.gui.color import get_color
@@ -71,8 +73,8 @@ class ColorScheme(object):
         fg, bg, attr = self.get(*flatten(keys))
         return attr | color_pair(get_color(fg, bg))
 
-    @staticmethod
-    def use(_):
+    @abstractmethod
+    def use(self, context):
         """Use the colorscheme to determine the (fg, bg, attr) tuple.
 
         Override this method in your own colorscheme.
@@ -108,7 +110,9 @@ def _colorscheme_name_to_class(signal):  # pylint: disable=too-many-branches
         if os.path.exists(signal.fm.confpath('colorschemes')):
             initpy = signal.fm.confpath('colorschemes', '__init__.py')
             if not os.path.exists(initpy):
-                open(initpy, 'a').close()
+                with open(initpy, "a", encoding="utf-8"):
+                    # Just create the file
+                    pass
 
     if usecustom and \
             exists(signal.fm.confpath('colorschemes', scheme_name)):
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index 96849686..202d6a53 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -16,7 +16,8 @@ CONTEXT_KEYS = [
     'good', 'bad',
     'space', 'permissions', 'owner', 'group', 'mtime', 'nlink',
     'scroll', 'all', 'bot', 'top', 'percentage', 'filter',
-    'flat', 'marked', 'tagged', 'tag_marker', 'cut', 'copied', 'frozen',
+    'flat', 'marked', 'tagged', 'tag_marker', 'line_number',
+    'cut', 'copied', 'frozen',
     'help_markup',  # COMPAT
     'seperator', 'key', 'special', 'border',  # COMPAT
     'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded',
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index 1c3fb3e4..67128f30 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -8,6 +8,12 @@ import curses
 from ranger.core.shared import FileManagerAware
 from ranger.gui.curses_shortcuts import CursesShortcuts
 
+try:
+    from bidi.algorithm import get_display  # pylint: disable=import-error
+    HAVE_BIDI = True
+except ImportError:
+    HAVE_BIDI = False
+
 
 class Displayable(  # pylint: disable=too-many-instance-attributes
         FileManagerAware, CursesShortcuts):
@@ -95,7 +101,7 @@ class Displayable(  # pylint: disable=too-many-instance-attributes
         return self.contains_point(y, x)
 
     def draw(self):
-        """Draw the oject.
+        """Draw the object.
 
         Called on every main iteration if visible.  Containers should call
         draw() on their contained objects here.  Override this!
@@ -110,22 +116,20 @@ class Displayable(  # pylint: disable=too-many-instance-attributes
 
         x and y should be absolute coordinates.
         """
-        return (x >= self.x and x < self.x + self.wid) and \
-            (y >= self.y and y < self.y + self.hei)
+        return (self.x <= x < self.x + self.wid) and \
+            (self.y <= y < self.y + self.hei)
 
     def click(self, event):
         """Called when a mouse key is pressed and self.focused is True.
 
         Override this!
         """
-        pass
 
     def press(self, key):
         """Called when a key is pressed and self.focused is True.
 
         Override this!
         """
-        pass
 
     def poke(self):
         """Called before drawing, even if invisible"""
@@ -141,7 +145,6 @@ class Displayable(  # pylint: disable=too-many-instance-attributes
 
         Override this!
         """
-        pass
 
     def resize(self, y, x, hei=None, wid=None):
         """Resize the widget"""
@@ -212,6 +215,11 @@ class Displayable(  # pylint: disable=too-many-instance-attributes
     def __str__(self):
         return self.__class__.__name__
 
+    def bidi_transpose(self, text):
+        if self.settings.bidi_support and HAVE_BIDI:
+            return get_display(text)
+        return text
+
 
 class DisplayableContainer(Displayable):
     """DisplayableContainers are Displayables which contain other Displayables.
@@ -243,7 +251,7 @@ class DisplayableContainer(Displayable):
 
         Displayable.__init__(self, win)
 
-    # ------------------------------------ extended or overidden methods
+    # ------------------------------------ extended or overridden methods
 
     def poke(self):
         """Recursively called on objects in container"""
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index d2dbb759..db12d6dc 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -9,6 +9,7 @@ import threading
 import curses
 from subprocess import CalledProcessError
 
+from ranger.ext.get_executables import get_executables
 from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY
 from ranger.ext.lazy_property import lazy_property
 from ranger.ext.signals import Signal
@@ -43,12 +44,22 @@ def _setup_mouse(signal):
         # starts curses again, (e.g. running a ## file by clicking on its
         # preview) and the next key is another mouse click, the bstate of this
         # mouse event will be invalid.  (atm, invalid bstates are recognized
-        # as scroll-down, so this avoids an errorneous scroll-down action)
+        # as scroll-down, so this avoids an erroneous scroll-down action)
         curses.ungetmouse(0, 0, 0, 0, 0)
     else:
         curses.mousemask(0)
 
 
+def _in_tmux():
+    return (os.environ.get("TMUX", "")
+            and 'tmux' in get_executables())
+
+
+def _in_screen():
+    return ('screen' in os.environ.get("TERM", "")
+            and 'screen' in get_executables())
+
+
 class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-methods
         DisplayableContainer):
     ALLOWED_VIEWMODES = 'miller', 'multipane'
@@ -73,8 +84,7 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
         self.multiplexer = None
         self._draw_title = None
         self._tmux_automatic_rename = None
-        self._tmux_title = None
-        self._screen_title = None
+        self._multiplexer_title = None
         self.browser = None
 
         if fm is not None:
@@ -229,7 +239,7 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
         key = self.win.getch()
         if key == curses.KEY_ENTER:
             key = ord('\n')
-        if key == 27 or (key >= 128 and key < 256):
+        if key == 27 or (128 <= key < 256):
             # Handle special keys like ALT+X or unicode here:
             keys = [key]
             previous_load_mode = self.load_mode
@@ -469,58 +479,63 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
     # Handles window renaming behaviour of the terminal multiplexers
     # GNU Screen and Tmux
     def handle_multiplexer(self):
-        if self.settings.update_tmux_title:
-            if 'TMUX' in os.environ:
-                # Stores the automatic-rename setting
-                # prints out a warning if the allow-rename in tmux is not set
-                tmux_allow_rename = check_output(
-                    ['tmux', 'show-window-options', '-v',
-                     'allow-rename']).strip()
-                if tmux_allow_rename == 'off':
-                    self.fm.notify('Warning: allow-rename not set in Tmux!',
-                                   bad=True)
-                elif self._tmux_title is None:
-                    self._tmux_title = check_output(
-                        ['tmux', 'display-message', '-p', '#W']).strip()
-                else:
+        if (self.settings.update_tmux_title and not self._multiplexer_title):
+            try:
+                if _in_tmux():
+                    # Stores the automatic-rename setting
+                    # prints out a warning if allow-rename isn't set in tmux
                     try:
+                        tmux_allow_rename = check_output(
+                            ['tmux', 'show-window-options', '-v',
+                             'allow-rename']).strip()
+                    except CalledProcessError:
+                        tmux_allow_rename = 'off'
+                    if tmux_allow_rename == 'off':
+                        self.fm.notify('Warning: allow-rename not set in Tmux!',
+                                       bad=True)
+                    else:
+                        self._multiplexer_title = check_output(
+                            ['tmux', 'display-message', '-p', '#W']).strip()
                         self._tmux_automatic_rename = check_output(
                             ['tmux', 'show-window-options', '-v',
                              'automatic-rename']).strip()
                         if self._tmux_automatic_rename == 'on':
                             check_output(['tmux', 'set-window-option',
                                           'automatic-rename', 'off'])
-                    except CalledProcessError:
-                        pass
-            elif 'screen' in os.environ['TERM'] and self._screen_title is None:
-                # Stores the screen window name before renaming it
-                # gives out a warning if $TERM is not "screen"
-                try:
-                    self._screen_title = check_output(
+                elif _in_screen():
+                    # Stores the screen window name before renaming it
+                    # gives out a warning if $TERM is not "screen"
+                    self._multiplexer_title = check_output(
                         ['screen', '-Q', 'title']).strip()
-                except CalledProcessError:
-                    self._screen_title = None
+            except CalledProcessError:
+                self.fm.notify("Couldn't access previous multiplexer window"
+                               " name, won't be able to restore.",
+                               bad=False)
+            if not self._multiplexer_title:
+                self._multiplexer_title = os.path.basename(
+                    os.environ.get("SHELL", "shell"))
 
             sys.stdout.write("\033kranger\033\\")
             sys.stdout.flush()
 
     # Restore window name
     def restore_multiplexer_name(self):
-        try:
-            if 'TMUX' in os.environ:
-                if self._tmux_automatic_rename:
-                    check_output(['tmux', 'set-window-option',
-                                  'automatic-rename',
-                                  self._tmux_automatic_rename])
-                else:
-                    check_output(['tmux', 'set-window-option', '-u',
-                                  'automatic-rename'])
-                if self._tmux_title:
-                    check_output(['tmux', 'rename-window', self._tmux_title])
-            elif 'screen' in os.environ['TERM'] and self._screen_title:
-                check_output(['screen', '-X', 'title', self._screen_title])
-        except CalledProcessError:
-            self.fm.notify("Could not restore window-name!", bad=True)
+        if self._multiplexer_title:
+            try:
+                if _in_tmux():
+                    if self._tmux_automatic_rename:
+                        check_output(['tmux', 'set-window-option',
+                                      'automatic-rename',
+                                      self._tmux_automatic_rename])
+                    else:
+                        check_output(['tmux', 'set-window-option', '-u',
+                                      'automatic-rename'])
+            except CalledProcessError:
+                self.fm.notify("Could not restore multiplexer window name!",
+                               bad=True)
+
+            sys.stdout.write("\033k{0}\033\\".format(self._multiplexer_title))
+            sys.stdout.flush()
 
     def hint(self, text=None):
         self.status.hint = text
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 1412ef6a..54149e0c 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -10,12 +10,6 @@ import stat
 from time import time
 from os.path import splitext
 
-try:
-    from bidi.algorithm import get_display  # pylint: disable=import-error
-    HAVE_BIDI = True
-except ImportError:
-    HAVE_BIDI = False
-
 from ranger.ext.widestring import WideString
 from ranger.core import linemode
 
@@ -99,7 +93,10 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
                             self.fm.thisdir.move_to_obj(clicked_file)
                             self.fm.execute_file(clicked_file)
         elif self.target.is_file:
-            self.scrollbit(direction)
+            if event.pressed(3):
+                self.fm.execute_file(self.target)
+            else:
+                self.scrollbit(direction)
         else:
             if self.level > 0 and not direction:
                 self.fm.move(right=0)
@@ -215,7 +212,7 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
 
     def _format_line_number(self, linum_format, i, selected_i):
         line_number = i
-        if self.settings.line_numbers == 'relative':
+        if self.settings.line_numbers.lower() == 'relative':
             line_number = abs(selected_i - i)
             if not self.settings.relative_current_zero and line_number == 0:
                 if self.settings.one_indexed:
@@ -276,14 +273,29 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
 
         copied = [f.path for f in self.fm.copy_buffer]
 
+        selected_i = self._get_index_of_selected_file()
+
         # Set the size of the linum text field to the number of digits in the
         # visible files in directory.
-        linum_text_len = len(str(self.scroll_begin + self.hei))
+        def nr_of_digits(number):
+            return len(str(number))
+
+        scroll_end = self.scroll_begin + min(self.hei, len(self.target)) - 1
+        distance_to_top = selected_i - self.scroll_begin
+        distance_to_bottom = scroll_end - selected_i
+        one_indexed_offset = 1 if self.settings.one_indexed else 0
+
+        if self.settings.line_numbers.lower() == "relative":
+            linum_text_len = nr_of_digits(max(distance_to_top,
+                                              distance_to_bottom))
+            if not self.settings.relative_current_zero:
+                linum_text_len = max(nr_of_digits(selected_i
+                                                  + one_indexed_offset),
+                                     linum_text_len)
+        else:
+            linum_text_len = nr_of_digits(scroll_end + one_indexed_offset)
         linum_format = "{0:>" + str(linum_text_len) + "}"
-        # add separator between line number and tag
-        linum_format += " "
 
-        selected_i = self._get_index_of_selected_file()
         for line in range(self.hei):
             i = line + self.scroll_begin
 
@@ -312,12 +324,15 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
                    drawn.path in copied, tagged_marker, drawn.infostring,
                    drawn.vcsstatus, drawn.vcsremotestatus, self.target.has_vcschild,
                    self.fm.do_cut, current_linemode.name, metakey, active_pane,
-                   self.settings.line_numbers)
+                   self.settings.line_numbers.lower(), linum_text_len)
 
             # Check if current line has not already computed and cached
             if key in drawn.display_data:
                 # Recompute line numbers because they can't be reliably cached.
-                if self.main_column and self.settings.line_numbers != 'false':
+                if (
+                    self.main_column
+                    and self.settings.line_numbers.lower() != 'false'
+                ):
                     line_number_text = self._format_line_number(linum_format,
                                                                 i,
                                                                 selected_i)
@@ -342,17 +357,19 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
             space = self.wid
 
             # line number field
-            if self.settings.line_numbers != 'false':
+            if self.settings.line_numbers.lower() != 'false':
                 if self.main_column and space - linum_text_len > 2:
                     line_number_text = self._format_line_number(linum_format,
                                                                 i,
                                                                 selected_i)
-                    predisplay_left.append([line_number_text, []])
+                    predisplay_left.append([line_number_text, ['line_number']])
                     space -= linum_text_len
 
                     # Delete one additional character for space separator
                     # between the line number and the tag
                     space -= 1
+                    # add separator between line number and tag
+                    predisplay_left.append([' ', []])
 
             # selection mark
             tagmark = self._draw_tagged_display(tagged, tagged_marker)
@@ -374,15 +391,16 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
             try:
                 infostringdata = current_linemode.infostring(drawn, metadata)
                 if infostringdata:
-                    infostring.append([" " + infostringdata + " ",
+                    infostring.append([" " + infostringdata,
                                        ["infostring"]])
             except NotImplementedError:
                 infostring = self._draw_infostring_display(drawn, space)
             if infostring:
                 infostringlen = self._total_len(infostring)
                 if space - infostringlen > 2:
-                    predisplay_right = infostring + predisplay_right
-                    space -= infostringlen
+                    sep = [[" ", []]] if predisplay_right else []
+                    predisplay_right = infostring + sep + predisplay_right
+                    space -= infostringlen + len(sep)
 
             textstring = self._draw_text_display(text, space)
             textstringlen = self._total_len(textstring)
@@ -413,7 +431,7 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
             self.color_reset()
 
     def _get_index_of_selected_file(self):
-        if self.fm.ui.viewmode == 'multipane' and self.tab:
+        if self.fm.ui.viewmode == 'multipane' and self.tab != self.fm.thistab:
             return self.tab.pointer
         return self.target.pointer
 
@@ -421,13 +439,8 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
     def _total_len(predisplay):
         return sum([len(WideString(s)) for s, _ in predisplay])
 
-    def _bidi_transpose(self, text):
-        if self.settings.bidi_support and HAVE_BIDI:
-            return get_display(text)
-        return text
-
     def _draw_text_display(self, text, space):
-        bidi_text = self._bidi_transpose(text)
+        bidi_text = self.bidi_transpose(text)
         wtext = WideString(bidi_text)
         wext = WideString(splitext(bidi_text)[1])
         wellip = WideString(self.ellipsis[self.settings.unicode_ellipsis])
@@ -453,7 +466,7 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
         infostring_display = []
         if self.display_infostring and drawn.infostring \
                 and self.settings.display_size_in_main_column:
-            infostring = str(drawn.infostring) + " "
+            infostring = str(drawn.infostring)
             if len(infostring) <= space:
                 infostring_display.append([infostring, ['infostring']])
         return infostring_display
@@ -539,7 +552,7 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
             self.target.scroll_begin = dirsize - winsize
             return self._get_scroll_begin()
 
-        if projected < upper_limit and projected > lower_limit:
+        if lower_limit < projected < upper_limit:
             return original
 
         if projected > upper_limit:
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index d796058c..f6d3461f 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -9,7 +9,9 @@ import curses
 import os
 import re
 from collections import deque
+from io import open
 
+from ranger import PY3
 from ranger.gui.widgets import Widget
 from ranger.ext.direction import Direction
 from ranger.ext.widestring import uwid, WideString
@@ -43,17 +45,20 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
             self.historypath = self.fm.datapath('history')
             if os.path.exists(self.historypath):
                 try:
-                    fobj = open(self.historypath, 'r')
-                except OSError as ex:
-                    self.fm.notify('Failed to read history file', bad=True, exception=ex)
-                else:
-                    try:
-                        for line in fobj:
-                            self.history.add(line[:-1])
-                    except UnicodeDecodeError as ex:
-                        self.fm.notify('Failed to parse corrupt history file',
-                                       bad=True, exception=ex)
-                    fobj.close()
+                    with open(self.historypath, "r", encoding="utf-8") as fobj:
+                        try:
+                            for line in fobj:
+                                self.history.add(line[:-1])
+                        except UnicodeDecodeError as ex:
+                            self.fm.notify(
+                                "Failed to parse corrupt history file",
+                                bad=True,
+                                exception=ex,
+                            )
+                except (OSError, IOError) as ex:
+                    self.fm.notify(
+                        "Failed to read history file", bad=True, exception=ex
+                    )
         self.history_backup = History(self.history)
 
         # NOTE: the console is considered in the "question mode" when the
@@ -75,16 +80,16 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
             return
         if self.historypath:
             try:
-                fobj = open(self.historypath, 'w')
-            except OSError as ex:
-                self.fm.notify('Failed to write history file', bad=True, exception=ex)
-            else:
-                for entry in self.history_backup:
-                    try:
-                        fobj.write(entry + '\n')
-                    except UnicodeEncodeError:
-                        pass
-                fobj.close()
+                with open(self.historypath, 'w', encoding="utf-8") as fobj:
+                    for entry in self.history_backup:
+                        try:
+                            fobj.write(entry + '\n')
+                        except UnicodeEncodeError:
+                            pass
+            except (OSError, IOError) as ex:
+                self.fm.notify(
+                    "Failed to write history file", bad=True, exception=ex
+                )
         Widget.destroy(self)
 
     def _calculate_offset(self):
@@ -216,7 +221,8 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
             self.unicode_buffer, self.line, self.pos = result
             self.on_line_change()
 
-    def _add_character(self, key, unicode_buffer, line, pos):
+    @staticmethod
+    def _add_character(key, unicode_buffer, line, pos):
         # Takes the pressed key, a string "unicode_buffer" containing a
         # potentially incomplete unicode character, the current line and the
         # position of the cursor inside the line.
@@ -228,7 +234,7 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
             except ValueError:
                 return unicode_buffer, line, pos
 
-        if self.fm.py3:
+        if PY3:
             if len(unicode_buffer) >= 4:
                 unicode_buffer = ""
             if ord(key) in range(0, 256):
@@ -280,7 +286,7 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
         direction = Direction(keywords)
         if direction.horizontal():
             # Ensure that the pointer is moved utf-char-wise
-            if self.fm.py3:
+            if PY3:
                 if self.question_queue:
                     umax = len(self.question_queue[0][0]) + 1 - self.wid
                 else:
@@ -317,13 +323,13 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
         """
         Returns a new position by moving word-wise in the line
 
-        >>> import sys
-        >>> if sys.version_info < (3, ):
+        >>> from ranger import PY3
+        >>> if PY3:
+        ...     line = "\\u30AA\\u30CF\\u30E8\\u30A6 world,  this is dog"
+        ... else:
         ...     # Didn't get the unicode test to work on python2, even though
         ...     # it works fine in ranger, even with unicode input...
         ...     line = "ohai world,  this is dog"
-        ... else:
-        ...     line = "\\u30AA\\u30CF\\u30E8\\u30A6 world,  this is dog"
         >>> Console.move_by_word(line, 0, -1)
         0
         >>> Console.move_by_word(line, 0, 1)
@@ -425,7 +431,7 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
                 self.close(trigger_cancel_function=False)
             return
         # Delete utf-char-wise
-        if self.fm.py3:
+        if PY3:
             left_part = self.line[:self.pos + mod]
             self.pos = len(left_part)
             self.line = left_part + self.line[self.pos + 1:]
@@ -437,6 +443,78 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
             self.line = left_part + ''.join(uchar[upos + 1:]).encode('utf-8', 'ignore')
         self.on_line_change()
 
+    def transpose_subr(self, line, x, y):
+        # Transpose substrings x & y of line
+        # x & y are tuples of length two containing positions of endpoints
+        if not 0 <= x[0] < x[1] <= y[0] < y[1] <= len(line):
+            self.fm.notify("Tried to transpose invalid regions.", bad=True)
+            return line
+
+        line_begin = line[:x[0]]
+        word_x = line[x[0]:x[1]]
+        line_middle = line[x[1]:y[0]]
+        word_y = line[y[0]:y[1]]
+        line_end = line[y[1]:]
+
+        line = line_begin + word_y + line_middle + word_x + line_end
+        return line
+
+    def transpose_chars(self):
+        if self.pos == 0:
+            return
+        elif self.pos == len(self.line):
+            x = max(0, self.pos - 2), max(0, self.pos - 1)
+            y = max(0, self.pos - 1), self.pos
+        else:
+            x = max(0, self.pos - 1), self.pos
+            y = self.pos, min(len(self.line), self.pos + 1)
+        self.line = self.transpose_subr(self.line, x, y)
+        self.pos = y[1]
+        self.on_line_change()
+
+    def transpose_words(self):
+        # Interchange adjacent words at the console with Alt-t
+        # like in Emacs and many terminal emulators
+        if self.line:
+            # If before the first word, interchange next two words
+            if not re.search(r'[\w\d]', self.line[:self.pos], re.UNICODE):
+                self.pos = self.move_by_word(self.line, self.pos, 1)
+
+            # If in/after last word, interchange last two words
+            if (re.match(r'[\w\d]*\s*$', self.line[self.pos:], re.UNICODE)
+                and (re.match(r'[\w\d]', self.line[self.pos - 1], re.UNICODE)
+                     if self.pos - 1 >= 0 else True)):
+                self.pos = self.move_by_word(self.line, self.pos, -1)
+
+            # Util function to increment position until out of word/whitespace
+            def _traverse(line, pos, regex):
+                while pos < len(line) and re.match(
+                        regex, line[pos], re.UNICODE):
+                    pos += 1
+                return pos
+
+            # Calculate endpoints of target words and pass them to
+            # 'self.transpose_subr'
+            x_begin = self.move_by_word(self.line, self.pos, -1)
+            x_end = _traverse(self.line, x_begin, r'[\w\d]')
+            x = x_begin, x_end
+
+            y_begin = self.pos
+
+            # If in middle of word, move to end
+            if re.match(r'[\w\d]', self.line[self.pos - 1], re.UNICODE):
+                y_begin = _traverse(self.line, y_begin, r'[\w\d]')
+
+            # Traverse whitespace to beginning of next word
+            y_begin = _traverse(self.line, y_begin, r'\s')
+
+            y_end = _traverse(self.line, y_begin, r'[\w\d]')
+            y = y_begin, y_end
+
+            self.line = self.transpose_subr(self.line, x, y)
+            self.pos = y[1]
+            self.on_line_change()
+
     def execute(self, cmd=None):
         if self.question_queue and cmd is None:
             question = self.question_queue[0]
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index fd44613e..4873c07a 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -42,6 +42,8 @@ class StatusBar(Widget):  # pylint: disable=too-many-instance-attributes
         self.column = column
         self.settings.signal_bind('setopt.display_size_in_status_bar',
                                   self.request_redraw, weak=True)
+        self.fm.signal_bind('tab.layoutchange', self.request_redraw, weak=True)
+        self.fm.signal_bind('setop.viewmode', self.request_redraw, weak=True)
 
     def request_redraw(self):
         self.need_redraw = True
@@ -52,9 +54,13 @@ class StatusBar(Widget):  # pylint: disable=too-many-instance-attributes
     def clear_message(self):
         self.msg = None
 
-    def draw(self):
+    def draw(self):  # pylint: disable=too-many-branches
         """Draw the statusbar"""
 
+        if self.column != self.fm.ui.browser.main_column:
+            self.column = self.fm.ui.browser.main_column
+            self.need_redraw = True
+
         if self.hint and isinstance(self.hint, str):
             if self.old_hint != self.hint:
                 self.need_redraw = True
diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py
index 765c1248..e9eb1b8b 100644
--- a/ranger/gui/widgets/titlebar.py
+++ b/ranger/gui/widgets/titlebar.py
@@ -114,12 +114,14 @@ class TitleBar(Widget):
             else:
                 clr = 'directory'
 
-            bar.add(path.basename, clr, directory=path)
+            bidi_basename = self.bidi_transpose(path.basename)
+            bar.add(bidi_basename, clr, directory=path)
             bar.add('/', clr, fixed=True, directory=path)
 
         if self.fm.thisfile is not None and \
                 self.settings.show_selection_in_titlebar:
-            bar.add(self.fm.thisfile.relative_path, 'file')
+            bidi_file_path = self.bidi_transpose(self.fm.thisfile.relative_path)
+            bar.add(bidi_file_path, 'file')
 
     def _get_right_part(self, bar):
         # TODO: fix that pressed keys are cut off when chaining CTRL keys
diff --git a/ranger/gui/widgets/view_multipane.py b/ranger/gui/widgets/view_multipane.py
index 9661d31e..cef8d9a8 100644
--- a/ranger/gui/widgets/view_multipane.py
+++ b/ranger/gui/widgets/view_multipane.py
@@ -3,6 +3,7 @@
 
 from __future__ import (absolute_import, division, print_function)
 
+import curses
 from ranger.gui.widgets.view_base import ViewBase
 from ranger.gui.widgets.browsercolumn import BrowserColumn
 
@@ -16,6 +17,17 @@ class ViewMultipane(ViewBase):  # pylint: disable=too-many-ancestors
         self.fm.signal_bind('tab.change', self._tabchange_handler)
         self.rebuild()
 
+        self.old_draw_borders = self._draw_borders_setting()
+
+    def _draw_borders_setting(self):
+        # If draw_borders_multipane has not been set, it defaults to `None`
+        # and we fallback to using draw_borders. Important to note:
+        # `None` is different from the string "none" referring to no borders
+        if self.settings.draw_borders_multipane is not None:
+            return self.settings.draw_borders_multipane
+        else:
+            return self.settings.draw_borders
+
     def _layoutchange_handler(self):
         if self.fm.ui.browser == self:
             self.rebuild()
@@ -43,13 +55,101 @@ class ViewMultipane(ViewBase):  # pylint: disable=too-many-ancestors
             self.add_child(column)
         self.resize(self.y, self.x, self.hei, self.wid)
 
+    def draw(self):
+        if self.need_clear:
+            self.win.erase()
+            self.need_redraw = True
+            self.need_clear = False
+
+        ViewBase.draw(self)
+
+        if self._draw_borders_setting():
+            draw_borders = self._draw_borders_setting()
+            if draw_borders in ['both', 'true']:   # 'true' for backwards compat.
+                border_types = ['separators', 'outline']
+            else:
+                border_types = [draw_borders]
+            self._draw_borders(border_types)
+        if self.draw_bookmarks:
+            self._draw_bookmarks()
+        elif self.draw_hints:
+            self._draw_hints()
+        elif self.draw_info:
+            self._draw_info(self.draw_info)
+
+    def _draw_border_rectangle(self, left_start, right_end):
+        win = self.win
+        win.hline(0, left_start, curses.ACS_HLINE, right_end - left_start)
+        win.hline(self.hei - 1, left_start, curses.ACS_HLINE, right_end - left_start)
+        win.vline(1, left_start, curses.ACS_VLINE, self.hei - 2)
+        win.vline(1, right_end, curses.ACS_VLINE, self.hei - 2)
+        # Draw the four corners
+        self.addch(0, left_start, curses.ACS_ULCORNER)
+        self.addch(self.hei - 1, left_start, curses.ACS_LLCORNER)
+        self.addch(0, right_end, curses.ACS_URCORNER)
+        self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER)
+
+    def _draw_borders(self, border_types):
+        # Referenced from ranger.gui.widgets.view_miller
+        win = self.win
+        self.color('in_browser', 'border')
+
+        left_start = 0
+        right_end = self.wid - 1
+
+        # Draw the outline borders
+        if 'active-pane' not in border_types:
+            if 'outline' in border_types:
+                try:
+                    self._draw_border_rectangle(left_start, right_end)
+                except curses.error:
+                    pass
+
+            # Draw the column separators
+            if 'separators' in border_types:
+                for child in self.columns[:-1]:
+                    x = child.x + child.wid
+                    y = self.hei - 1
+                    try:
+                        win.vline(1, x, curses.ACS_VLINE, y - 1)
+                        if 'outline' in border_types:
+                            self.addch(0, x, curses.ACS_TTEE, 0)
+                            self.addch(y, x, curses.ACS_BTEE, 0)
+                        else:
+                            self.addch(0, x, curses.ACS_VLINE, 0)
+                            self.addch(y, x, curses.ACS_VLINE, 0)
+                    except curses.error:
+                        pass
+        else:
+            bordered_column = self.main_column
+            left_start = max(bordered_column.x, 0)
+            right_end = min(left_start + bordered_column.wid, self.wid - 1)
+            try:
+                self._draw_border_rectangle(left_start, right_end)
+            except curses.error:
+                pass
+
     def resize(self, y, x, hei=None, wid=None):
         ViewBase.resize(self, y, x, hei, wid)
+
+        border_type = self._draw_borders_setting()
+        if border_type in ['outline', 'both', 'true', 'active-pane']:
+            # 'true' for backwards compat., no height pad needed for 'separators'
+            pad = 1
+        else:
+            pad = 0
         column_width = int((wid - len(self.columns) + 1) / len(self.columns))
         left = 0
         top = 0
         for column in self.columns:
-            column.resize(top, left, hei, max(1, column_width))
+            column.resize(top + pad, left, hei - pad * 2, max(1, column_width))
             left += column_width + 1
             column.need_redraw = True
         self.need_redraw = True
+
+    def poke(self):
+        ViewBase.poke(self)
+
+        if self.old_draw_borders != self._draw_borders_setting():
+            self.resize(self.y, self.x, self.hei, self.wid)
+            self.old_draw_borders = self._draw_borders_setting()