summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--doc/ranger.pod2
-rw-r--r--ranger/config/rc.conf2
-rwxr-xr-xranger/data/scope.sh3
-rw-r--r--ranger/ext/shell_escape.py2
-rw-r--r--ranger/ext/vcs/bzr.py22
-rw-r--r--ranger/ext/vcs/git.py87
-rw-r--r--ranger/ext/vcs/hg.py10
-rw-r--r--ranger/ext/vcs/svn.py25
-rw-r--r--ranger/ext/vcs/vcs.py11
-rw-r--r--ranger/gui/displayable.py6
-rw-r--r--ranger/gui/ui.py14
-rw-r--r--ranger/gui/widgets/statusbar.py2
-rw-r--r--ranger/gui/widgets/view_miller.py5
14 files changed, 105 insertions, 91 deletions
diff --git a/README.md b/README.md
index b5fc03ea..5174f1c1 100644
--- a/README.md
+++ b/README.md
@@ -69,12 +69,13 @@ Optional:
 Optional, for enhanced file previews (with "scope.sh"):
 
 * img2txt (from caca-utils) for ASCII-art image previews
-* highlight for syntax highlighting of code
-* atool for previews of archives
+* highlight or pygmentize for syntax highlighting of code
+* atool, acat, bsdtar and/or unrar for previews of archives
 * lynx, w3m or elinks for previews of html pages
 * pdftotext for pdf previews
 * transmission-show for viewing bit-torrent information
 * mediainfo or exiftool for viewing information about media files
+* odt2txt for OpenDocument text files (odt, ods, odp and sxw)
 
 
 Installing
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 7c5a7768..6b0752bd 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -1063,7 +1063,7 @@ Examples:
 =item filter [I<string>]
 
 Displays only the files which contain the I<string> in their basename.  Running
-this command without any parameter will reset the fitler.
+this command without any parameter will reset the filter.
 
 This command is based on the I<scout> command and supports all of its options.
 
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 560fbe4d..cc7f5007 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -259,7 +259,7 @@ map !  console shell%space
 map @  console -p6 shell  %%s
 map #  console shell -p%space
 map s  console shell%space
-map r  chain draw_possible_programs; console open_with%space
+map r  chain draw_possible_programs; console open_with%%space
 map f  console find%space
 map cd console cd%space
 
diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh
index 669d1e34..44fcec2b 100755
--- a/ranger/data/scope.sh
+++ b/ranger/data/scope.sh
@@ -80,6 +80,9 @@ case "$extension" in
     # BitTorrent Files
     torrent)
         try transmission-show "$path" && { dump | trim; exit 5; } || exit 1;;
+    # ODT Files
+    odt|ods|odp|sxw)
+        try odt2txt "$path" && { dump | trim; exit 5; } || exit 1;;
     # HTML Pages:
     htm|html|xhtml)
         try w3m    -dump "$path" && { dump | trim | fmt -s -w $width; exit 4; }
diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py
index fe542084..d57ff339 100644
--- a/ranger/ext/shell_escape.py
+++ b/ranger/ext/shell_escape.py
@@ -3,7 +3,7 @@
 
 """Functions to escape metacharacters of arguments for shell commands."""
 
