about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--HACKING.md2
-rw-r--r--Makefile7
-rw-r--r--README.md53
-rw-r--r--doc/ranger.141
-rw-r--r--doc/ranger.pod15
-rw-r--r--doc/rifle.122
-rw-r--r--ranger/api/commands.py3
-rwxr-xr-xranger/config/commands.py13
-rw-r--r--ranger/config/rc.conf3
-rw-r--r--ranger/config/rifle.conf12
-rw-r--r--ranger/container/bookmarks.py19
-rw-r--r--ranger/container/fsobject.py12
-rw-r--r--ranger/core/actions.py68
-rw-r--r--ranger/core/loader.py6
-rw-r--r--ranger/core/main.py30
-rwxr-xr-xranger/data/scope.sh59
-rw-r--r--ranger/ext/direction.py3
-rw-r--r--ranger/ext/human_readable.py10
-rw-r--r--ranger/ext/img_display.py4
-rw-r--r--ranger/ext/iter_tools.py3
-rw-r--r--ranger/ext/keybinding_parser.py2
-rw-r--r--ranger/ext/lazy_property.py3
-rwxr-xr-xranger/ext/rifle.py2
-rw-r--r--ranger/ext/signals.py3
-rw-r--r--ranger/ext/vcs/svn.py2
-rw-r--r--ranger/ext/widestring.py2
-rw-r--r--ranger/gui/ansi.py3
-rw-r--r--ranger/gui/curses_shortcuts.py6
-rw-r--r--ranger/gui/widgets/console.py3
-rw-r--r--ranger/gui/widgets/statusbar.py8
-rw-r--r--tests/ranger/container/test_bookmarks.py26
31 files changed, 325 insertions, 120 deletions
diff --git a/HACKING.md b/HACKING.md
index 57ac90e6..6e200a43 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -50,7 +50,7 @@ Good places to read about ranger internals are:
 About the UI:
 
 * `ranger/gui/widgets/browsercolumn.py`
-* `ranger/gui/widgets/browserview.py`
+* `ranger/gui/widgets/view_miller.py`
 * `ranger/gui/ui.py`
 
 
diff --git a/Makefile b/Makefile
index 79b4f9d4..c6b3b35d 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,7 @@ endif
 SETUPOPTS ?= '--record=install_log.txt'
 DOCDIR ?= doc/pydoc
 DESTDIR ?= /
+PREFIX ?= /usr/local
 PYOPTIMIZE ?= 1
 FILTER ?= .
 
@@ -55,7 +56,8 @@ help:
 
 install:
 	$(PYTHON) setup.py install $(SETUPOPTS) \
-		'--root=$(DESTDIR)' --optimize=$(PYOPTIMIZE)
+		'--prefix=$(PREFIX)' '--root=$(DESTDIR)' \
+		--optimize=$(PYOPTIMIZE)
 
 compile: clean
 	PYTHONOPTIMIZE=$(PYOPTIMIZE) $(PYTHON) -m compileall -q ranger
@@ -96,7 +98,8 @@ test_flake8:
 
 test_doctest:
 	@echo "Running doctests..."
-	@for FILE in $(shell grep -IHm 1 doctest -r ranger | grep $(FILTER) | cut -d: -f1); do \
+	@set -e; \
+	for FILE in $(shell grep -IHm 1 doctest -r ranger | grep $(FILTER) | cut -d: -f1); do \
 		echo "Testing $$FILE..."; \
 		RANGER_DOCTEST=1 PYTHONPATH=".:"$$PYTHONPATH ${PYTHON} $$FILE; \
 	done
