summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--examples/bash_automatic_cd.sh2
-rw-r--r--ranger/__init__.py37
-rw-r--r--ranger/api/commands.py5
-rw-r--r--ranger/container/fsobject.py4
-rw-r--r--ranger/core/actions.py16
-rw-r--r--ranger/core/fm.py19
-rw-r--r--ranger/core/linemode.py4
-rw-r--r--ranger/core/main.py46
-rw-r--r--ranger/ext/logutils.py78
-rw-r--r--ranger/ext/spawn.py83
-rw-r--r--ranger/ext/vcs/vcs.py47
-rw-r--r--ranger/gui/widgets/view_multipane.py6
12 files changed, 225 insertions, 122 deletions
diff --git a/examples/bash_automatic_cd.sh b/examples/bash_automatic_cd.sh
index 040bf21a..bdac5757 100644
--- a/examples/bash_automatic_cd.sh
+++ b/examples/bash_automatic_cd.sh
@@ -6,6 +6,8 @@
 # the last visited one after ranger quits.
 # To undo the effect of this function, you can type "cd -" to return to the
 # original directory.
+# 
+# On OS X 10 or later, replace `usr/bin/ranger` with `/usr/local/bin/ranger`.
 
 function ranger-cd {
     tempfile="$(mktemp -t tmp.XXXXXX)"
diff --git a/ranger/__init__.py b/ranger/__init__.py
index 1ef62b87..c2aa804e 100644
--- a/ranger/__init__.py
+++ b/ranger/__init__.py
@@ -27,46 +27,11 @@ MACRO_DELIMITER = '%'
 DEFAULT_PAGER = 'less'
 CACHEDIR = os.path.expanduser("~/.cache/ranger")
 USAGE = '%prog [options] [path]'
-VERSION = 'ranger-stable %s\n\nPython %s' % (__version__, sys.version)
+VERSION = 'ranger-master %s\n\nPython %s' % (__version__, sys.version)
 
-try:
-    ExceptionClass = FileNotFoundError
-except NameError:
-    ExceptionClass = IOError
-try:
-    LOGFILE = tempfile.gettempdir() + '/ranger_errorlog'
-except ExceptionClass:
-    LOGFILE = '/dev/null'
-del ExceptionClass
 
 # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored
 # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead.
 CONFDIR = '~/.config/ranger'
 
-# Debugging functions.  These will be activated when run with --debug.
-# Example usage in the code:
-# import ranger; ranger.log("hello world")
-
-
-def log(*objects, **keywords):
-    """Writes objects to a logfile (for the purpose of debugging only.)
-    Has the same arguments as print() in python3.
-    """
-    from ranger import arg
-    if LOGFILE is None or not arg.debug or arg.clean:
-        return
-    start = keywords.get('start', 'ranger:')
-    sep   = keywords.get('sep', ' ')
-    end   = keywords.get('end', '\n')
-    _file = keywords['file'] if 'file' in keywords else open(LOGFILE, 'a')
-    _file.write(sep.join(map(str, (start, ) + objects)) + end)
-
-
-def log_traceback():
-    from ranger import arg
-    if LOGFILE is None or not arg.debug or arg.clean:
-        return
-    import traceback
-    traceback.print_stack(file=open(LOGFILE, 'a'))
-
 from ranger.core.main import main
diff --git a/ranger/api/commands.py b/ranger/api/commands.py
index 9b8ec7d5..93c50adb 100644
--- a/ranger/api/commands.py
+++ b/ranger/api/commands.py
@@ -236,6 +236,11 @@ class Command(FileManagerAware):
                     break
         return flags, rest
 
+    @lazy_property
+    def log(self):
+        import logging
+        return logging.getLogger('ranger.commands.' + self.__class__.__name__)
+
     # XXX: Lazy properties? Not so smart? self.line can change after all!
     @lazy_property
     def _tabinsert_left(self):
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index d73fa76c..9b312e6d 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -20,7 +20,7 @@ from pwd import getpwuid
 from ranger.core.linemode import *
 from ranger.core.shared import FileManagerAware, SettingsAware
 from ranger.ext.shell_escape import shell_escape
-from ranger.ext.spawn import spawn
+from ranger.ext import spawn
 from ranger.ext.lazy_property import lazy_property
 from ranger.ext.human_readable import human_readable
 
@@ -131,7 +131,7 @@ class FileSystemObject(FileManagerAware, SettingsAware):
     @lazy_property
     def filetype(self):
         try:
-            return spawn(["file", '-Lb', '--mime-type', self.path])
+            return spawn.check_output(["file", '-Lb', '--mime-type', self.path])
         except OSError:
             return ""
 
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index c119c501..1b35e57b 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -13,6 +13,7 @@ from inspect import cleandoc
 from stat import S_IEXEC
 from hashlib import sha1
 from sys import version_info
+from logging import getLogger
 
 import ranger
 from ranger.ext.direction import Direction
@@ -31,6 +32,8 @@ from ranger.core.linemode import DEFAULT_LINEMODE
 
 MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
 
+log = getLogger(__name__)
+
 
 class _MacroTemplate(string.Template):
     """A template for substituting macros in commands"""
@@ -152,7 +155,7 @@ class Actions(FileManagerAware, SettingsAware):
         elif bad is True and ranger.arg.debug:
             raise Exception(str(text))
         text = str(text)
-        self.log.appendleft(text)
+        log.debug("Command notify invoked: [Bad: {0}, Text: '{1}']".format(bad, text))
         if self.ui and self.ui.is_on:
             self.ui.status.notify("  ".join(text.split("\n")),
                     duration=duration, bad=bad)
@@ -344,9 +347,10 @@ class Actions(FileManagerAware, SettingsAware):
         Load a config file.
         """
         filename = os.path.expanduser(filename)
+        log.debug("Sourcing config file '{0}'".format(filename))
         with open(filename, 'r') as f:
             for line in f:
-                line = line.lstrip().rstrip("\r\n")
+                line = line.strip(" \r\n")
                 if line.startswith("#") or not line.strip():
                     continue
                 try:
@@ -419,7 +423,7 @@ class Actions(FileManagerAware, SettingsAware):
         Example:
         self.move(down=4, pages=True)  # moves down by 4 pages.
         self.move(to=2, pages=True)  # moves to page 2.
-        self.move(to=1, percentage=True)  # moves to 80%
+        self.move(to=80, percentage=True)  # moves to 80%
         """
         cwd = self.thisdir
         direction = Direction(kw)
@@ -869,11 +873,13 @@ class Actions(FileManagerAware, SettingsAware):
             self.notify("Could not find manpage.", bad=True)
 
     def display_log(self):
+        logs = list(self.get_log())
         pager = self.ui.open_pager()
-        if self.log:
-            pager.set_source(["Message Log:"] + list(self.log))
+        if logs:
+            pager.set_source(["Message Log:"] + logs)
         else:
             pager.set_source(["Message Log:", "No messages!"])
+        pager.move(to=100, percentage=True)
 
     def display_file(self):
         if not self.thisfile or not self.thisfile.is_file:
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index a08de2e1..5d630464 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -5,6 +5,7 @@
 
 from time import time
 from collections import deque
+import logging
 import mimetypes
 import os.path
 import pwd
@@ -25,8 +26,10 @@ 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 import __version__
 from ranger.core.loader import Loader
+from ranger.ext import logutils
+
+log = logging.getLogger(__name__)
 
 
 class FM(Actions, SignalDispatcher):
@@ -50,7 +53,6 @@ class FM(Actions, SignalDispatcher):
             self.ui = ui
         self.start_paths = paths
         self.directories = dict()
-        self.log = deque(maxlen=1000)
         self.bookmarks = bookmarks
         self.current_tab = 1
         self.tabs = {}
@@ -71,10 +73,6 @@ class FM(Actions, SignalDispatcher):
         self.hostname = socket.gethostname()
         self.home_path = os.path.expanduser('~')
 
-        self.log.appendleft('ranger {0} started! Process ID is {1}.'
-                .format(__version__, os.getpid()))
-        self.log.appendleft('Running on Python ' + sys.version.replace('\n', ''))
-
         mimetypes.knownfiles.append(os.path.expanduser('~/.mime.types'))
         mimetypes.knownfiles.append(self.relpath('data/mime.types'))
         self.mimetypes = mimetypes.MimeTypes()
@@ -205,6 +203,15 @@ class FM(Actions, SignalDispatcher):
                 if debug:
                     raise
 
+    def get_log(self):
+        """Return the current log
+
+        The log is returned as a list of string
+        """
+        for log in logutils.log_queue:
+            for line in log.split('\n'):
+                yield line
+
     def _get_image_displayer(self):
         if self.settings.preview_images_method == "w3m":
             return W3MImageDisplayer()
diff --git a/ranger/core/linemode.py b/ranger/core/linemode.py
index a8ce4e6d..c7839723 100644
--- a/ranger/core/linemode.py
+++ b/ranger/core/linemode.py
@@ -7,7 +7,7 @@ import sys
 from abc import *
 from datetime import datetime
 from ranger.ext.human_readable import human_readable
-from ranger.ext.spawn import spawn
+from ranger.ext import spawn
 
 DEFAULT_LINEMODE = "filename"
 
@@ -100,7 +100,7 @@ class FileInfoLinemode(LinemodeBase):
         if not file.is_directory:
             from subprocess import Popen, PIPE, CalledProcessError
             try:
-                fileinfo = spawn(["file", "-bL", file.path]).strip()
+                fileinfo = spawn.check_output(["file", "-bL", file.path]).strip()
             except CalledProcessError:
                 return "unknown"
             if sys.version_info[0] >= 3:
diff --git a/ranger/core/main.py b/ranger/core/main.py
index 93c71e81..b0e3b600 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -6,6 +6,10 @@
 import os.path
 import sys
 import tempfile
+from ranger import __version__
+from logging import getLogger
+
+log = getLogger(__name__)
 
 
 def main():
@@ -15,6 +19,14 @@ def main():
     from ranger.container.settings import Settings
     from ranger.core.shared import FileManagerAware, SettingsAware
     from ranger.core.fm import FM
+    from ranger.ext.logutils import setup_logging
+
+    ranger.arg = arg = parse_arguments()
+    setup_logging(debug=arg.debug, logfile=arg.logfile)
+
+    log.info("Ranger version {0}".format(__version__))
+    log.info('Running on Python ' + sys.version.replace('\n', ''))
+    log.info("Process ID is {0}".format(os.getpid()))
 
     try:
         locale.setlocale(locale.LC_ALL, '')
@@ -31,7 +43,9 @@ def main():
     if 'SHELL' not in os.environ:
         os.environ['SHELL'] = 'sh'
 
-    ranger.arg = arg = parse_arguments()
+    log.debug("config dir: '{0}'".format(arg.confdir))
+    log.debug("cache dir: '{0}'".format(arg.cachedir))
+
     if arg.copy_config is not None:
         fm = FM()
         fm.copy_config_files(arg.copy_config)
@@ -73,8 +87,6 @@ def main():
                    "deprecated.\nPlease use the standalone file launcher "
                    "'rifle' instead.\n")
 