-META_CHARS = (' ', "'", '"', '`', '&', '|', ';',
+META_CHARS = (' ', "'", '"', '`', '&', '|', ';', '#',
         '$', '!', '(', ')', '[', ']', '<', '>', '\t')
 UNESCAPABLE = set(map(chr, list(range(9)) + list(range(10, 32))
         + list(range(127, 256))))
diff --git a/ranger/ext/vcs/bzr.py b/ranger/ext/vcs/bzr.py
index 5decc8b1..d0db1faf 100644
--- a/ranger/ext/vcs/bzr.py
+++ b/ranger/ext/vcs/bzr.py
@@ -25,7 +25,7 @@ class Bzr(Vcs):
     def _remote_url(self):
         """Remote url"""
         try:
-            return self._run(['config', 'parent_location']).rstrip('\n') or None
+            return self._run(['config', 'parent_location']) or None
         except VcsError:
             return None
 
@@ -87,29 +87,29 @@ class Bzr(Vcs):
         statuses = set()
 
         # Paths with status
-        output = self._run(['status', '--short', '--no-classify']).rstrip('\n')
-        if not output:
+        lines = self._run(['status', '--short', '--no-classify']).split('\n')
+        if not lines:
             return 'sync'
-        for line in output.split('\n'):
+        for line in lines:
             statuses.add(self._status_translate(line[:2]))
 
         for status in self.DIRSTATUSES:
             if status in statuses:
                 return status
+
         return 'sync'
 
     def data_status_subpaths(self):
         statuses = {}
 
         # Ignored
-        output = self._run(['ls', '--null', '--ignored']).rstrip('\x00')
-        if output:
-            for path in output.split('\x00'):
-                statuses[path] = 'ignored'
+        paths = self._run(['ls', '--null', '--ignored']).split('\0')[:-1]
+        for path in paths:
+            statuses[path] = 'ignored'
 
         # Paths with status
-        output = self._run(['status', '--short', '--no-classify']).rstrip('\n')
-        for line in output.split('\n'):
+        lines = self._run(['status', '--short', '--no-classify']).split('\n')
+        for line in lines:
             statuses[os.path.normpath(line[4:])] = self._status_translate(line[:2])
 
         return statuses
@@ -121,7 +121,7 @@ class Bzr(Vcs):
 
     def data_branch(self):
         try:
-            return self._run(['nick']).rstrip('\n') or None
+            return self._run(['nick']) or None
         except VcsError:
             return None
 
diff --git a/ranger/ext/vcs/git.py b/ranger/ext/vcs/git.py
index 8f4d9ff8..06e066d2 100644
--- a/ranger/ext/vcs/git.py
+++ b/ranger/ext/vcs/git.py
@@ -4,13 +4,19 @@
 """Git module"""
 
 from datetime import datetime
-import json
 import os
 import re
+import unicodedata
 
 from .vcs import Vcs, VcsError
 
 
+def string_control_replace(string, replacement):
+    """Replace all unicode control characters with replacement"""
+    return ''.join(
+        (replacement if unicodedata.category(char)[0] == 'C' else char for char in string))
+
+
 class Git(Vcs):
     """VCS implementation for Git"""
     _status_translations = (
@@ -30,26 +36,17 @@ class Git(Vcs):
 
     def _head_ref(self):
         """Returns HEAD reference"""
-        return self._run(['symbolic-ref', self.HEAD]).rstrip('\n') or None
+        return self._run(['symbolic-ref', self.HEAD]) or None
 
     def _remote_ref(self, ref):
         """Returns remote reference associated to given ref"""
         if ref is None:
             return None
-        return self._run(['for-each-ref', '--format=%(upstream)', ref]).rstrip('\n') or None
+        return self._run(['for-each-ref', '--format=%(upstream)', ref]) or None
 
     def _log(self, refspec=None, maxres=None, filelist=None):
         """Returns an array of dicts containing revision info for refspec"""
-        args = [
-            '--no-pager', 'log',
-            '--pretty={'
-            '%x00short%x00:%x00%h%x00,'
-            '%x00revid%x00:%x00%H%x00,'
-            '%x00author%x00:%x00%an <%ae>%x00,'
-            '%x00date%x00:%ct,'
-            '%x00summary%x00:%x00%s%x00'
-            '}'
-        ]
+        args = ['--no-pager', 'log', '--pretty=%h%x00%H%x00%an <%ae>%x00%ct%x00%s%x00%x00']
         if refspec:
             args += ['-1', refspec]
         elif maxres:
@@ -58,18 +55,22 @@ class Git(Vcs):
             args += ['--'] + filelist
 
         try:
-            output = self._run(args).rstrip('\n')
+            output = self._run(args)
         except VcsError:
             return None
         if not output:
             return None
 
         log = []
-        for line in output\
-                .replace('\\', '\\\\').replace('"', '\\"').replace('\x00', '"').split('\n'):
-            line = json.loads(line)
-            line['date'] = datetime.fromtimestamp(line['date'])
-            log.append(line)
+        for line in output[:-2].split('\0\0\n'):
+            commit_hash_abbrev, commit_hash, author, timestamp, subject = line.split('\0')
+            log.append({
+                'short': commit_hash_abbrev,
+                'revid': commit_hash,
+                'author': string_control_replace(author, ' '),
+                'date': datetime.fromtimestamp(int(timestamp)),
+                'summary': string_control_replace(subject, ' '),
+            })
         return log
 
     def _status_translate(self, code):
@@ -100,10 +101,10 @@ class Git(Vcs):
 
         # Paths with status
         skip = False
-        output = self._run(['status', '--porcelain', '-z']).rstrip('\x00')
-        if not output:
+        lines = self._run(['status', '--porcelain', '-z']).split('\0')[:-1]
+        if not lines:
             return 'sync'
-        for line in output.split('\x00'):
+        for line in lines:
             if skip:
                 skip = False
                 continue
@@ -114,39 +115,37 @@ class Git(Vcs):
         for status in self.DIRSTATUSES:
             if status in statuses:
                 return status
+
         return 'sync'
 
     def data_status_subpaths(self):
         statuses = {}
 
         # Ignored directories
-        output = self._run([
+        paths = self._run([
             'ls-files', '-z', '--others', '--directory', '--ignored', '--exclude-standard'
-        ]).rstrip('\x00')
-        if output:
-            for path in output.split('\x00'):
-                if path.endswith('/'):
-                    statuses[os.path.normpath(path)] = 'ignored'
+        ]).split('\0')[:-1]
+        for path in paths:
+            if path.endswith('/'):
+                statuses[os.path.normpath(path)] = 'ignored'
 
         # Empty directories
-        output = self._run(
-            ['ls-files', '-z', '--others', '--directory', '--exclude-standard']).rstrip('\x00')
-        if output:
-            for path in output.split('\x00'):
-                if path.endswith('/'):
-                    statuses[os.path.normpath(path)] = 'none'
+        paths = self._run(
+            ['ls-files', '-z', '--others', '--directory', '--exclude-standard']).split('\0')[:-1]
+        for path in paths:
+            if path.endswith('/'):
+                statuses[os.path.normpath(path)] = 'none'
 
         # Paths with status
-        output = self._run(['status', '--porcelain', '-z', '--ignored']).rstrip('\x00')
-        if output:
-            skip = False
-            for line in output.split('\x00'):
-                if skip:
-                    skip = False
-                    continue
-                statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2])
-                if line.startswith('R'):
-                    skip = True
+        lines = self._run(['status', '--porcelain', '-z', '--ignored']).split('\0')[:-1]
+        skip = False
+        for line in lines:
+            if skip:
+                skip = False
+                continue
+            statuses[os.path.normpath(line[3:])] = self._status_translate(line[:2])
+            if line.startswith('R'):
+                skip = True
 
         return statuses
 
