summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--doc/ranger.130
-rw-r--r--doc/ranger.pod26
-rw-r--r--examples/rc_emacs.conf2
-rw-r--r--ranger/api/commands.py40
-rw-r--r--ranger/colorschemes/default.py3
-rw-r--r--ranger/colorschemes/jungle.py3
-rw-r--r--ranger/colorschemes/solarized.py3
-rwxr-xr-xranger/config/commands.py9
-rw-r--r--ranger/config/rc.conf38
-rw-r--r--ranger/config/rifle.conf1
-rw-r--r--ranger/container/fsobject.py10
-rw-r--r--ranger/container/settings.py11
-rw-r--r--ranger/core/actions.py56
-rw-r--r--ranger/core/fm.py8
-rw-r--r--ranger/core/linemode.py21
-rw-r--r--ranger/core/loader.py3
-rwxr-xr-xranger/data/scope.sh11
-rw-r--r--ranger/gui/context.py1
-rw-r--r--ranger/gui/displayable.py7
-rw-r--r--ranger/gui/ui.py61
-rw-r--r--ranger/gui/widgets/browsercolumn.py35
-rw-r--r--ranger/gui/widgets/console.py1
-rw-r--r--ranger/gui/widgets/pager.py1
-rw-r--r--ranger/gui/widgets/view_base.py159
-rw-r--r--ranger/gui/widgets/view_miller.py (renamed from ranger/gui/widgets/browserview.py)134
-rw-r--r--ranger/gui/widgets/view_multipane.py51
-rw-r--r--tests/ranger/container/test_fsobject.py34
28 files changed, 576 insertions, 184 deletions
diff --git a/.gitignore b/.gitignore
index 81e1b8b5..e97a99b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ stuff/*
 doc/ranger.1.html
 build
 pytestdebug.log
+install_log.txt
diff --git a/doc/ranger.1 b/doc/ranger.1
index ee487e1e..db608eaf 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -133,7 +133,7 @@
 .\" ========================================================================
 .\"
 .IX Title "RANGER 1"
-.TH RANGER 1 "ranger-1.7.2" "02/24/2016" "ranger manual"
+.TH RANGER 1 "ranger-1.7.2" "04/15/2016" "ranger manual"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -332,6 +332,8 @@ The macro \f(CW%rangerdir\fR expands to the directory of ranger's python library
 can use it for something like this command:
   alias show_commands shell less \f(CW%rangerdir\fR/config/commands.py
 .PP
+\&\f(CW%confdir\fR expands to the directory given by \fB\-\-confdir\fR.
+.PP
 The macro \f(CW%space\fR expands to a space character. You can use it to add spaces to
 the end of a command when needed, while preventing editors to strip spaces off
 the end of the line automatically.
@@ -487,10 +489,13 @@ Change the permissions of the selection.  For example, \f(CW\*(C`777=\*(C'\fR is
 \&\f(CW\*(C`chmod 777 %s\*(C'\fR, \f(CW\*(C`+ar\*(C'\fR does \f(CW\*(C`chmod a+r %s\*(C'\fR, \f(CW\*(C`\-ow\*(C'\fR does \f(CW\*(C`chmod o\-w %s\*(C'\fR etc.
 .IP "yy" 14
 .IX Item "yy"
-Copy (yank) the selection, like pressing Ctrl+C in modern \s-1GUI\s0 programs.
+Copy (yank) the selection, like pressing Ctrl+C in modern \s-1GUI\s0 programs.  (You
+can also type \*(L"ya\*(R" to add files to the copy buffer, \*(L"yr\*(R" to remove files again,
+or \*(L"yt\*(R" for toggling.)
 .IP "dd" 14
 .IX Item "dd"
-Cut the selection, like pressing Ctrl+X in modern \s-1GUI\s0 programs.
+Cut the selection, like pressing Ctrl+X in modern \s-1GUI\s0 programs.  (There are
+also \*(L"da\*(R", \*(L"dr\*(R" and \*(L"dt\*(R" shortcuts equivalent to \*(L"ya\*(R", \*(L"yr\*(R" and \*(L"yt\*(R".)
 .IP "pp" 14
 .IX Item "pp"
 Paste the files which were previously copied or cut, like pressing Ctrl+V in
@@ -630,6 +635,12 @@ fly with the command \fB:set option value\fR.  Examples:
 \& set show_hidden true
 .Ve
 .PP
+Toggling options can be done with:
+.PP
+.Vb 1
+\& set show_hidden!
+.Ve
+.PP
 The different types of settings and an example for each type:
 .PP
 .Vb 7
@@ -834,6 +845,9 @@ Sets the state for the version control backend. The possible values are:
 .IX Item "xterm_alt_key [bool]"
 Enable this if key combinations with the Alt Key don't work for you.
 (Especially on xterm)
+.IP "clear_filters_on_dir_change [bool]" 4
+.IX Item "clear_filters_on_dir_change [bool]"
+If set to 'true', persistent filters would be cleared upon leaving the directory
 .SH "COMMANDS"
 .IX Header "COMMANDS"
 You can enter the commands in the console which is opened by pressing \*(L":\*(R".
@@ -889,6 +903,7 @@ ranger.  For your convenience, this is a list of the \*(L"public\*(R" commands i
 \& setintag tags option value
 \& setlocal [path=<path>] option value
 \& shell [\-FLAGS] command
+\& source filename
 \& terminal
 \& tmap key command
 \& touch filename
@@ -1200,6 +1215,15 @@ use \f(CW\*(C`path=~/dl$\*(C'\fR.
 .IP "shell [\-\fIflags\fR] \fIcommand\fR" 2
 .IX Item "shell [-flags] command"
 Run a shell command.  \fIflags\fR are discussed in their own section.
+.IP "source \fIfilename\fR" 2
+.IX Item "source filename"
+Reads commands from a file and executes them in the ranger console.
+.Sp
+This can be used to re-evaluate the rc.conf file after changing it:
+.Sp
+.Vb 1
+\& map X chain shell vim \-p %confdir/rc.conf %rangerdir/config/rc.conf; source %confdir/rc.conf
+.Ve
 .IP "terminal" 2
 .IX Item "terminal"
 Spawns the \fIx\-terminal-emulator\fR starting in the current directory.
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 91ba904a..43b248ce 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -228,6 +228,8 @@ The macro %rangerdir expands to the directory of ranger's python library, you
 can use it for something like this command:
   alias show_commands shell less %rangerdir/config/commands.py
 
+%confdir expands to the directory given by B<--confdir>.
+
 The macro %space expands to a space character. You can use it to add spaces to
 the end of a command when needed, while preventing editors to strip spaces off
 the end of the line automatically.
@@ -404,11 +406,14 @@ C<chmod 777 %s>, C<+ar> does C<chmod a+r %s>, C<-ow> does C<chmod o-w %s> etc.
 
 =item yy
 
-Copy (yank) the selection, like pressing Ctrl+C in modern GUI programs.
+Copy (yank) the selection, like pressing Ctrl+C in modern GUI programs.  (You
+can also type "ya" to add files to the copy buffer, "yr" to remove files again,
+or "yt" for toggling.)
 
 =item dd
 
-Cut the selection, like pressing Ctrl+X in modern GUI programs.
+Cut the selection, like pressing Ctrl+X in modern GUI programs.  (There are
+also "da", "dr" and "dt" shortcuts equivalent to "ya", "yr" and "yt".)
 
 =item pp
 
@@ -601,6 +606,10 @@ fly with the command B<:set option value>.  Examples:
  set column_ratios 1,2,3
  set show_hidden true
 
+Toggling options can be done with:
+
+ set show_hidden!
+
 The different types of settings and an example for each type:
 
  setting type   | example values
@@ -849,6 +858,10 @@ Sets the state for the version control backend. The possible values are:
 Enable this if key combinations with the Alt Key don't work for you.
 (Especially on xterm)
 
+=item clear_filters_on_dir_change [bool]
+
+If set to 'true', persistent filters would be cleared upon leaving the directory
+
 =back
 
 
@@ -906,6 +919,7 @@ ranger.  For your convenience, this is a list of the "public" commands including
  setintag tags option value
  setlocal [path=<path>] option value
  shell [-FLAGS] command
+ source filename
  terminal
  tmap key command
  touch filename
@@ -1256,6 +1270,14 @@ use C<path=~/dl$>.
 
 Run a shell command.  I<flags> are discussed in their own section.
 
+=item source I<filename>
+
+Reads commands from a file and executes them in the ranger console.
+
+This can be used to re-evaluate the rc.conf file after changing it:
+
+ map X chain shell vim -p %confdir/rc.conf %rangerdir/config/rc.conf; source %confdir/rc.conf
+
 =item terminal
 
 Spawns the I<x-terminal-emulator> starting in the current directory.
diff --git a/examples/rc_emacs.conf b/examples/rc_emacs.conf
index f5e66b89..f0752e62 100644
--- a/examples/rc_emacs.conf
+++ b/examples/rc_emacs.conf
@@ -347,7 +347,7 @@ map <C-r> search_next forward=False
 map <C-x>b    tab_move 1
 map <A-Right> tab_move 1
 map <A-Left>  tab_move -1
-map <C-x>f    tab_new ~
+map <C-x><C-f> tab_new ~
 map <C-_>k    tab_restore
 map <a-1>     tab_open 1
 map <a-2>     tab_open 2
diff --git a/ranger/api/commands.py b/ranger/api/commands.py
index a20adecb..c3e0a59a 100644
--- a/ranger/api/commands.py
+++ b/ranger/api/commands.py
@@ -152,6 +152,27 @@ class Command(FileManagerAware):
         return ''.join([self._tabinsert_left, word, self._tabinsert_right])
 
     def parse_setting_line(self):
+        """
+        Parses the command line argument that is passed to the `:set` command.
+        Returns [option, value, name_complete].
+
+        Can parse incomplete lines too, and `name_complete` is a boolean
+        indicating whether the option name looks like it's completed or
+        unfinished.  This is useful for generating tab completions.
+
+        >>> Command("set foo=bar").parse_setting_line()
+        ['foo', 'bar', True]
+        >>> Command("set foo").parse_setting_line()
+        ['foo', '', False]
+        >>> Command("set foo=").parse_setting_line()
+        ['foo', '', True]
+        >>> Command("set foo ").parse_setting_line()
+        ['foo', '', True]
+        >>> Command("set myoption myvalue").parse_setting_line()
+        ['myoption', 'myvalue', True]
+        >>> Command("set").parse_setting_line()
+        ['', '', False]
+        """
         if self._setting_line is not None:
             return self._setting_line
         match = _SETTINGS_RE.match(self.rest(1))
@@ -163,6 +184,25 @@ class Command(FileManagerAware):
         self._setting_line = result
         return result
 
+    def parse_setting_line_v2(self):
+        """
+        Parses the command line argument that is passed to the `:set` command.
+        Returns [option, value, name_complete, toggle].
+
+        >>> Command("set foo=bar").parse_setting_line_v2()
+        ['foo', 'bar', True, False]
+        >>> Command("set foo!").parse_setting_line_v2()
+        ['foo', '', True, True]
+        """
+        option, value, name_complete = self.parse_setting_line()
+        if len(option) >= 2 and option[-1] == '!':
+            toggle = True
+            option = option[:-1]
+            name_complete = True
+        else:
+            toggle = False
+        return [option, value, name_complete, toggle]
+
     def parse_flags(self):
         """Finds and returns flags in the command
 
diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py
index e5dbf3a6..37b372a3 100644
--- a/ranger/colorschemes/default.py
+++ b/ranger/colorschemes/default.py
@@ -67,6 +67,9 @@ class Default(ColorScheme):
                 else:
                     fg = magenta
 
+            if context.inactive_pane:
+                fg = cyan
+
         elif context.in_titlebar:
             attr |= bold
             if context.hostname:
diff --git a/ranger/colorschemes/jungle.py b/ranger/colorschemes/jungle.py
index a4fb1c1d..6a9c3c52 100644
--- a/ranger/colorschemes/jungle.py
+++ b/ranger/colorschemes/jungle.py
@@ -9,7 +9,8 @@ class Scheme(Default):
     def use(self, context):
         fg, bg, attr = Default.use(self, context)
 
-        if context.directory and not context.marked and not context.link:
+        if context.directory and not context.marked and not context.link \
+                and not context.inactive_pane:
             fg = green
 
         if context.in_titlebar and context.hostname:
diff --git a/ranger/colorschemes/solarized.py b/ranger/colorschemes/solarized.py
index 07552ce2..2177ca3d 100644
--- a/ranger/colorschemes/solarized.py
+++ b/ranger/colorschemes/solarized.py
@@ -80,6 +80,9 @@ class Solarized(ColorScheme):
                 else:
                     fg = magenta
 
+            if context.inactive_pane:
+                fg = 241
+
         elif context.in_titlebar:
             attr |= bold
             if context.hostname:
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index c41bfcb6..4d00b4c8 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -336,12 +336,17 @@ class set_(Command):
     """:set <option name>=<python expression>
 
     Gives an option a new value.
+
+    Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!`
     """
     name = 'set'  # don't override the builtin set class
     def execute(self):
         name = self.arg(1)
-        name, value, _ = self.parse_setting_line()
-        self.fm.set_option_from_string(name, value)
+        name, value, _, toggle = self.parse_setting_line_v2()
+        if toggle:
+            self.fm.toggle_option(name)
+        else:
+            self.fm.set_option_from_string(name, value)
 
     def tab(self, tabnum):
         from ranger.gui.colorscheme import get_all_colorschemes
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 43a9b99a..914b1d0f 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -19,6 +19,13 @@
 # == Options
 # ===================================================================
 
+# Which viewmode should be used?  Possible values are:
+#     miller: Use miller columns which show multiple levels of the hierarchy
+#     multipane: Midnight-commander like multipane view showing all tabs next
+#                to each other
+set viewmode miller
+#set viewmode multipane
+
 # How many columns are there, and what are their relative widths?
 set column_ratios 1,3,4
 
@@ -235,6 +242,7 @@ map <C-r> reset
 map <C-l> redraw_window
 map <C-c> abort
 map <esc> change_mode normal
+map ~ set viewmode!
 
 map i display_file
 map ? help
@@ -255,7 +263,9 @@ map cd console cd%space
 # Change the line mode
 map Mf linemode filename
 map Mi linemode fileinfo
+map Mm linemode mtime
 map Mp linemode permissions
+map Ms linemode sizemtime
 map Mt linemode metatitle
 
 # Tagging / Marking
@@ -360,11 +370,13 @@ map dd cut
 map ud uncut
 map da cut mode=add
 map dr cut mode=remove
+map dt cut mode=toggle
 
 map yy copy
 map uy uncut
 map ya copy mode=add
 map yr copy mode=remove
+map yt copy mode=toggle
 
 # Temporary workarounds
 map dgg eval fm.cut(dirarg=dict(to=0), narg=quantifier)
@@ -410,7 +422,7 @@ map <a-8>     tab_open 8
 map <a-9>     tab_open 9
 
 # Sorting
-map or toggle_option sort_reverse
+map or set sort_reverse!
 map oz set sort=random
 map os chain set sort=size;      set sort_reverse=False
 map ob chain set sort=basename;  set sort_reverse=False
@@ -433,18 +445,18 @@ map oE chain set sort=extension; set sort_reverse=True
 map dc get_cumulative_size
 
 # Settings
-map zc    toggle_option collapse_preview
-map zd    toggle_option sort_directories_first
-map zh    toggle_option show_hidden
-map <C-h> toggle_option show_hidden
-map zI    toggle_option flushinput
-map zi    toggle_option preview_images
-map zm    toggle_option mouse_enabled
-map zp    toggle_option preview_files
-map zP    toggle_option preview_directories
-map zs    toggle_option sort_case_insensitive
-map zu    toggle_option autoupdate_cumulative_size
-map zv    toggle_option use_preview_script
+map zc    set collapse_preview!
+map zd    set sort_directories_first!
+map zh    set show_hidden!
+map <C-h> set show_hidden!
+map zI    set flushinput!
+map zi    set preview_images!
+map zm    set mouse_enabled!
+map zp    set preview_files!
+map zP    set preview_directories!
+map zs    set sort_case_insensitive!
+map zu    set autoupdate_cumulative_size!
+map zv    set use_preview_script!
 map zf    console filter%space
 
 # Bookmarks
diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf
index 31db35a4..f95c94ff 100644
--- a/ranger/config/rifle.conf
+++ b/ranger/config/rifle.conf
@@ -168,6 +168,7 @@ ext djvu, has atril,  X, flag f = atril -- "$@"
 mime ^image/svg, has inkscape, X, flag f = inkscape -- "$@"
 mime ^image/svg, has display,  X, flag f = display -- "$@"
 
+mime ^image, has pqiv,      X, flag f = pqiv -- "$@"
 mime ^image, has sxiv,      X, flag f = sxiv -- "$@"
 mime ^image, has feh,       X, flag f = feh -- "$@"
 mime ^image, has mirage,    X, flag f = mirage -- "$@"
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index 1daf6d70..fff29c36 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -30,7 +30,8 @@ else:
     from string import maketrans
 _unsafe_chars = '\n' + ''.join(map(chr, range(32))) + ''.join(map(chr, range(128, 256)))
 _safe_string_table = maketrans(_unsafe_chars, '?' * len(_unsafe_chars))
-_extract_number_re = re.compile(r'(\d+)')
+_extract_number_re = re.compile(r'(\d+|\D)')
+_integers = set("0123456789")
 
 def safe_path(path):
     return path.translate(_safe_string_table)
@@ -79,7 +80,8 @@ class FileSystemObject(FileManagerAware, SettingsAware):
     _linemode = DEFAULT_LINEMODE
     linemode_dict = dict(
         (linemode.name, linemode()) for linemode in
-        [DefaultLinemode, TitleLinemode, PermissionsLinemode, FileInfoLinemode]
+        [DefaultLinemode, TitleLinemode, PermissionsLinemode, FileInfoLinemode,
+         MtimeLinemode, SizeMtimeLinemode]
     )
 
     def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel_to=None):
@@ -134,12 +136,12 @@ class FileSystemObject(FileManagerAware, SettingsAware):
 
     @lazy_property
     def basename_natural(self):
-        return [int(s) if s.isdigit() else s \
+        return [('0', int(s)) if s in _integers else (s, 0) \
                 for s in _extract_number_re.split(self.relative_path)]
 
     @lazy_property
     def basename_natural_lower(self):
-        return [int(s) if s.isdigit() else s \
+        return [('0', int(s)) if s in _integers else (s, 0) \
                 for s in _extract_number_re.split(self.relative_path_lower)]
 
     @lazy_property
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index d7258d6d..14ff9bca 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -56,6 +56,7 @@ ALLOWED_SETTINGS = {
     'update_title': bool,
     'update_tmux_title': bool,
     'use_preview_script': bool,
+    'viewmode': str,
     'vcs_aware': bool,
     'vcs_backend_bzr': str,
     'vcs_backend_git': str,
@@ -65,6 +66,16 @@ ALLOWED_SETTINGS = {
     'clear_filters_on_dir_change': bool
 }
 
+ALLOWED_VALUES = {
+    'confirm_on_delete': ['always', 'multiple', 'never'],
+    'preview_images_method': ['w3m', 'iterm2'],
+    'vcs_backend_bzr': ['enabled', 'local', 'disabled'],
+    'vcs_backend_git': ['enabled', 'local', 'disabled'],
+    'vcs_backend_hg': ['enabled', 'local', 'disabled'],
+    'vcs_backend_svn': ['enabled', 'local', 'disabled'],
+    'viewmode': ['miller', 'multipane'],
+}
+
 DEFAULT_VALUES = {
     bool: False,
     type(None): None,
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index a44e30e1..71cb5929 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -23,9 +23,10 @@ from ranger.ext.next_available_filename import next_available_filename
 from ranger.ext.rifle import squash_flags, ASK_COMMAND
 from ranger.core.shared import FileManagerAware, SettingsAware
 from ranger.core.tab import Tab
+from ranger.container.directory import Directory
 from ranger.container.file import File
 from ranger.core.loader import CommandLoader, CopyLoader
-from ranger.container.settings import ALLOWED_SETTINGS
+from ranger.container.settings import ALLOWED_SETTINGS, ALLOWED_VALUES
 from ranger.core.linemode import DEFAULT_LINEMODE
 
 MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
@@ -245,6 +246,7 @@ class Actions(FileManagerAware, SettingsAware):
         macros = {}
 
         macros['rangerdir'] = ranger.RANGERDIR
+        macros['confdir'] = self.fm.confpath()
         macros['space'] = ' '
 
         if self.fm.thisfile:
@@ -341,18 +343,19 @@ class Actions(FileManagerAware, SettingsAware):
         Load a config file.
         """
         filename = os.path.expanduser(filename)
-        for line in open(filename, 'r'):
-            line = line.lstrip().rstrip("\r\n")
-            if line.startswith("#") or not line.strip():
-                continue
-            try:
-                self.execute_console(line)
-            except Exception as e:
-                if ranger.arg.debug:
-                    raise
-                else:
-                    self.notify('Error in line `%s\':\n  %s' %
-                            (line, str(e)), bad=True)
+        with open(filename, 'r') as f:
+            for line in f:
+                line = line.lstrip().rstrip("\r\n")
+                if line.startswith("#") or not line.strip():
+                    continue
+                try:
+                    self.execute_console(line)
+                except Exception as e:
+                    if ranger.arg.debug:
+                        raise
+                    else:
+                        self.notify('Error in line `%s\':\n  %s' %
+                                (line, str(e)), bad=True)
 
     def execute_file(self, files, **kw):
         """Uses the "rifle" module to open/execute a file
@@ -564,7 +567,7 @@ class Actions(FileManagerAware, SettingsAware):
     def pager_close(self):
         if self.ui.pager.visible:
             self.ui.close_pager()
-        if self.ui.browser.pager.visible:
+        if hasattr(self.ui.browser, 'pager') and self.ui.browser.pager.visible:
             self.ui.close_embedded_pager()
 
     def taskview_open(self):
@@ -593,6 +596,17 @@ class Actions(FileManagerAware, SettingsAware):
         """
         if isinstance(self.settings[string], bool):
             self.settings[string] ^= True
+        elif string in ALLOWED_VALUES:
+            current = self.settings[string]
+            allowed = ALLOWED_VALUES[string]
+            if len(allowed) > 0:
+                if current not in allowed and current == "":
+                    current = allowed[0]
+                if current in allowed:
+                    self.settings[string] = \
+                        allowed[(allowed.index(current) + 1) % len(allowed)]
+                else:
+                    self.settings[string] = allowed[0]
 
     def set_option(self, optname, value):
         """:set_option <optname>
@@ -782,10 +796,14 @@ class Actions(FileManagerAware, SettingsAware):
         except KeyError:
             pass
 
-    def set_bookmark(self, key):
+    def set_bookmark(self, key, val=None):
         """Set the bookmark with the name <key> to the current directory"""
+        if val is None:
+            val = self.thisdir
+        else:
+            val = Directory(val)
         self.bookmarks.update_if_outdated()
-        self.bookmarks[str(key)] = self.thisdir
+        self.bookmarks[str(key)] = val
 
     def unset_bookmark(self, key):
         """Delete the bookmark with the name <key>"""
@@ -1039,6 +1057,7 @@ class Actions(FileManagerAware, SettingsAware):
         if tab_has_changed:
             self.change_mode('normal')
             self.signal_emit('tab.change', old=previous_tab, new=self.thistab)
+            self.signal_emit('tab.layoutchange')
 
     def tab_close(self, name=None):
         if name is None:
@@ -1053,6 +1072,7 @@ class Actions(FileManagerAware, SettingsAware):
         if name in self.tabs:
             del self.tabs[name]
         self.restorable_tabs.append(tab)
+        self.signal_emit('tab.layoutchange')
 
     def tab_restore(self):
         # NOTE: The name of the tab is not restored.
@@ -1222,7 +1242,7 @@ class Actions(FileManagerAware, SettingsAware):
         Copy the selected items.
         Modes are: 'set', 'add', 'remove'.
         """
-        assert mode in ('set', 'add', 'remove')
+        assert mode in ('set', 'add', 'remove', 'toggle')
         cwd = self.thisdir
         if not narg and not dirarg:
             selected = (f for f in self.thistab.get_selection() if f in cwd.files)
@@ -1244,6 +1264,8 @@ class Actions(FileManagerAware, SettingsAware):
             self.copy_buffer.update(set(selected))
         elif mode == 'remove':
             self.copy_buffer.difference_update(set(selected))
+        elif mode == 'toggle':
+            self.copy_buffer.symmetric_difference_update(set(selected))
         self.do_cut = False
         self.ui.browser.main_column.request_redraw()
 
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 046eb788..f6792b95 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -136,7 +136,7 @@ class FM(Actions, SignalDispatcher):
 
             if self.settings.open_all_images and \
                     len(self.thisdir.marked_items) == 0 and \
-                    re.match(r'^(feh|sxiv|imv) ', command):
+                    re.match(r'^(feh|sxiv|imv|pqiv) ', command):
 
                 images = [f.relative_path for f in self.thisdir.files if f.image]
                 escaped_filenames = " ".join(shell_quote(f) \
@@ -161,6 +161,12 @@ class FM(Actions, SignalDispatcher):
                         new_command = command.replace("imv ",
                                 "imv -n %d " % number, 1)
 
+                    if command[0:5] == 'pqiv ':
+                        number = images.index(self.thisfile.relative_path)
+                        new_command = command.replace("pqiv ",
+                                "pqiv --action \"goto_file_byindex(%d)\" " % \
+                                number, 1)
+
                     if new_command:
                         command = "set -- %s; %s" % (escaped_filenames,
                                 new_command)
diff --git a/ranger/core/linemode.py b/ranger/core/linemode.py
index 76e8f7cc..b7aef23f 100644
--- a/ranger/core/linemode.py
+++ b/ranger/core/linemode.py
@@ -5,6 +5,8 @@
 
 import sys
 from abc import *
+from datetime import datetime
+from ranger.ext.human_readable import human_readable
 
 DEFAULT_LINEMODE = "filename"
 
@@ -102,3 +104,22 @@ class FileInfoLinemode(LinemodeBase):
             return fileinfo
         else:
             raise NotImplementedError
+
+class MtimeLinemode(LinemodeBase):
+    name = "mtime"
+
+    def filetitle(self, file, metadata):
+        return file.relative_path
+
+    def infostring(self, file, metadata):
+        return datetime.fromtimestamp(file.stat.st_mtime).strftime("%Y-%m-%d %H:%M")
+
+class SizeMtimeLinemode(LinemodeBase):
+    name = "sizemtime"
+
+    def filetitle(self, file, metadata):
+        return file.relative_path
+
+    def infostring(self, file, metadata):
+        return "%s %s" % (human_readable(file.size),
+                          datetime.fromtimestamp(file.stat.st_mtime).strftime("%Y-%m-%d %H:%M"))
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index 2184d2b1..b1aabb53 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -260,7 +260,8 @@ def safeDecode(string):
         return string.decode("utf-8")
     except (UnicodeDecodeError):
         if HAVE_CHARDET:
-            return string.decode(chardet.detect(string)["encoding"])
+            codec = chardet.detect(string)["encoding"]
+            return string.decode(codec, 'ignore')
         else:
             return ""
 
diff --git a/ranger/data/scope.sh b/ranger/data/scope.sh
index afaf131f..669d1e34 100755
--- a/ranger/data/scope.sh
+++ b/ranger/data/scope.sh
@@ -91,8 +91,15 @@ esac
 case "$mimetype" in
     # Syntax highlight for text files:
     text/* | */xml)