-            def print_function(string):
-                print(string)
             from ranger.ext.rifle import Rifle
             fm = FM()
             if not arg.clean and os.path.isfile(fm.confpath('rifle.conf')):
@@ -112,7 +124,7 @@ def main():
         if fm.username == 'root':
             fm.settings.preview_files = False
             fm.settings.use_preview_script = False
-            fm.log.appendleft("Running as root, disabling the file previews.")
+            log.info("Running as root, disabling the file previews.")
         if not arg.debug:
             from ranger.ext import curses_interrupt_handler
             curses_interrupt_handler.install_interrupt_handler()
@@ -199,6 +211,8 @@ def parse_arguments():
             help="activate debug mode")
     parser.add_option('-c', '--clean', action='store_true',
             help="don't touch/require any config files. ")
+    parser.add_option('--logfile', type='string', metavar='file',
+            help="log file to use, '-' for stderr")
     parser.add_option('-r', '--confdir', type='string',
             metavar='dir', default=default_confdir,
             help="change the configuration directory. (%default)")
@@ -265,14 +279,18 @@ def load_settings(fm, clean):
         allow_access_to_confdir(ranger.arg.confdir, True)
 
         # Load custom commands
-        if os.path.exists(fm.confpath('commands.py')):
+        custom_comm_path = fm.confpath('commands.py')
+        if os.path.exists(custom_comm_path):
             old_bytecode_setting = sys.dont_write_bytecode
             sys.dont_write_bytecode = True
             try:
                 import commands
                 fm.commands.load_commands_from_module(commands)