diff --git a/ranger/ext/vcs/hg.py b/ranger/ext/vcs/hg.py
index cf15e35e..21460975 100644
--- a/ranger/ext/vcs/hg.py
+++ b/ranger/ext/vcs/hg.py
@@ -36,7 +36,7 @@ class Hg(Vcs):
             args += ['--'] + filelist
 
         try:
-            output = self._run(args).rstrip('\n')
+            output = self._run(args)
         except VcsError:
             return None
         if not output:
@@ -56,13 +56,13 @@ class Hg(Vcs):
     def _remote_url(self):
         """Remote url"""
         try:
-            return self._run(['showconfig', 'paths.default']).rstrip('\n') or None
+            return self._run(['showconfig', 'paths.default']) or None
         except VcsError:
             return None
 
     def _status_translate(self, code):
         """Translate status code"""
-        for code_x, status in self._status_translations:  # pylint: disable=invalid-name
+        for code_x, status in self._status_translations:
             if code in code_x:
                 return status
         return 'unknown'
@@ -80,7 +80,7 @@ class Hg(Vcs):
         if filelist:
             args += filelist
         else:
-            args += self.rootvcs.status_subpaths.keys()
+            args += self.rootvcs.status_subpaths.keys()  # pylint: disable=no-member
         self._run(args, catchout=False)
 
     # Data interface
@@ -117,7 +117,7 @@ class Hg(Vcs):
         return 'unknown'
 
     def data_branch(self):
-        return self._run(['branch']).rstrip('\n') or None
+        return self._run(['branch']) or None
 
     def data_info(self, rev=None):
         if rev is None:
diff --git a/ranger/ext/vcs/svn.py b/ranger/ext/vcs/svn.py
index 1813f857..1f09cd52 100644
--- a/ranger/ext/vcs/svn.py
+++ b/ranger/ext/vcs/svn.py
@@ -36,7 +36,7 @@ class SVN(Vcs):
             args += ['--'] + filelist
 
         try:
-            output = self._run(args).rstrip('\n')
+            output = self._run(args)
         except VcsError:
             return None
         if not output:
@@ -66,7 +66,7 @@ class SVN(Vcs):
     def _remote_url(self):
         """Remote url"""
         try:
-            output = self._run(['info', '--xml']).rstrip('\n')
+            output = self._run(['info', '--xml'])
         except VcsError:
             return None
         if not output:
@@ -86,7 +86,7 @@ class SVN(Vcs):
         if filelist:
             args += filelist
         else:
-            args += self.rootvcs.status_subpaths.keys()
+            args += self.rootvcs.status_subpaths.keys()  # pylint: disable=no-member
         self._run(args, catchout=False)
 
     # Data Interface
@@ -95,10 +95,10 @@ class SVN(Vcs):
         statuses = set()
 
         # Paths with status
-        output = self._run(['status']).rstrip('\n')
-        if not output:
+        lines = self._run(['status']).split('\n')
+        if not lines:
             return 'sync'
-        for line in output.split('\n'):
+        for line in lines:
             code = line[0]
             if code == ' ':
                 continue