-        try safepipe highlight --out-format=ansi "$path" && { dump | trim; exit 5; }
-        try safepipe pygmentize "$path" && { dump | trim; exit 5; }
+        if [ "$(tput colors)" -ge 256 ]; then
+            pygmentize_format=terminal256
+            highlight_format=xterm256
+        else
+            pygmentize_format=terminal
+            highlight_format=ansi
+        fi
+        try safepipe highlight --out-format=${highlight_format} "$path" && { dump | trim; exit 5; }
+        try safepipe pygmentize -f ${pygmentize_format} "$path" && { dump | trim; exit 5; }
         exit 2;;
     # Ascii-previews of images:
     image/*)
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index ac597e5a..2d23d4f1 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -4,6 +4,7 @@
 CONTEXT_KEYS = ['reset', 'error', 'badinfo',
         'in_browser', 'in_statusbar', 'in_titlebar', 'in_console',
         'in_pager', 'in_taskview',
+        'active_pane', 'inactive_pane',
         'directory', 'file', 'hostname',
         'executable', 'media', 'link', 'fifo', 'socket', 'device',
         'video', 'audio', 'image', 'media', 'document', 'container',
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index d3adfe50..c6e21d54 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -94,10 +94,9 @@ class Displayable(FileManagerAware, CursesShortcuts):
         """
 
     def destroy(self):
-        """Called when the object is destroyed.
-
-        Override this!
-        """
+        """Called when the object is destroyed."""
+        if hasattr(self, 'win'):
+            del self.win
 
     def contains_point(self, y, x):
         """Test whether the point lies inside this object.
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index ad95d754..e5833839 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -11,6 +11,7 @@ from .displayable import DisplayableContainer
 from .mouse_event import MouseEvent
 from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY
 from ranger.ext.lazy_property import lazy_property
+from ranger.ext.signals import Signal
 
 MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION
 
@@ -34,6 +35,8 @@ def _setup_mouse(signal):
         curses.mousemask(0)
 
 class UI(DisplayableContainer):
+    ALLOWED_VIEWMODES = 'miller', 'multipane'
+
     is_set_up = False
     load_mode = False
     is_on = False
@@ -87,6 +90,7 @@ class UI(DisplayableContainer):
             self.win.addstr("loading...")
             self.win.refresh()
             self._draw_title = curses.tigetflag('hs') # has_status_line
+
         self.update_size()
         self.is_on = True
 
@@ -223,7 +227,7 @@ class UI(DisplayableContainer):
 
     def setup(self):
         """Build up the UI by initializing widgets."""
-        from ranger.gui.widgets.browserview import BrowserView
+        from ranger.gui.widgets.view_miller import ViewMiller
         from ranger.gui.widgets.titlebar import TitleBar
         from ranger.gui.widgets.console import Console
         from ranger.gui.widgets.statusbar import StatusBar
@@ -235,9 +239,10 @@ class UI(DisplayableContainer):
         self.add_child(self.titlebar)
 
         # Create the browser view
-        self.browser = BrowserView(self.win, self.settings.column_ratios)
-        self.settings.signal_bind('setopt.column_ratios',
-                self.browser.change_ratios)
+        self.settings.signal_bind('setopt.viewmode', self._set_viewmode)
+        self._viewmode = None
+        # The following line sets self.browser implicitly through the signal
+        self.viewmode = self.settings.viewmode
         self.add_child(self.browser)
 
         # Create the process manager
@@ -339,10 +344,11 @@ class UI(DisplayableContainer):
     def draw_images(self):
         if self.pager.visible:
             self.pager.draw_image()
-        elif self.browser.pager.visible:
-            self.browser.pager.draw_image()
-        else:
-            self.browser.columns[-1].draw_image()
+        elif hasattr(self.browser, 'pager'):
+            if self.browser.pager.visible:
+                self.browser.pager.draw_image()
+            else:
+                self.browser.columns[-1].draw_image()
 
     def close_pager(self):
         if self.console.visible:
@@ -411,7 +417,44 @@ class UI(DisplayableContainer):
         self.status.hint = text
 
     def get_pager(self):
-        if self.browser.pager.visible:
+        if hasattr(self.browser, 'pager') and self.browser.pager.visible:
             return self.browser.pager
         else:
             return self.pager
+
+    def _get_viewmode(self):
+        return self._viewmode
+
+    def _set_viewmode(self, value):
+        if isinstance(value, Signal):
+            value = value.value
+        if value == '':
+            value = self.ALLOWED_VIEWMODES[0]
+        if value in self.ALLOWED_VIEWMODES:
+            if self._viewmode != value:
+                self._viewmode = value
+                resize = False
+                if hasattr(self, 'browser'):
+                    old_size = self.browser.y, self.browser.x, self.browser.hei, self.browser.wid
+                    self.remove_child(self.browser)
+                    self.browser.destroy()
+                    resize = True
+
+                self.browser = self._viewmode_to_class(value)(self.win)
+                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)))
+
+    viewmode = property(_get_viewmode, _set_viewmode)
+
+    def _viewmode_to_class(self, viewmode):
+        if viewmode == 'miller':
+            from ranger.gui.widgets.view_miller import ViewMiller
+            return ViewMiller
+        if viewmode == 'multipane':
+            from ranger.gui.widgets.view_multipane import ViewMultipane
+            return ViewMultipane
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 07830b31..129d8486 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -27,7 +27,7 @@ class BrowserColumn(Pager):
     old_dir = None
     old_thisfile = None
 
-    def __init__(self, win, level):
+    def __init__(self, win, level, tab=None):
         """Initializes a Browser Column Widget
 
         win = the curses window object of the BrowserView
@@ -40,6 +40,7 @@ class BrowserColumn(Pager):
         Pager.__init__(self, win)
         Widget.__init__(self, win)
         self.level = level
+        self.tab = tab
         self.original_level = level
 
         self.settings.signal_bind('setopt.display_size_in_main_column',
@@ -48,9 +49,6 @@ class BrowserColumn(Pager):
     def request_redraw(self):
         self.need_redraw = True
 
-    def resize(self, y, x, hei, wid):
-        Widget.resize(self, y, x, hei, wid)
-
     def click(self, event):
         """Handle a MouseEvent"""
         direction = event.mouse_wheel_direction()
@@ -132,7 +130,11 @@ class BrowserColumn(Pager):
 
     def poke(self):
         Widget.poke(self)
-        self.target = self.fm.thistab.at_level(self.level)
+        if self.tab is None:
+            tab = self.fm.thistab
+        else:
+            tab = self.tab
+        self.target = tab.at_level(self.level)
 
     def draw(self):
         """Call either _draw_file() or _draw_directory()"""
@@ -201,6 +203,15 @@ class BrowserColumn(Pager):
 
         base_color = ['in_browser']
 
+        if self.fm.ui.viewmode == 'multipane' and self.tab is not None:
+            active_pane = self.tab == self.fm.thistab
+            if active_pane:
+                base_color.append('active_pane')
+            else:
+                base_color.append('inactive_pane')
+        else:
+            active_pane = False
+
         self.win.move(0, 0)
 
         if not self.target.content_loaded:
@@ -228,7 +239,7 @@ class BrowserColumn(Pager):
 
         copied = [f.path for f in self.fm.copy_buffer]
 
-        selected_i = self.target.pointer
+        selected_i = self._get_index_of_selected_file()
         for line in range(self.hei):
             i = line + self.scroll_begin
             if line > self.hei:
@@ -258,7 +269,7 @@ class BrowserColumn(Pager):
             key = (self.wid, selected_i == i, drawn.marked, self.main_column,
                    drawn.path in copied, tagged_marker, drawn.infostring,
                    drawn.vcsstatus, drawn.vcsremotestatus, self.target.has_vcschild,
-                   self.fm.do_cut, current_linemode.name, metakey)
+                   self.fm.do_cut, current_linemode.name, metakey, active_pane)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
@@ -336,6 +347,12 @@ class BrowserColumn(Pager):
             self.execute_curses_batch(line, display_data)
             self.color_reset()
 
+    def _get_index_of_selected_file(self):
+        if self.fm.ui.viewmode == 'multipane' and hasattr(self, 'tab'):
+            return self.tab.pointer
+        else:
+            return self.target.pointer
+
     def _total_len(self, predisplay):
         return sum([len(WideString(s)) for s, L in predisplay])
 
@@ -391,7 +408,7 @@ class BrowserColumn(Pager):
 
     def _draw_directory_color(self, i, drawn, copied):
         this_color = []
-        if i == self.target.pointer:
+        if i == self._get_index_of_selected_file():
             this_color.append('selected')
 
         if drawn.marked:
@@ -431,7 +448,7 @@ class BrowserColumn(Pager):
         dirsize = len(self.target)
         winsize = self.hei
         halfwinsize = winsize // 2
-        index = self.target.pointer or 0
+        index = self._get_index_of_selected_file() or 0
         original = self.target.scroll_begin
         projected = index - original
 
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 159b8f1e..156298ab 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -76,6 +76,7 @@ class Console(Widget):
                     except UnicodeEncodeError:
                         pass
                 f.close()
+        Widget.destroy(self)
 
     def draw(self):
         self.win.erase()
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index ce3cc1bf..0d185f98 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -52,6 +52,7 @@ class Pager(Widget):
 
     def destroy(self):
         self.clear_image(force=True)
+        Widget.destroy(self)
 
     def finalize(self):
         self.fm.ui.win.move(self.y, self.x)
diff --git a/ranger/gui/widgets/view_base.py b/ranger/gui/widgets/view_base.py
new file mode 100644
index 00000000..9fbd9586
--- /dev/null
+++ b/ranger/gui/widgets/view_base.py
@@ -0,0 +1,159 @@
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+
+"""The base GUI element for views on the directory"""
+
+import curses, _curses
+from ranger.ext.keybinding_parser import key_to_string
+from . import Widget
+from ..displayable import DisplayableContainer
+
+class ViewBase(Widget, DisplayableContainer):
+    draw_bookmarks = False
+    need_clear = False
+    draw_hints = False
+    draw_info = False
+
+    def __init__(self, win):
+        DisplayableContainer.__init__(self, win)
+
+        self.fm.signal_bind('move', self.request_clear)
+        self.old_draw_borders = self.settings.draw_borders
+
+    def request_clear(self):
+        self.need_clear = True
+
+    def draw(self):
+        if self.need_clear:
+            self.win.erase()
+            self.need_redraw = True
+            self.need_clear = False
+        for tab in self.fm.tabs.values():
+            directory = tab.thisdir
+            if directory:
+                directory.load_content_if_outdated()
+                directory.use()
+        DisplayableContainer.draw(self)
+        if self.draw_bookmarks:
+            self._draw_bookmarks()
+        elif self.draw_hints:
+            self._draw_hints()
+        elif self.draw_info:
+            self._draw_info(self.draw_info)
+
+    def finalize(self):
+        if hasattr(self, 'pager') and self.pager.visible:
+            try:
+                self.fm.ui.win.move(self.main_column.y, self.main_column.x)
+            except:
+                pass
+        else:
+            try:
+                x = self.main_column.x
+                y = self.main_column.y + self.main_column.target.pointer\
+                        - self.main_column.scroll_begin
+                self.fm.ui.win.move(y, x)
+            except:
+                pass
+
+    def _draw_bookmarks(self):
+        self.columns[-1].clear_image(force=True)
+        self.fm.bookmarks.update_if_outdated()
+        self.color_reset()
+        self.need_clear = True
+
+        sorted_bookmarks = sorted((item for item in self.fm.bookmarks \
+            if self.fm.settings.show_hidden_bookmarks or \
+            '/.' not in item[1].path), key=lambda t: t[0].lower())
+
+        hei = min(self.hei - 1, len(sorted_bookmarks))
+        ystart = self.hei - hei
+
+        maxlen = self.wid
+        self.addnstr(ystart - 1, 0, "mark  path".ljust(self.wid), self.wid)
+
+        whitespace = " " * maxlen
+        for line, items in zip(range(self.hei-1), sorted_bookmarks):
+            key, mark = items
+            string = " " + key + "   " + mark.path
+            self.addstr(ystart + line, 0, whitespace)
+            self.addnstr(ystart + line, 0, string, self.wid)
+
+        self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
+
+    def _draw_info(self, lines):
+        self.columns[-1].clear_image(force=True)
+        self.need_clear = True
+        hei = min(self.hei - 1, len(lines))
+        ystart = self.hei - hei
+        i = ystart
+        whitespace = " " * self.wid
+        for line in lines:
+            if i >= self.hei:
+                break
+            self.addstr(i, 0, whitespace)
+            self.addnstr(i, 0, line, self.wid)
+            i += 1
+
+    def _draw_hints(self):
+        self.columns[-1].clear_image(force=True)
+        self.need_clear = True
+        hints = []
+        for k, v in self.fm.ui.keybuffer.pointer.items():
+            k = key_to_string(k)
+            if isinstance(v, dict):
+                text = '...'
+            else:
+                text = v
+            if text.startswith('hint') or text.startswith('chain hint'):
+                continue
+            hints.append((k, text))
+        hints.sort(key=lambda t: t[1])
+
+        hei = min(self.hei - 1, len(hints))
+        ystart = self.hei - hei
+        self.addnstr(ystart - 1, 0, "key          command".ljust(self.wid),
+                self.wid)
+        try:
+            self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
+        except:
+            pass
+        whitespace = " " * self.wid
+        i = ystart
+        for key, cmd in hints:
+            string = " " + key.ljust(11) + " " + cmd
+            self.addstr(i, 0, whitespace)
+            self.addnstr(i, 0, string, self.wid)
+            i += 1
+
+    def _collapse(self):
+        # Should the last column be cut off? (Because there is no preview)
+        if not self.settings.collapse_preview or not self.preview \
+                or not self.stretch_ratios:
+            return False
+        result = not self.columns[-1].has_preview()
+        target = self.columns[-1].target
+        if not result and target and target.is_file:
+            if self.fm.settings.preview_script and \
+                    self.fm.settings.use_preview_script:
+                try:
+                    result = not self.fm.previews[target.realpath]['foundpreview']
+                except:
+                    return self.old_collapse
+
+        self.old_collapse = result
+        return result
+
+    def click(self, event):
+        if DisplayableContainer.click(self, event):
+            return True
+        direction = event.mouse_wheel_direction()
+        if direction:
+            self.main_column.scroll(direction)
+        return False
+
+    def resize(self, y, x, hei, wid):
+        DisplayableContainer.resize(self, y, x, hei, wid)
+
+    def poke(self):
+        DisplayableContainer.poke(self)
diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/view_miller.py
index e9640208..dbf59ff8 100644
--- a/ranger/gui/widgets/browserview.py
+++ b/ranger/gui/widgets/view_miller.py
@@ -1,50 +1,49 @@
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
 
-"""The BrowserView manages a set of BrowserColumns."""
+"""ViewMiller arranges the view in miller columns"""
 
 import curses, _curses
 from ranger.ext.signals import Signal
-from ranger.ext.keybinding_parser import key_to_string
-from . import Widget
 from .browsercolumn import BrowserColumn
 from .pager import Pager
 from ..displayable import DisplayableContainer
+from ranger.gui.widgets.view_base import ViewBase
 
-class BrowserView(Widget, DisplayableContainer):
+class ViewMiller(ViewBase):
     ratios = None
     preview = True
     is_collapsed = False
-    draw_bookmarks = False
     stretch_ratios = None
-    need_clear = False
     old_collapse = False
-    draw_hints = False
-    draw_info = False
 
-    def __init__(self, win, ratios, preview = True):
-        DisplayableContainer.__init__(self, win)
-        self.preview = preview
+    def __init__(self, win):
+        ViewBase.__init__(self, win)
+        self.preview = True
         self.columns = []
 
         self.pager = Pager(self.win, embedded=True)
         self.pager.visible = False
         self.add_child(self.pager)
 
-        self.change_ratios(ratios)
+        self.rebuild()
 
         for option in ('preview_directories', 'preview_files'):
             self.settings.signal_bind('setopt.' + option,
                     self._request_clear_if_has_borders, weak=True)
 
-        self.fm.signal_bind('move', self.request_clear)
         self.settings.signal_bind('setopt.column_ratios', self.request_clear)
+        self.settings.signal_bind('setopt.column_ratios', self.rebuild)
 
         self.old_draw_borders = self.settings.draw_borders
 
-    def change_ratios(self, ratios):
-        if isinstance(ratios, Signal):
-            ratios = ratios.value
+    def rebuild(self):
+        for child in self.container:
+            if isinstance(child, BrowserColumn):
+                self.remove_child(child)
+                child.destroy()
+
+        ratios = self.settings.column_ratios
 
         for column in self.columns:
             column.destroy()
@@ -82,9 +81,6 @@ class BrowserView(Widget, DisplayableContainer):
         if self.settings.draw_borders:
             self.request_clear()
 
-    def request_clear(self):
-        self.need_clear = True
-
     def draw(self):
         if self.need_clear:
             self.win.erase()
@@ -105,21 +101,6 @@ class BrowserView(Widget, DisplayableContainer):
         elif self.draw_info:
             self._draw_info(self.draw_info)
 
-    def finalize(self):
-        if self.pager.visible:
-            try:
-                self.fm.ui.win.move(self.main_column.y, self.main_column.x)
-            except:
-                pass
-        else:
-            try:
-                x = self.main_column.x
-                y = self.main_column.y + self.main_column.target.pointer\
-                        - self.main_column.scroll_begin
-                self.fm.ui.win.move(y, x)
-            except:
-                pass
-
     def _draw_borders(self):
         win = self.win
         self.color('in_browser', 'border')
@@ -180,76 +161,6 @@ class BrowserView(Widget, DisplayableContainer):
         self.addch(0, right_end, curses.ACS_URCORNER)
         self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER)
 
-    def _draw_bookmarks(self):
-        self.columns[-1].clear_image(force=True)
-        self.fm.bookmarks.update_if_outdated()
-        self.color_reset()
-        self.need_clear = True
-
-        sorted_bookmarks = sorted((item for item in self.fm.bookmarks \
-            if self.fm.settings.show_hidden_bookmarks or \
-            '/.' not in item[1].path), key=lambda t: t[0].lower())
-
-        hei = min(self.hei - 1, len(sorted_bookmarks))
-        ystart = self.hei - hei
-
-        maxlen = self.wid
-        self.addnstr(ystart - 1, 0, "mark  path".ljust(self.wid), self.wid)
-
-        whitespace = " " * maxlen
-        for line, items in zip(range(self.hei-1), sorted_bookmarks):
-            key, mark = items
-            string = " " + key + "   " + mark.path
-            self.addstr(ystart + line, 0, whitespace)
-            self.addnstr(ystart + line, 0, string, self.wid)
-
-        self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
-
-    def _draw_info(self, lines):
-        self.columns[-1].clear_image(force=True)
-        self.need_clear = True
-        hei = min(self.hei - 1, len(lines))
-        ystart = self.hei - hei
-        i = ystart
-        whitespace = " " * self.wid
-        for line in lines:
-            if i >= self.hei:
-                break
-            self.addstr(i, 0, whitespace)
-            self.addnstr(i, 0, line, self.wid)
-            i += 1
-
-    def _draw_hints(self):
-        self.columns[-1].clear_image(force=True)
-        self.need_clear = True
-        hints = []
-        for k, v in self.fm.ui.keybuffer.pointer.items():
-            k = key_to_string(k)
-            if isinstance(v, dict):
-                text = '...'
-            else:
-                text = v
-            if text.startswith('hint') or text.startswith('chain hint'):
-                continue
-            hints.append((k, text))
-        hints.sort(key=lambda t: t[1])
-
-        hei = min(self.hei - 1, len(hints))
-        ystart = self.hei - hei
-        self.addnstr(ystart - 1, 0, "key          command".ljust(self.wid),
-                self.wid)
-        try:
-            self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
-        except:
-            pass
-        whitespace = " " * self.wid
-        i = ystart
-        for key, cmd in hints:
-            string = " " + key.ljust(11) + " " + cmd
-            self.addstr(i, 0, whitespace)
-            self.addnstr(i, 0, string, self.wid)
-            i += 1
-
     def _collapse(self):
         # Should the last column be cut off? (Because there is no preview)
         if not self.settings.collapse_preview or not self.preview \
@@ -270,7 +181,8 @@ class BrowserView(Widget, DisplayableContainer):
 
     def resize(self, y, x, hei, wid):
         """Resize all the columns according to the given ratio"""
-        DisplayableContainer.resize(self, y, x, hei, wid)
+        ViewBase.resize(self, y, x, hei, wid)
+
         borders = self.settings.draw_borders
         pad = 1 if borders else 0
         left = pad
@@ -291,7 +203,7 @@ class BrowserView(Widget, DisplayableContainer):
                 if not cut_off:
                     wid = int(self.wid - left + 1 - pad)
                 else:
-                    self.columns[i].resize(pad, left - 1, hei - pad * 2, 1)
+                    self.columns[i].resize(pad, max(0, left - 1), hei - pad * 2, 1)
                     self.columns[i].visible = False
                     continue
 
@@ -312,14 +224,6 @@ class BrowserView(Widget, DisplayableContainer):
 
             left += wid
 
-    def click(self, event):
-        if DisplayableContainer.click(self, event):
-            return True
-        direction = event.mouse_wheel_direction()
-        if direction:
-            self.main_column.scroll(direction)
-        return False
-
     def open_pager(self):
         self.pager.visible = True
         self.pager.focused = True
@@ -343,7 +247,7 @@ class BrowserView(Widget, DisplayableContainer):
             pass
 
     def poke(self):
-        DisplayableContainer.poke(self)
+        ViewBase.poke(self)
 
         # Show the preview column when it has a preview but has
         # been hidden (e.g. because of padding_right = False)
diff --git a/ranger/gui/widgets/view_multipane.py b/ranger/gui/widgets/view_multipane.py
new file mode 100644
index 00000000..1899687e
--- /dev/null
+++ b/ranger/gui/widgets/view_multipane.py
@@ -0,0 +1,51 @@
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+
+from ranger.gui.widgets.view_base import ViewBase
+from ranger.gui.widgets.browsercolumn import BrowserColumn
+
+class ViewMultipane(ViewBase):
+    def __init__(self, win):
+        ViewBase.__init__(self, win)
+
+        self.fm.signal_bind('tab.layoutchange', self._layoutchange_handler)
+        self.fm.signal_bind('tab.change', self._tabchange_handler)
+        self.rebuild()
+
+    def _layoutchange_handler(self):
+        if self.fm.ui.browser == self:
+            self.rebuild()
+
+    def _tabchange_handler(self, signal):
+        if self.fm.ui.browser == self:
+            if signal.old:
+                signal.old.need_redraw = True
+            if signal.new:
+                signal.new.need_redraw = True
+
+    def rebuild(self):
+        self.columns = []
+
+        for child in self.container:
+            self.remove_child(child)
+            child.destroy()
+        for name, tab in self.fm.tabs.items():
+            column = BrowserColumn(self.win, 0, tab=tab)
+            column.main_column = True
+            column.display_infostring = True
+            if name == self.fm.current_tab:
+                self.main_column = column
+            self.columns.append(column)
+            self.add_child(column)
+        self.resize(self.y, self.x, self.hei, self.wid)
+
+    def resize(self, y, x, hei, wid):
+        ViewBase.resize(self, y, x, hei, wid)
+        column_width = int(float(wid) / 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.need_redraw = True
+        self.need_redraw = True
diff --git a/tests/ranger/container/test_fsobject.py b/tests/ranger/container/test_fsobject.py
new file mode 100644
index 00000000..3ea52d6f
--- /dev/null
+++ b/tests/ranger/container/test_fsobject.py
@@ -0,0 +1,34 @@
+import pytest
+import operator
+
+from ranger.container.fsobject import FileSystemObject
+
+
+class MockFM(object):
+    """Used to fullfill the dependency by FileSystemObject."""
+
+    default_linemodes = []
+
+
+def create_filesystem_object(path):
+    """Create a FileSystemObject without an fm object."""
+    fso = FileSystemObject.__new__(FileSystemObject)
+    fso.fm = MockFM()
+    fso.__init__(path)
+    return fso
+
+
+def test_basename_natural1():
+    """Test filenames without extensions."""
+    fsos = [create_filesystem_object(path)
+            for path in ("hello", "hello1", "hello2")]
+    assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural")))
+    assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural_lower")))
+
+
+def test_basename_natural2():
+    """Test filenames with extensions."""
+    fsos = [create_filesystem_object(path)
+            for path in ("hello", "hello.txt", "hello1.txt", "hello2.txt")]
+    assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural")))
+    assert(fsos == sorted(fsos[::-1], key=operator.attrgetter("basename_natural_lower")))