-            except ImportError:
-                pass
+            except ImportError as e:
+                log.debug("Failed to import custom commands from '{0}'".format(custom_comm_path))
+                log.exception(e)
+            else:
+                log.debug("Loaded custom commands from '{0}'".format(custom_comm_path))
             sys.dont_write_bytecode = old_bytecode_setting
 
         allow_access_to_confdir(ranger.arg.confdir, False)
@@ -297,6 +315,7 @@ def load_settings(fm, clean):
             pass
         else:
             if not os.path.exists(fm.confpath('plugins', '__init__.py')):
+                log.debug("Creating missing '__init__.py' file in plugin folder")
                 f = open(fm.confpath('plugins', '__init__.py'), 'w')
                 f.close()
 
@@ -313,11 +332,12 @@ def load_settings(fm, clean):
                     else:
                         module = importlib.import_module('plugins.' + plugin)
                         fm.commands.load_commands_from_module(module)
-                    fm.log.appendleft("Loaded plugin '%s'." % plugin)
-                except Exception:
-                    import traceback
-                    fm.log.extendleft(reversed(traceback.format_exc().splitlines()))
-                    fm.notify("Error in plugin '%s'" % plugin, bad=True)
+                    log.debug("Loaded plugin '{0}'".format(plugin))
+                except Exception as e:
+                    mex = "Error while loading plugin '{0}'".format(plugin)
+                    log.error(mex)
+                    log.exception(e)
+                    fm.notify(mex, bad=True)
             ranger.fm = None
 
         # COMPAT: Load the outdated options.py