@@ -113,13 +113,12 @@ class SVN(Vcs):
         statuses = {}
 
         # Paths with status
-        output = self._run(['status']).rstrip('\n')
-        if output:
-            for line in output.split('\n'):
-                code, path = line[0], line[8:]
-                if code == ' ':
-                    continue
-                statuses[os.path.normpath(path)] = self._status_translate(code)
+        lines = self._run(['status']).split('\n')
+        for line in lines:
+            code, path = line[0], line[8:]
+            if code == ' ':
+                continue
+            statuses[os.path.normpath(path)] = self._status_translate(code)
 
         return statuses
 
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index 487c9405..9c7be653 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -109,7 +109,8 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
 
     # Generic
 
-    def _run(self, args, path=None, catchout=True, retbytes=False):
+    def _run(self, args, path=None,  # pylint: disable=too-many-arguments
+             catchout=True, retbytes=False, rstrip_newline=True):
         """Run a command"""
         cmd = [self.repotype] + args
         if path is None:
@@ -119,7 +120,13 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
             try:
                 if catchout:
                     output = subprocess.check_output(cmd, cwd=path, stderr=devnull)
-                    return output if retbytes else output.decode('UTF-8')
+                    if retbytes:
+                        return output
+                    else:
+                        output = output.decode('UTF-8')
+                        if rstrip_newline and output.endswith('\n'):
+                            return output[:-1]
+                        return output
                 else:
                     subprocess.check_call(cmd, cwd=path, stdout=devnull, stderr=devnull)
             except (subprocess.CalledProcessError, FileNotFoundError):
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index 7b5aa954..62eb5300 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -211,6 +211,7 @@ class DisplayableContainer(Displayable):
     New methods:
 
     add_child(object) -- add the object to the container.
+    replace_child(old_obj, new_obj) -- replaces old object with new object.
     remove_child(object) -- remove the object from the container.
 
     New attributes:
@@ -290,6 +291,11 @@ class DisplayableContainer(Displayable):
         self.container.append(obj)
         obj.parent = self
 
+    def replace_child(self, old_obj, new_obj):
+        """Replace the old object with the new instance in the container."""
+        self.container[self.container.index(old_obj)] = new_obj
+        new_obj.parent = self
+
     def remove_child(self, obj):
         """Remove the object from the container."""
         try:
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index 4c302e00..f10bc0f2 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -437,18 +437,18 @@ class UI(DisplayableContainer):
         if value in self.ALLOWED_VIEWMODES:
             if self._viewmode != value:
                 self._viewmode = value
-                resize = False
+                new_browser = self._viewmode_to_class(value)(self.win)
+
                 if hasattr(self, 'browser'):
                     old_size = self.browser.y, self.browser.x, self.browser.hei, self.browser.wid
-                    self.remove_child(self.browser)
+                    self.replace_child(self.browser, new_browser)
                     self.browser.destroy()
-                    resize = True
+                    new_browser.resize(*old_size)
+                else:
+                    self.add_child(new_browser)
 
-                self.browser = self._viewmode_to_class(value)(self.win)
+                self.browser = new_browser
                 self.redraw_window()
-                self.add_child(self.browser)
-                if resize:
-                    self.browser.resize(*old_size)
         else:
             raise ValueError("Attempting to set invalid viewmode `%s`, should "
                     "be one of `%s`." % (value, "`, `".join(self.ALLOWED_VIEWMODES)))
diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py
index 4eb06692..0828c372 100644
--- a/ranger/gui/widgets/statusbar.py
+++ b/ranger/gui/widgets/statusbar.py
@@ -258,7 +258,7 @@ class StatusBar(Widget):
             right.add("', ", "space")
 
         if target.marked_items:
-            if len(target.marked_items) == len(target.files):
+            if len(target.marked_items) == target.size:
                 right.add(human_readable(target.disk_usage, separator=''))
             else:
                 sumsize = sum(f.size for f in target.marked_items if not
diff --git a/ranger/gui/widgets/view_miller.py b/ranger/gui/widgets/view_miller.py
index 90046456..42013fe9 100644
--- a/ranger/gui/widgets/view_miller.py
+++ b/ranger/gui/widgets/view_miller.py
@@ -256,9 +256,8 @@ class ViewMiller(ViewBase):
 
         # Show the preview column when it has a preview but has
         # been hidden (e.g. because of padding_right = False)
-        if not self.pager.visible and not self.columns[-1].visible and \
-        self.columns[-1].target and self.columns[-1].target.is_directory \
-        or self.columns[-1].has_preview() and not self.pager.visible:
+        if not self.columns[-1].visible and self.columns[-1].has_preview() \
+        and not self.pager.visible:
             self.columns[-1].visible = True
 
         if self.preview and self.is_collapsed != self._collapse():