diff --git a/README.md b/README.md
index d7ca0493..6ef246a8 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,9 @@ ranger 1.9.2
 ============
 
 [![Build Status](https://travis-ci.org/ranger/ranger.svg?branch=master)](https://travis-ci.org/ranger/ranger)
+<a href="https://repology.org/metapackage/ranger/versions">
+  <img src="https://repology.org/badge/latest-versions/ranger.svg" alt="latest packaged version(s)">
+</a>
 
 ranger is a console file manager with VI key bindings.  It provides a
 minimalistic and nice curses interface with a view on the directory hierarchy.
@@ -11,11 +14,12 @@ out which program to use for what file type.
 ![screenshot](https://raw.githubusercontent.com/ranger/ranger-assets/master/screenshots/screenshot.png)
 
 This file describes ranger and how to get it to run.  For instructions on the
-usage, please read the man page.  See `HACKING.md` for development specific
-information.
+usage, please read the man page (`man ranger` in a terminal).  See `HACKING.md`
+for development-specific information.
 
 For configuration, check the files in `ranger/config/` or copy the
-default config to `~/.config/ranger` with `ranger --copy-config`.
+default config to `~/.config/ranger` with `ranger --copy-config`
+(see [instructions](#getting-started)).
 
 The `examples/` directory contains several scripts and plugins that demonstrate how
 ranger can be extended or combined with other programs.  These files can be
@@ -40,12 +44,12 @@ Design Goals
 * An easily maintainable file manager in a high level language
 * A quick way to switch directories and browse the file system
 * Keep it small but useful, do one thing and do it well
-* Console based, with smooth integration into the unix shell
+* Console-based, with smooth integration into the unix shell
 
 
 Features
 --------
-* UTF-8 Support  (if your python copy supports it)
+* UTF-8 Support  (if your Python copy supports it)
 * Multi-column display
 * Preview of the selected file/directory
 * Common file operations (create/chmod/copy/delete/...)
@@ -53,20 +57,20 @@ Features
 * VIM-like console and hotkeys
 * Automatically determine file types and run them with correct programs
 * Change the directory of your shell after exiting ranger
-* Tabs, bookmarks, mouse support
+* Tabs, bookmarks, mouse support...
 
 
 Dependencies
 ------------
 * Python (`>=2.6` or `>=3.1`) with the `curses` module
-  and (optionally) wide-unicode support.
+  and (optionally) wide-unicode support
 * A pager (`less` by default)
 
 Optional:
 
 * The `file` program for determining file types
-* The python module `chardet`, in case of encoding detection problems
-* `sudo` to use the "run as root"-feature
+* The Python module `chardet`, in case of encoding detection problems
+* `sudo` to use the "run as root" feature
 * `w3m` for the `w3mimgdisplay` program to preview images
 * `python-bidi` for correct display of RTL file names (Hebrew, Arabic)
 
@@ -76,8 +80,8 @@ Optional, for enhanced file previews (with `scope.sh`):
 * `highlight` or `pygmentize` for syntax highlighting of code
 * `atool`, `bsdtar` and/or `unrar` for previews of archives
 * `lynx`, `w3m` or `elinks` for previews of html pages
-* `pdftotext` or `mutool` for pdf previews
-* `transmission-show` for viewing bit-torrent information
+* `pdftotext` or `mutool` for `pdf` previews
+* `transmission-show` for viewing BitTorrent information
 * `mediainfo` or `exiftool` for viewing information about media files
 * `odt2txt` for OpenDocument text files (`odt`, `ods`, `odp` and `sxw`)
 * `chardet` (Python package) for improved encoding detection of text files
@@ -86,7 +90,24 @@ Optional, for enhanced file previews (with `scope.sh`):
 Installing
 ----------
 Use the package manager of your operating system to install ranger.
-Note that ranger can be started without installing by simply running `ranger.py`.
+You can also install ranger through PyPI: ```pip install ranger-fm```.
+
+<details>
+  <summary>
+    Check current version:
+    <sub>
+      <a href="https://repology.org/metapackage/ranger/versions">
+        <img src="https://repology.org/badge/tiny-repos/ranger.svg" alt="Packaging status">
+      </a>
+    </sub>
+  </summary>
+  <a href="https://repology.org/metapackage/ranger/versions">
+    <img src="https://repology.org/badge/vertical-allrepos/ranger.svg" alt="Packaging status">
+  </a>
+</details>
+
+### Installing from a clone
+Note that you don't *have* to install ranger; you can simply run `ranger.py`.
 
 To install ranger manually:
 ```
@@ -104,10 +125,10 @@ use to uninstall ranger.
 
 Getting Started
 ---------------
-After starting ranger, you can use the Arrow Keys or `h` `j` `k` `l` to navigate, `Enter`
-to open a file or type `Q` to quit.  The third column shows a preview of the
-current file.  The second is the main column and the first shows the parent
-directory.
+After starting ranger, you can use the Arrow Keys or `h` `j` `k` `l` to
+navigate, `Enter` to open a file or `q` to quit.  The third column shows a
+preview of the current file.  The second is the main column and the first shows
+the parent directory.
 
 Ranger can automatically copy default configuration files to `~/.config/ranger`
 if you run it with the switch `--copy-config=( rc | scope | ... | all )`.
diff --git a/doc/ranger.1 b/doc/ranger.1
index 38926f87..43a13659 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -1,4 +1,4 @@
-.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35)
+.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35)
 .\"
 .\" Standard preamble:
 .\" ========================================================================
@@ -54,20 +54,16 @@
 .\" Avoid warning from groff about undefined register 'F'.
 .de IX
 ..
-.nr rF 0
-.if \n(.g .if rF .nr rF 1
-.if (\n(rF:(\n(.g==0)) \{\
-.    if \nF \{\
-.        de IX
-.        tm Index:\\$1\t\\n%\t"\\$2"
+.if !\nF .nr F 0
+.if \nF>0 \{\
+.    de IX
+.    tm Index:\\$1\t\\n%\t"\\$2"
 ..
-.        if !\nF==2 \{\
-.            nr % 0
-.            nr F 2
-.        \}
+.    if !\nF==2 \{\
+.        nr % 0
+.        nr F 2
 .    \}
 .\}
-.rr rF
 .\"
 .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
 .\" Fear.  Run.  Save yourself.  No user-serviceable parts.
@@ -173,7 +169,7 @@ plugins, sample configuration files and some programs for integrating ranger
 with other software.  They are usually installed to
 \&\fI/usr/share/doc/ranger/examples\fR.
 .PP
-The man page of \fBrifle\fR\|(1) describes the functions of the file opener
+The man page of \fIrifle\fR\|(1) describes the functions of the file opener
 .PP
 The section \fI\s-1LINKS\s0\fR of this man page contains further resources.
 .SH "POSITIONAL ARGUMENTS"
@@ -415,7 +411,7 @@ Note: The bookmarks ' (Apostrophe) and ` (Backtick) are the same.
 .SS "\s-1RIFLE\s0"
 .IX Subsection "RIFLE"
 Rifle is the file opener of ranger.  It can be used as a standalone program or
-a python module.  It is located at \fIranger/ext/rifle.py\fR.  In contrast to
+a python module.  It is located at \fI\f(CI$repo\fI/ranger/ext/rifle.py\fR.  In contrast to
 other, more simple file openers, rifle can automatically find installed
 programs so it can be used effectively out of the box on a variety of systems.
 .PP
@@ -427,14 +423,16 @@ by typing \*(L"<rulenumber><enter>\*(R".  If you use rifle standalone, you can l
 rules with the \*(L"\-l\*(R" option and pick a rule with \*(L"\-p <number>\*(R".
 .PP
 The rules, along with further documentation, are contained in
-\&\fIranger/config/rifle.conf\fR.
+\&\fI\f(CI$repo\fI/ranger/config/rifle.conf\fR.
 .SS "\s-1FLAGS\s0"
 .IX Subsection "FLAGS"
 Flags give you a way to modify the behavior of the spawned process.  They are
 used in the commands \f(CW\*(C`:open_with\*(C'\fR (key \*(L"r\*(R") and \f(CW\*(C`:shell\*(C'\fR (key \*(L"!\*(R").
 .PP
-.Vb 4
-\& f   Fork the process.  (Run in background)
+.Vb 6
+\& f   Fork the process, i.e. run in background.  Please use this flag instead of
+\&     calling "disown" or "nohup", to avoid killing the background command when
+\&     pressing Ctrl+C in ranger.
 \& c   Run the current file only, instead of the selection
 \& r   Run application with root privilege (requires sudo)
 \& t   Run application in a new terminal window
@@ -945,6 +943,9 @@ Start line numbers from 1.  Possible values are:
 .IX Item "open_all_images [bool]"
 Open all images in this directory when running certain image viewers like feh
 or sxiv?  You can still open selected files by marking them.
+.Sp
+If there would be too many files for the system to handle, this option
+will be temporarily disabled automatically.
 .IP "padding_right [bool]" 4
 .IX Item "padding_right [bool]"
 When collapse_preview is on and there is no preview, should there remain a
@@ -1090,7 +1091,7 @@ Enable this if key combinations with the Alt Key don't work for you.
 .IX Header "COMMANDS"
 You can enter the commands in the console which is opened by pressing \*(L":\*(R".
 .PP
-You can always get a list of the currently existing commands by typing \*(L"2?\*(R" in
+You can always get a list of the currently existing commands by typing \*(L"?c\*(R" in
 ranger.  For your convenience, this is a list of the \*(L"public\*(R" commands including their parameters, excluding descriptions:
 .PP
 .Vb 10
@@ -1611,7 +1612,7 @@ by checking for this variable.
 .IP "\s-1RANGER_LOAD_DEFAULT_RC\s0" 8
 .IX Item "RANGER_LOAD_DEFAULT_RC"
 If this variable is set to \s-1FALSE,\s0 ranger will not load the default rc.conf.
-This can save time if you copied the whole rc.conf to ~/.config/ranger/ and
+This can save time if you copied the whole rc.conf to \fI~/.config/ranger/\fR and
 don't need the default one at all.
 .IP "\s-1VISUAL\s0" 8
 .IX Item "VISUAL"
@@ -1675,7 +1676,7 @@ copy, run:
 .Ve
 .SH "SEE ALSO"
 .IX Header "SEE ALSO"
-\&\fBrifle\fR\|(1)
+\&\fIrifle\fR\|(1)
 .SH "BUGS"
 .IX Header "BUGS"
 Report bugs here: <https://github.com/ranger/ranger/issues>
diff --git a/doc/ranger.pod b/doc/ranger.pod
index b67ded7f..d182f3af 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -320,7 +320,7 @@ Note: The bookmarks ' (Apostrophe) and ` (Backtick) are the same.
 =head2 RIFLE
 
 Rifle is the file opener of ranger.  It can be used as a standalone program or
-a python module.  It is located at F<ranger/ext/rifle.py>.  In contrast to
+a python module.  It is located at F<$repo/ranger/ext/rifle.py>.  In contrast to
 other, more simple file openers, rifle can automatically find installed
 programs so it can be used effectively out of the box on a variety of systems.
 
@@ -332,14 +332,16 @@ by typing "<rulenumber><enter>".  If you use rifle standalone, you can list all
 rules with the "-l" option and pick a rule with "-p <number>".
 
 The rules, along with further documentation, are contained in
-F<ranger/config/rifle.conf>.
+F<$repo/ranger/config/rifle.conf>.
 
 =head2 FLAGS
 
 Flags give you a way to modify the behavior of the spawned process.  They are
 used in the commands C<:open_with> (key "r") and C<:shell> (key "!").
 
- f   Fork the process.  (Run in background)
+ f   Fork the process, i.e. run in background.  Please use this flag instead of
+     calling "disown" or "nohup", to avoid killing the background command when
+     pressing Ctrl+C in ranger.
  c   Run the current file only, instead of the selection
  r   Run application with root privilege (requires sudo)
  t   Run application in a new terminal window
@@ -967,6 +969,9 @@ Start line numbers from 1.  Possible values are:
 Open all images in this directory when running certain image viewers like feh
 or sxiv?  You can still open selected files by marking them.
 
+If there would be too many files for the system to handle, this option
+will be temporarily disabled automatically.
+
 =item padding_right [bool]
 
 When collapse_preview is on and there is no preview, should there remain a
@@ -1149,7 +1154,7 @@ Enable this if key combinations with the Alt Key don't work for you.
 
 You can enter the commands in the console which is opened by pressing ":".
 
-You can always get a list of the currently existing commands by typing "2?" in
+You can always get a list of the currently existing commands by typing "?c" in
 ranger.  For your convenience, this is a list of the "public" commands including their parameters, excluding descriptions:
 
  alias [newcommand] [oldcommand]
@@ -1753,7 +1758,7 @@ by checking for this variable.
 =item RANGER_LOAD_DEFAULT_RC
 
 If this variable is set to FALSE, ranger will not load the default rc.conf.
-This can save time if you copied the whole rc.conf to ~/.config/ranger/ and
+This can save time if you copied the whole rc.conf to I<~/.config/ranger/> and
 don't need the default one at all.
 
 =item VISUAL
diff --git a/doc/rifle.1 b/doc/rifle.1
index 9ed1a145..a42734d2 100644
--- a/doc/rifle.1
+++ b/doc/rifle.1
@@ -1,4 +1,4 @@
-.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35)
+.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35)
 .\"
 .\" Standard preamble:
 .\" ========================================================================
@@ -54,20 +54,16 @@
 .\" Avoid warning from groff about undefined register 'F'.
 .de IX
 ..
-.nr rF 0
-.if \n(.g .if rF .nr rF 1
-.if (\n(rF:(\n(.g==0)) \{\
-.    if \nF \{\
-.        de IX
-.        tm Index:\\$1\t\\n%\t"\\$2"
+.if !\nF .nr F 0
+.if \nF>0 \{\
+.    de IX
+.    tm Index:\\$1\t\\n%\t"\\$2"
 ..
-.        if !\nF==2 \{\
-.            nr % 0
-.            nr F 2
-.        \}
+.    if !\nF==2 \{\
+.        nr % 0
+.        nr F 2
 .    \}
 .\}
-.rr rF
 .\"
 .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
 .\" Fear.  Run.  Save yourself.  No user-serviceable parts.
@@ -133,7 +129,7 @@
 .\" ========================================================================
 .\"
 .IX Title "RIFLE 1"
-.TH RIFLE 1 "rifle-1.9.2" "2018-09-09" "rifle manual"
+.TH RIFLE 1 "rifle-1.9.2" "2019-04-03" "rifle manual"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
diff --git a/ranger/api/commands.py b/ranger/api/commands.py
index 9c687927..90a79488 100644
--- a/ranger/api/commands.py
+++ b/ranger/api/commands.py
@@ -448,4 +448,5 @@ def command_function_factory(func):
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index d177203a..36a6070e 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -996,7 +996,7 @@ class rename_append(Command):
         relpath = tfile.relative_path.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
         basename = tfile.basename.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
 
-        if basename.find('.') <= 0:
+        if basename.find('.') <= 0 or os.path.isdir(relpath):
             self.fm.open_console('rename ' + relpath)
             return
 
@@ -1639,6 +1639,17 @@ class flat(Command):
         self.fm.thisdir.flat = level
         self.fm.thisdir.load_content()
 
+
+class reset_previews(Command):
+    """:reset_previews
+
+    Reset the file previews.
+    """
+    def execute(self):
+        self.fm.previews = {}
+        self.fm.ui.need_redraw = True
+
+
 # Version control commands
 # --------------------------------
 
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 8913213c..f559290d 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -478,6 +478,9 @@ map pl paste_symlink relative=False
 map pL paste_symlink relative=True
 map phl paste_hardlink
 map pht paste_hardlinked_subtree
+map pd console paste dest=
+map p`<any> paste dest=%any_path
+map p'<any> paste dest=%any_path
 
 map dD console delete
 
diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf
index babdcda7..fb9b70f0 100644
--- a/ranger/config/rifle.conf
+++ b/ranger/config/rifle.conf
@@ -98,7 +98,7 @@ ext exe                       = wine "$1"
 name ^[mM]akefile$            = make
 
 #--------------------------------------------
-# Code
+# Scripts
 #-------------------------------------------
 ext py  = python -- "$1"
 ext pl  = perl -- "$1"
@@ -131,7 +131,7 @@ mime ^video|audio, has totem,    X, flag f = totem -- "$@"
 mime ^video|audio, has totem,    X, flag f = totem --fullscreen -- "$@"
 
 #--------------------------------------------
-# Video without X:
+# Video without X
 #-------------------------------------------
 mime ^video, terminal, !X, has mpv       = mpv -- "$@"
 mime ^video, terminal, !X, has mplayer2  = mplayer2 -- "$@"
@@ -170,11 +170,12 @@ ext epub, has ebook-viewer, X, flag f = ebook-viewer -- "$@"
 ext mobi, has ebook-viewer, X, flag f = ebook-viewer -- "$@"
 
 #-------------------------------------------
-# Image Viewing:
+# Images
 #-------------------------------------------
 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 -- "$@"
@@ -212,6 +213,11 @@ ext rar, has unrar = unrar l "$1" | less
 ext rar, has unrar = for file in "$@"; do unrar x "$file"; done
 
 #-------------------------------------------
+# Fonts
+#-------------------------------------------
+mime ^font, has fontforge, X, flag f = fontforge "$@"
+
+#-------------------------------------------
 # Flag t fallback terminals
 #-------------------------------------------
 # Rarely installed terminal emulators get higher priority; It is assumed that
diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py
index 59838c00..7b5e56b7 100644
--- a/ranger/container/bookmarks.py
+++ b/ranger/container/bookmarks.py
@@ -88,7 +88,11 @@ class Bookmarks(FileManagerAware):
         if key == '`':
             key = "'"
         if key in self.dct:
-            return self.dct[key]
+            value = self.dct[key]
+            if self._validate(value):
+                return value
+            else:
+                raise KeyError("Cannot open bookmark: `%s'!" % key)
         else:
             raise KeyError("Nonexistant Bookmark: `%s'!" % key)
 
@@ -185,7 +189,13 @@ class Bookmarks(FileManagerAware):
             old_perms = os.stat(self.path)
             os.chown(path_new, old_perms.st_uid, old_perms.st_gid)
             os.chmod(path_new, old_perms.st_mode)
-            os.rename(path_new, self.path)
+
+            if os.path.islink(self.path):
+                target_path = os.path.realpath(self.path)
+                os.rename(path_new, target_path)
+            else:
+                os.rename(path_new, self.path)
+
         except OSError as ex:
             self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True)
             return
@@ -223,7 +233,7 @@ class Bookmarks(FileManagerAware):
         for line in fobj:
             if self.load_pattern.match(line):
                 key, value = line[0], line[2:-1]
-                if key in ALLOWED_KEYS and not os.path.isfile(value):
+                if key in ALLOWED_KEYS:
                     dct[key] = self.bookmarktype(value)
         fobj.close()
         return dct
@@ -247,3 +257,6 @@ class Bookmarks(FileManagerAware):
 
     def _update_mtime(self):
         self.last_mtime = self._get_mtime()
+
+    def _validate(self, value):  # pylint: disable=no-self-use
+        return os.path.isdir(str(value))
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index a82532c9..4fb47354 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -29,11 +29,13 @@ except AttributeError:
 
 
 CONTAINER_EXTENSIONS = ('7z', 'ace', 'ar', 'arc', 'bz', 'bz2', 'cab', 'cpio',
-                        'cpt', 'deb', 'dgc', 'dmg', 'gz', 'iso', 'jar', 'msi', 'pkg', 'rar',
-                        'shar', 'tar', 'tbz', 'tgz', 'xar', 'xpi', 'xz', 'zip')
-DOCUMENT_EXTENSIONS = ('cfg', 'css', 'cvs', 'djvu', 'doc', 'docx', 'gnm',
-                       'gnumeric', 'htm', 'html', 'md', 'odf', 'odg', 'odp', 'ods', 'odt', 'pdf',
-                       'pod', 'ps', 'rtf', 'sxc', 'txt', 'xls', 'xlw', 'xml', 'xslx')
+                        'cpt', 'deb', 'dgc', 'dmg', 'gz', 'iso', 'jar', 'msi',
+                        'pkg', 'rar', 'shar', 'tar', 'tbz', 'tgz', 'txz',
+                        'xar', 'xpi', 'xz', 'zip')
+DOCUMENT_EXTENSIONS = ('cbr', 'cbz', 'cfg', 'css', 'cvs', 'djvu', 'doc',
+                       'docx', 'gnm', 'gnumeric', 'htm', 'html', 'md', 'odf',
+                       'odg', 'odp', 'ods', 'odt', 'pdf', 'pod', 'ps', 'rtf',
+                       'sxc', 'txt', 'xls', 'xlw', 'xml', 'xslx')
 DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits',
                       'hacking', 'help', 'install', 'license', 'readme', 'todo')
 
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 6cf376eb..e7be0c65 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -247,8 +247,6 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 try:
                     val = self.fm.bookmarks[key_to_string(char)]
                 except KeyError:
-                    self.notify('No bookmark defined for `{}`'.format(
-                        key_to_string(char)), bad=True)
                     val = MACRO_FAIL
                 return ('any_path{:d}'.format(i), val)
 
@@ -412,7 +410,7 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                         raise
                     self.notify('Error in line `%s\':\n  %s' % (line, str(ex)), bad=True)
 
-    def execute_file(self, files, **kw):
+    def execute_file(self, files, **kw):  # pylint: disable=too-many-branches
         """Uses the "rifle" module to open/execute a file
 
         Arguments are the same as for ranger.ext.rifle.Rifle.execute:
@@ -459,8 +457,23 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         self.signal_emit('execute.before', keywords=kw)
         filenames = [f.path for f in files]
         label = kw.get('label', kw.get('app', None))
-        try:
+
+        def execute():
             return self.rifle.execute(filenames, mode, label, flags, None)
+        try:
+            return execute()
+        except OSError as err:
+            # Argument list too long.
+            if err.errno == 7 and self.settings.open_all_images:
+                old_value = self.settings.open_all_images
+                try:
+                    self.notify("Too many files: Disabling open_all_images temporarily.")
+                    self.settings.open_all_images = False
+                    return execute()
+                finally:
+                    self.settings.open_all_images = old_value
+            else:
+                raise
         finally:
             self.signal_emit('execute.after')
 
@@ -508,6 +521,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 selection = self.thistab.get_selection()
             else:
                 selection = [tfile]
+            if tfile is None:
+                return
             if tfile.is_directory:
                 self.thistab.enter_dir(tfile)
             elif selection:
@@ -857,10 +872,16 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
     # Tags are saved in ~/.config/ranger/tagged and simply mark if a
     # file is important to you in any context.
 
-    def tag_toggle(self, paths=None, value=None, movedown=None, tag=None):
+    def tag_toggle(self, tag=None, paths=None, value=None, movedown=None):
         """:tag_toggle <character>
 
         Toggle a tag <character>.
+
+        Keyword arguments:
+            tag=<character>
+            paths=<paths to tag>
+            value=<True: add/False: remove/anything else: toggle>
+            movedown=<boolean>
         """
         if not self.tags:
             return
@@ -882,11 +903,19 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
 
         self.ui.redraw_main_column()
 
-    def tag_remove(self, paths=None, movedown=None, tag=None):
-        self.tag_toggle(paths=paths, value=False, movedown=movedown, tag=tag)
+    def tag_remove(self, tag=None, paths=None, movedown=None):
+        """:tag_remove <character>
 
-    def tag_add(self, paths=None, movedown=None, tag=None):
-        self.tag_toggle(paths=paths, value=True, movedown=movedown, tag=tag)
+        Remove a tag <character>. See :tag_toggle for keyword arguments.
+        """
+        self.tag_toggle(tag=tag, paths=paths, value=False, movedown=movedown)
+
+    def tag_add(self, tag=None, paths=None, movedown=None):
+        """:tag_add <character>
+
+        Add a tag <character>. See :tag_toggle for keyword arguments.
+        """
+        self.tag_toggle(tag=tag, paths=paths, value=True, movedown=movedown)
 
     # --------------------------
     # -- Bookmarks
@@ -1088,6 +1117,12 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             data['loading'] = False
             return path
 
+        if ranger.args.clean:
+            # Don't access args.cachedir in clean mode
+            return None
+
+        if not os.path.exists(ranger.args.cachedir):
+            os.makedirs(ranger.args.cachedir)
         cacheimg = os.path.join(ranger.args.cachedir, self.sha1_encode(path))
         if self.settings.preview_images and \
                 os.path.isfile(cacheimg) and \
@@ -1555,14 +1590,19 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 link(source_path,
                      next_available_filename(target_path))
 
-    def paste(self, overwrite=False, append=False):
+    def paste(self, overwrite=False, append=False, dest=None):
         """:paste
 
-        Paste the selected items into the current directory.
+        Paste the selected items into the current directory or to dest
+        if provided.
         """
-        loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite)
-        self.loader.add(loadable, append=append)
-        self.do_cut = False
+        if dest is None or isdir(dest):
+            loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite,
+                                  dest)
+            self.loader.add(loadable, append=append)
+            self.do_cut = False
+        else:
+            self.notify('Failed to paste. The given path is invalid.', bad=True)
 
     def delete(self, files=None):
         # XXX: warn when deleting mount points/unseen marked files?
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index 9f32535f..96d000ac 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -51,11 +51,11 @@ class Loadable(object):
 class CopyLoader(Loadable, FileManagerAware):  # pylint: disable=too-many-instance-attributes
     progressbar_supported = True
 
-    def __init__(self, copy_buffer, do_cut=False, overwrite=False):
+    def __init__(self, copy_buffer, do_cut=False, overwrite=False, dest=None):
         self.copy_buffer = tuple(copy_buffer)
         self.do_cut = do_cut
         self.original_copy_buffer = copy_buffer
-        self.original_path = self.fm.thistab.path
+        self.original_path = dest if dest is not None else self.fm.thistab.path
         self.overwrite = overwrite
         self.percent = 0
         if self.copy_buffer:
@@ -102,7 +102,7 @@ class CopyLoader(Loadable, FileManagerAware):  # pylint: disable=too-many-instan
                         tag = self.fm.tags.tags[path]
                         self.fm.tags.remove(path)
                         self.fm.tags.tags[
-                            path.replace(fobj.path, self.original_path + '/' + fobj.basename)
+                            path.replace(fobj.path, path.join(self.original_path, fobj.basename))
                         ] = tag
                         self.fm.tags.dump()
                 n = 0
diff --git a/ranger/core/main.py b/ranger/core/main.py
index 598ce243..23648677 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -105,7 +105,8 @@ def main(
         if not os.access(path_abs, os.F_OK):
             paths_inaccessible += [path]
     if paths_inaccessible:
-        print('Inaccessible paths: {0}'.format(paths), file=sys.stderr)
+        print('Inaccessible paths: {0}'.format(', '.join(paths_inaccessible)),
+              file=sys.stderr)
         return 1
 
     profile = None
@@ -145,11 +146,6 @@ def main(
             from ranger.ext import curses_interrupt_handler
             curses_interrupt_handler.install_interrupt_handler()
 
-        # Create cache directory
-        if fm.settings.preview_images and fm.settings.use_preview_script:
-            if not os.path.exists(args.cachedir):
-                os.makedirs(args.cachedir)
-
         if not args.clean:
             # Create data directory
             if not os.path.exists(args.datadir):
@@ -326,9 +322,14 @@ def parse_arguments():
             sys.exit(1)
         return path
 
-    args.cachedir = path_init('cachedir')
-    args.confdir = path_init('confdir')
-    args.datadir = path_init('datadir')
+    if args.clean:
+        args.cachedir = None
+        args.confdir = None
+        args.datadir = None
+    else:
+        args.cachedir = path_init('cachedir')
+        args.confdir = path_init('confdir')
+        args.datadir = path_init('datadir')
     if args.choosefile:
         args.choosefile = path_init('choosefile')
     if args.choosefiles:
@@ -407,8 +408,15 @@ def load_settings(  # pylint: disable=too-many-locals,too-many-branches,too-many
         except OSError:
             LOG.debug('Unable to access plugin directory: %s', plugindir)
         else:
-            plugins = [p[:-3] for p in plugin_files
-                       if p.endswith('.py') and not p.startswith('_')]
+            plugins = []
+            for path in plugin_files:
+                if not path.startswith('_'):
+                    if path.endswith('.py'):
+                        # remove trailing '.py'
+                        plugins.append(path[:-3])
+                    elif os.path.isdir(os.path.join(plugindir, path)):
+                        plugins.append(path)
+
             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')
diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh
index 8e0a0f6d..33666fcb 100755
--- a/ranger/data/scope.sh
+++ b/ranger/data/scope.sh
@@ -31,7 +31,7 @@ IMAGE_CACHE_PATH="${4}"  # Full path that should be used to cache image preview
 PV_IMAGE_ENABLED="${5}"  # 'True' if image previews are enabled, 'False' otherwise.
 
 FILE_EXTENSION="${FILE_PATH##*.}"
-FILE_EXTENSION_LOWER=$(echo ${FILE_EXTENSION} | tr '[:upper:]' '[:lower:]')
+FILE_EXTENSION_LOWER="$(printf "%s" "${FILE_EXTENSION}" | tr '[:upper:]' '[:lower:]')"
 
 # Settings
 HIGHLIGHT_SIZE_MAX=262143  # 256KiB
@@ -92,13 +92,24 @@ handle_extension() {
 }
 
 handle_image() {
+    # Size of the preview if there are multiple options or it has to be rendered
+    # from vector graphics. If the conversion program allows specifying only one
+    # dimension while keeping the aspect ratio, the width will be used.
+    local DEFAULT_SIZE="1920x1080"
+
     local mimetype="${1}"
     case "${mimetype}" in
         # SVG
         # image/svg+xml)
-        #     convert "${FILE_PATH}" "${IMAGE_CACHE_PATH}" && exit 6
+        #     convert -- "${FILE_PATH}" "${IMAGE_CACHE_PATH}" && exit 6
         #     exit 1;;
 
+        # DjVu
+        # image/vnd.djvu)
+        #     ddjvu -format=tiff -quality=90 -page=1 -size="${DEFAULT_SIZE}" \
+        #           - "${IMAGE_CACHE_PATH}" < "${FILE_PATH}" \
+        #           && exit 6 || exit 1;;
+
         # Image
         image/*)
             local orientation
@@ -110,7 +121,7 @@ handle_image() {
                 convert -- "${FILE_PATH}" -auto-orient "${IMAGE_CACHE_PATH}" && exit 6
             fi
 
-            # `w3mimgdisplay` will be called for all images (unless overriden as above),
+            # `w3mimgdisplay` will be called for all images (unless overridden as above),
             # but might fail for unsupported types.
             exit 7;;
 
@@ -119,16 +130,49 @@ handle_image() {
         #     # Thumbnail
         #     ffmpegthumbnailer -i "${FILE_PATH}" -o "${IMAGE_CACHE_PATH}" -s 0 && exit 6
         #     exit 1;;
+
         # PDF
         # application/pdf)
         #     pdftoppm -f 1 -l 1 \
-        #              -scale-to-x 1920 \
+        #              -scale-to-x "${DEFAULT_SIZE%x*}" \
         #              -scale-to-y -1 \
         #              -singlefile \
         #              -jpeg -tiffcompression jpeg \
         #              -- "${FILE_PATH}" "${IMAGE_CACHE_PATH%.*}" \
         #         && exit 6 || exit 1;;
 
+        # ePub, MOBI, FB2 (using Calibre)
+        # application/epub+zip|application/x-mobipocket-ebook|application/x-fictionbook+xml)
+        #     ebook-meta --get-cover="${IMAGE_CACHE_PATH}" -- "${FILE_PATH}" > /dev/null \
+        #         && exit 6 || exit 1;;
+
+        # ePub (using <https://github.com/marianosimone/epub-thumbnailer>)
+        # application/epub+zip)
+        #     epub-thumbnailer \
+        #         "${FILE_PATH}" "${IMAGE_CACHE_PATH}" "${DEFAULT_SIZE%x*}" \
+        #         && exit 6 || exit 1;;
+
+        # Font
+        application/font*|application/*opentype)
+            preview_png="/tmp/$(basename "${IMAGE_CACHE_PATH%.*}").png"
+            if fontimage -o "${preview_png}" \
+                         --pixelsize "120" \
+                         --fontname \
+                         --pixelsize "80" \
+                         --text "  ABCDEFGHIJKLMNOPQRSTUVWXYZ  " \
+                         --text "  abcdefghijklmnopqrstuvwxyz  " \
+                         --text "  0123456789.:,;(*!?') ff fl fi ffi ffl  " \
+                         --text "  The quick brown fox jumps over the lazy dog.  " \
+                         "${FILE_PATH}";
+            then
+                convert -- "${preview_png}" "${IMAGE_CACHE_PATH}" \
+                    && rm "${preview_png}" \
+                    && exit 6
+            else
+                exit 1
+            fi
+            ;;
+
         # Preview archives using the first image inside.
         # (Very useful for comic book collections for example.)
         # application/zip|application/x-rar|application/x-7z-compressed|\
@@ -189,6 +233,13 @@ handle_mime() {
             # pygmentize -f "${pygmentize_format}" -O "style=${PYGMENTIZE_STYLE}" -- "${FILE_PATH}" && exit 5
             exit 2;;
 
+        # DjVu
+        image/vnd.djvu)
+            # Preview as text conversion (requires djvulibre)
+            djvutxt "${FILE_PATH}" | fmt -w ${PV_WIDTH} && exit 5
+            exiftool "${FILE_PATH}" && exit 5
+            exit 1;;
+
         # Image
         image/*)
             # Preview as text conversion
diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py
index 33ebb604..32b69954 100644
--- a/ranger/ext/direction.py
+++ b/ranger/ext/direction.py
@@ -174,4 +174,5 @@ class Direction(dict):
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py
index 2a5bc81b..44f23079 100644
--- a/ranger/ext/human_readable.py
+++ b/ranger/ext/human_readable.py
@@ -71,5 +71,13 @@ def human_readable_time(timestamp):
 
 
 if __name__ == '__main__':
+
+    # XXX: This mock class is a temporary (as of 2019-01-27) hack.
+    class SettingsAwareMock(object):  # pylint: disable=too-few-public-methods
+        class settings(object):  # pylint: disable=invalid-name,too-few-public-methods
+            size_in_bytes = False
+    SettingsAware = SettingsAwareMock  # noqa: F811
+
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py
index 9c84ce5e..7c9f35a7 100644
--- a/ranger/ext/img_display.py
+++ b/ranger/ext/img_display.py
@@ -484,7 +484,7 @@ class URXVTImageFSDisplayer(URXVTImageDisplayer):
         return self._get_centered_offsets()
 
 
-class KittyImageDisplayer(ImageDisplayer):
+class KittyImageDisplayer(ImageDisplayer, FileManagerAware):
     """Implementation of ImageDisplayer for kitty (https://github.com/kovidgoyal/kitty/)
     terminal. It uses the built APC to send commands and data to kitty,
     which in turn renders the image. The APC takes the form
@@ -649,6 +649,8 @@ class KittyImageDisplayer(ImageDisplayer):
         # 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.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')
diff --git a/ranger/ext/iter_tools.py b/ranger/ext/iter_tools.py
index f321aee0..c710fbba 100644
--- a/ranger/ext/iter_tools.py
+++ b/ranger/ext/iter_tools.py
@@ -47,4 +47,5 @@ def unique(iterable):
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py
index e5fb6a86..8703d458 100644
--- a/ranger/ext/keybinding_parser.py
+++ b/ranger/ext/keybinding_parser.py
@@ -278,4 +278,4 @@ class KeyBuffer(object):  # pylint: disable=too-many-instance-attributes
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/ext/lazy_property.py b/ranger/ext/lazy_property.py
index bb54bd5e..42b61979 100644
--- a/ranger/ext/lazy_property.py
+++ b/ranger/ext/lazy_property.py
@@ -59,4 +59,5 @@ class lazy_property(object):  # pylint: disable=invalid-name,too-few-public-meth
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py
index 377f9b8a..ad7d0049 100755
--- a/ranger/ext/rifle.py
+++ b/ranger/ext/rifle.py
@@ -534,6 +534,6 @@ def main():  # pylint: disable=too-many-locals
 if __name__ == '__main__':
     if 'RANGER_DOCTEST' in os.environ:
         import doctest
-        doctest.testmod()
+        sys.exit(doctest.testmod()[0])
     else:
         main()
diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py
index 67c8960d..0973249c 100644
--- a/ranger/ext/signals.py
+++ b/ranger/ext/signals.py
@@ -278,4 +278,5 @@ class SignalDispatcher(object):
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py
index ef958a24..27216a43 100644
--- a/ranger/ext/vcs/svn.py
+++ b/ranger/ext/vcs/svn.py
@@ -98,6 +98,7 @@ class SVN(Vcs):
 
         # Paths with status
         lines = self._run(['status']).split('\n')
+        lines = list(filter(None, lines))
         if not lines:
             return 'sync'
         for line in lines:
@@ -116,6 +117,7 @@ class SVN(Vcs):
 
         # Paths with status
         lines = self._run(['status']).split('\n')
+        lines = list(filter(None, lines))
         for line in lines:
             code, path = line[0], line[8:]
             if code == ' ':
diff --git a/ranger/ext/widestring.py b/ranger/ext/widestring.py
index 2721643c..390da639 100644
--- a/ranger/ext/widestring.py
+++ b/ranger/ext/widestring.py
@@ -164,4 +164,4 @@ class WideString(object):  # pylint: disable=too-few-public-methods
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py
index ff8b2fd9..ce735317 100644
--- a/ranger/gui/ansi.py
+++ b/ranger/gui/ansi.py
@@ -175,4 +175,5 @@ def char_slice(ansi_text, start, length):
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py
index 14f1e0e4..fcd2be30 100644
--- a/ranger/gui/curses_shortcuts.py
+++ b/ranger/gui/curses_shortcuts.py
@@ -43,7 +43,7 @@ class CursesShortcuts(SettingsAware):
 
                 try:
                     self.win.addstr(*_fix_surrogates(args))
-                except (curses.error, UnicodeError):
+                except (curses.error, UnicodeError, ValueError, TypeError):
                     pass
 
     def addnstr(self, *args):
@@ -51,13 +51,13 @@ class CursesShortcuts(SettingsAware):
 
         try:
             self.win.addnstr(*args)
-        except (curses.error, TypeError):
+        except (curses.error, TypeError, ValueError, TypeError):
             if len(args) > 2:
                 self.win.move(y, x)
 
                 try:
                     self.win.addnstr(*_fix_surrogates(args))
-                except (curses.error, UnicodeError):
+                except (curses.error, UnicodeError, ValueError, TypeError):
                     pass
 
     def addch(self, *args):
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 13201e34..d796058c 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -530,4 +530,5 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
 
 if __name__ == '__main__':
     import doctest
-    doctest.testmod()
+    import sys
+    sys.exit(doctest.testmod()[0])
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index 3457955e..71064ed4 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -183,7 +183,11 @@ class StatusBar(Widget):  # pylint: disable=too-many-instance-attributes
                 left.add(target.infostring.replace(" ", ""))
                 left.add_space()
 
-            left.add(strftime(self.timeformat, localtime(stat.st_mtime)), 'mtime')
+            try:
+                date = strftime(self.timeformat, localtime(stat.st_mtime))
+            except OSError:
+                date = '?'
+            left.add(date, 'mtime')
 
         directory = target if target.is_directory else \
             target.fm.get_directory(os.path.dirname(target.path))
@@ -277,7 +281,7 @@ class StatusBar(Widget):  # pylint: disable=too-many-instance-attributes
             right.add(human_readable(target.disk_usage, separator='') + " sum")
             if self.settings.display_free_space_in_status_bar:
                 try:
-                    free = get_free_space(target.mount_path)
+                    free = get_free_space(target.path)
                 except OSError:
                     pass
                 else:
diff --git a/tests/ranger/container/test_bookmarks.py b/tests/ranger/container/test_bookmarks.py
index 6fba2a3d..84ac42c2 100644
--- a/tests/ranger/container/test_bookmarks.py
+++ b/tests/ranger/container/test_bookmarks.py
@@ -8,11 +8,16 @@ import pytest
 from ranger.container.bookmarks import Bookmarks
 
 
+class NotValidatedBookmarks(Bookmarks):
+    def _validate(self, value):
+        return True
+
+
 def testbookmarks(tmpdir):
     # Bookmarks point to directory location and allow fast access to
     # 'favorite' directories. They are persisted to a bookmark file, plain text.
     bookmarkfile = tmpdir.join("bookmarkfile")
-    bmstore = Bookmarks(str(bookmarkfile))
+    bmstore = NotValidatedBookmarks(str(bookmarkfile))
 
     # loading an empty bookmark file doesnot crash
     bmstore.load()
@@ -33,7 +38,7 @@ def testbookmarks(tmpdir):
 
     # We can persist bookmarks to disk and restore them from disk
     bmstore.save()
-    secondstore = Bookmarks(str(bookmarkfile))
+    secondstore = NotValidatedBookmarks(str(bookmarkfile))
     secondstore.load()
     assert "'" in secondstore
     assert secondstore["'"] == "the milk"
@@ -56,3 +61,20 @@ def testbookmarks(tmpdir):
         secondstore.update_if_outdated()
     secondstore.update = origupdate
     secondstore.update_if_outdated()
+
+
+def test_bookmark_symlink(tmpdir):
+    # Initialize plain file and symlink paths
+    bookmarkfile_link = tmpdir.join("bookmarkfile")
+    bookmarkfile_orig = tmpdir.join("bookmarkfile.orig")
+
+    # Create symlink pointing towards the original plain file.
+    os.symlink(str(bookmarkfile_orig), str(bookmarkfile_link))
+
+    # Initialize the bookmark file and save the file.
+    bmstore = Bookmarks(str(bookmarkfile_link))
+    bmstore.save()
+
+    # Once saved, the bookmark file should still be a symlink pointing towards the plain file.
+    assert os.path.islink(str(bookmarkfile_link))
+    assert not os.path.islink(str(bookmarkfile_orig))