@@ -360,6 +380,8 @@ def allow_access_to_confdir(confdir, allow):
                 print("To run ranger without the need for configuration")
                 print("files, use the --clean option.")
                 raise SystemExit()
+        else:
+            log.debug("Created config directory '{0}'".format(confdir))
         if confdir not in sys.path:
             sys.path[0:0] = [confdir]
     else:
diff --git a/ranger/ext/logutils.py b/ranger/ext/logutils.py
new file mode 100644
index 00000000..0de6c333
--- /dev/null
+++ b/ranger/ext/logutils.py
@@ -0,0 +1,78 @@
+import logging
+from collections import deque
+
+LOG_FORMAT = "[%(levelname)s] %(message)s"
+LOG_FORMAT_EXT = "%(asctime)s,%(msecs)d [%(name)s] |%(levelname)s| %(message)s"
+LOG_DATA_FORMAT = "%H:%M:%S"
+
+
+class QueueHandler(logging.Handler):
+    """
+    This handler store logs events into a queue.
+    """
+
+    def __init__(self, queue):
+        """
+        Initialize an instance, using the passed queue.
+        """
+        logging.Handler.__init__(self)
+        self.queue = queue
+
+    def enqueue(self, record):
+        """
+        Enqueue a log record.
+        """
+        self.queue.append(record)
+
+    def emit(self, record):
+        self.enqueue(self.format(record))
+
+
+log_queue = deque(maxlen=1000)
+concise_formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=LOG_DATA_FORMAT)
+extended_formatter = logging.Formatter(fmt=LOG_FORMAT_EXT, datefmt=LOG_DATA_FORMAT)
+
+
+def setup_logging(debug=True, logfile=None):
+    """
+    All the produced logs using the standard logging function
+    will be collected in a queue by the `queue_handler` as well
+    as outputted on the standard error `stderr_handler`.
+
+    The verbosity and the format of the log message is
+    controlled by the `debug` parameter
+
+     - debug=False:
+            a concise log format will be used, debug messsages will be discarded
+            and only important message will be passed to the `stderr_handler`
+
+     - debug=True:
+            an extended log format will be used, all messages will be processed
+            by both the handlers
+    """
+    root_logger = logging.getLogger()
+
+    if debug:
+        # print all logging in extended format
+        log_level = logging.DEBUG
+        formatter = extended_formatter
+    else:
+        # print only warning and critical message
+        # in a concise format
+        log_level = logging.INFO
+        formatter = concise_formatter
+
+    handlers = []
+    handlers.append(QueueHandler(log_queue))
+    if logfile:
+        if logfile is '-':
+            handlers.append(logging.StreamHandler())
+        else:
+            handlers.append(logging.FileHandler(logfile))
+
+    for handler in handlers:
+        handler.setLevel(log_level)
+        handler.setFormatter(formatter)
+        root_logger.addHandler(handler)
+
+    root_logger.setLevel(0)
diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py
index 393d48d9..04abfbd2 100644
--- a/ranger/ext/spawn.py
+++ b/ranger/ext/spawn.py
@@ -1,11 +1,50 @@
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
 
+from os import devnull
 from subprocess import Popen, PIPE, CalledProcessError
 ENCODING = 'utf-8'
 
 
-def spawn(*args, **kwargs):
+def check_output(popenargs, **kwargs):
+    """Runs a program, waits for its termination and returns its output
+
+    This function is functionally identical to python 2.7's subprocess.check_output,
+    but is favored due to python 2.6 compatibility.
+
+    Will be run through a shell if `popenargs` is a string, otherwise the command
+    is executed directly.
+
+    The keyword argument `decode` determines if the output shall be decoded
+    with the encoding UTF-8.
+
+    Further keyword arguments are passed to Popen.
+    """
+
+    do_decode = kwargs.pop('decode', True)
+    kwargs.setdefault('stdout', PIPE)
+    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)
+            stdout, _ = process.communicate()
+
+    if process.returncode != 0:
+        error = CalledProcessError(process.returncode, popenargs)
+        error.output = stdout
+        raise error
+
+    if do_decode and stdout is not None:
+        stdout = stdout.decode(ENCODING)
+
+    return stdout
+
+
+def spawn(*popenargs, **kwargs):
     """Runs a program, waits for its termination and returns its stdout
 
     This function is similar to python 2.7's subprocess.check_output,
@@ -17,41 +56,15 @@ def spawn(*args, **kwargs):
         spawn(command, arg1, arg2...)
         spawn([command, arg1, arg2])
 
-    The string will be run through a shell, otherwise the command is executed
-    directly.
+    Will be run through a shell if `popenargs` is a string, otherwise the command
+    is executed directly.
 
-    The keyword argument "decode" determines if the output shall be decoded
-    with the encoding '%s'.
+    The keyword argument `decode` determines if the output shall be decoded
+    with the encoding UTF-8.
 
     Further keyword arguments are passed to Popen.
-    """ % (ENCODING, )
-
-    if len(args) == 1:
-        popen_arguments = args[0]
-        shell = isinstance(popen_arguments, str)
-    else:
-        popen_arguments = args
-        shell = False
-
-    if 'decode' in kwargs:
-        do_decode = kwargs['decode']
-        del kwargs['decode']
-    else:
-        do_decode = True
-    if 'stdout' not in kwargs:
-        kwargs['stdout'] = PIPE
-    if 'shell' not in kwargs:
-        kwargs['shell'] = shell
-
-    process = Popen(popen_arguments, **kwargs)
-    stdout, stderr = process.communicate()
-    return_value = process.poll()
-    if return_value:
-        error = CalledProcessError(return_value, popen_arguments[0])
-        error.output = stdout
-        raise error
-
-    if do_decode:
-        return stdout.decode(ENCODING)
+    """
+    if len(popenargs) == 1:
+        return check_output(popenargs[0], **kwargs)
     else:
-        return stdout
+        return check_output(list(popenargs), **kwargs)
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index cfdc1e3b..fa00777a 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -7,7 +7,9 @@ import os
 import subprocess
 import threading
 import time
-from ranger.ext.spawn import spawn
+from logging import getLogger
+
+from ranger.ext import spawn
 
 # Python2 compatibility
 try:
@@ -20,6 +22,9 @@ except NameError:
     FileNotFoundError = OSError  # pylint: disable=redefined-builtin
 
 
+LOG = getLogger(__name__)
+
+
 class VcsError(Exception):
     """VCS exception"""
     pass
@@ -117,20 +122,17 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
         if path is None:
             path = self.path
 
-        with open(os.devnull, 'w') as devnull:
-            try:
-                if catchout:
-                    output = spawn(cmd, cwd=path, stderr=devnull,
-                            decode=not retbytes)
-                    if (not retbytes and rstrip_newline and
-                            output.endswith('\n')):
-                        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):
-                raise VcsError('{0:s}: {1:s}'.format(str(cmd), path))
+        try:
+            if catchout:
+                output = spawn.check_output(cmd, cwd=path, decode=not retbytes)
+                if not retbytes and rstrip_newline and output.endswith('\n'):
+                    return output[:-1]
+                return output
+            else:
+                with open(os.devnull, mode='w') as fd_devnull:
+                    subprocess.check_call(cmd, cwd=path, stdout=fd_devnull, stderr=fd_devnull)
+        except (subprocess.CalledProcessError, FileNotFoundError):
+            raise VcsError('{0:s}: {1:s}'.format(str(cmd), path))
 
     def _get_repotype(self, path):
         """Get type for path"""
@@ -234,7 +236,9 @@ class VcsRoot(Vcs):  # pylint: disable=abstract-method
             self.branch = self.data_branch()
             self.obj.vcsremotestatus = self.data_status_remote()
             self.obj.vcsstatus = self.data_status_root()
-        except VcsError:
+        except VcsError as error:
+            LOG.exception(error)
+            self.obj.fm.notify('VCS Exception: View log for more info', bad=True)
             return False
         self.rootinit = True
         return True
@@ -247,7 +251,9 @@ class VcsRoot(Vcs):  # pylint: disable=abstract-method
             self.status_subpaths = self.data_status_subpaths()
             self.obj.vcsremotestatus = self.data_status_remote()
             self.obj.vcsstatus = self._status_root()
-        except VcsError:
+        except VcsError as error:
+            LOG.exception(error)
+            self.obj.fm.notify('VCS Exception: View log for more info', bad=True)
             return False
         self.rootinit = True
         self.updatetime = time.time()
@@ -470,10 +476,9 @@ class VcsThread(threading.Thread):  # pylint: disable=too-many-instance-attribut
                             column.need_redraw = True
                     self.ui.status.need_redraw = True
                     self.ui.redraw()
-            except Exception:  # pylint: disable=broad-except
-                import traceback
-                self.ui.fm.log.extendleft(reversed(traceback.format_exc().splitlines()))
-                self.ui.fm.notify('VCS Exception', bad=True)
+            except Exception as error:  # pylint: disable=broad-except
+                LOG.exception(error)
+                self.ui.fm.notify('VCS Exception: View log for more info', bad=True)
 
     def pause(self):
         """Pause thread"""
diff --git a/ranger/gui/widgets/view_multipane.py b/ranger/gui/widgets/view_multipane.py
index 7b77b3db..f8efd80f 100644
--- a/ranger/gui/widgets/view_multipane.py
+++ b/ranger/gui/widgets/view_multipane.py
@@ -42,11 +42,11 @@ class ViewMultipane(ViewBase):
 
     def resize(self, y, x, hei, wid):
         ViewBase.resize(self, y, x, hei, wid)
-        column_width = int(float(wid) / len(self.columns))
+        column_width = int((float(wid) - len(self.columns) + 1) / len(self.columns))
         left = 0
         top = 0
         for i, column in enumerate(self.columns):
-            column.resize(top, left, hei, max(1, column_width - 1))
-            left += column_width
+            column.resize(top, left, hei, max(1, column_width))
+            left += column_width + 1
             column.need_redraw = True
         self.need_redraw = True