summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/ranger.161
-rw-r--r--doc/ranger.pod73
-rw-r--r--ranger/config/commands.py166
-rw-r--r--ranger/config/rc.conf10
-rw-r--r--ranger/container/directory.py78
-rw-r--r--ranger/container/file.py2
-rw-r--r--ranger/container/fsobject.py54
-rw-r--r--ranger/container/settings.py1
-rw-r--r--ranger/core/actions.py47
-rw-r--r--ranger/core/fm.py16
-rw-r--r--ranger/ext/papermanager.py171
-rw-r--r--ranger/gui/widgets/browsercolumn.py46
-rw-r--r--ranger/gui/widgets/titlebar.py2
13 files changed, 655 insertions, 72 deletions
diff --git a/doc/ranger.1 b/doc/ranger.1
index 9aa4e318..27de2aa9 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -133,7 +133,7 @@
 .\" ========================================================================
 .\"
 .IX Title "RANGER 1"
-.TH RANGER 1 "ranger-1.6.1" "10/31/2014" "ranger manual"
+.TH RANGER 1 "ranger-1.6.1" "12/05/2014" "ranger manual"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -539,6 +539,11 @@ Go to the next or previous tab. You can also use \s-1TAB\s0 and \s-1SHIFT+TAB\s0
 .IP "gc, ^W" 14
 .IX Item "gc, ^W"
 Close the current tab.  The last tab cannot be closed this way.
+.IP "M" 14
+.IX Item "M"
+A key chain that allows you to quickly change the line mode of all the files of
+the current directory.  For a more permanent solution, use the command
+\&\*(L"default_linemode\*(R" in your rc.conf.
 .SS "READLINE-LIKE \s-1BINDINGS IN THE CONSOLE\s0"
 .IX Subsection "READLINE-LIKE BINDINGS IN THE CONSOLE"
 .IP "^B, ^F" 14
@@ -684,6 +689,11 @@ How many directory changes should be kept in history?
 .IP "mouse_enabled [bool] <zm>" 4
 .IX Item "mouse_enabled [bool] <zm>"
 Enable mouse input?
+.IP "papermanager_deep_search [bool]" 4
+.IX Item "papermanager_deep_search [bool]"
+When the paper manager module looks for metadata, should it only look for a
+\&\*(L".paperinfo\*(R" file in the current directory, or do a deep search and check all
+directories above the current one as well?
 .IP "padding_right [bool]" 4
 .IX Item "padding_right [bool]"
 When collapse_preview is on and there is no preview, should there remain a
@@ -798,18 +808,25 @@ ranger.  For your convenience, this is a list of the \*(L"public\*(R" commands i
 \& copypmap key newkey [newkey2...]
 \& copytmap key newkey [newkey2...]
 \& cunmap keys...
+\& default_linemode [path=regexp | tag=tags] linemodename
 \& delete
 \& edit [filename]
 \& eval [\-q] python_code
 \& filter [string]
 \& find pattern
 \& grep pattern
+\& linemode linemodename
 \& load_copy_buffer
 \& map key command
 \& mark pattern
 \& mark_tag [tags]
 \& mkdir dirname
 \& open_with [application] [flags] [mode]
+\& paper
+\& paper_authors [authors]
+\& paper_title [title]
+\& paper_url [url]
+\& paper_year [year]
 \& pmap key command
 \& punmap keys...
 \& quit
@@ -898,6 +915,20 @@ See \f(CW\*(C`copymap\*(C'\fR
 .IP "cunmap [\fIkeys...\fR]" 2
 .IX Item "cunmap [keys...]"
 Removes key mappings of the console. Works like the \f(CW\*(C`unmap\*(C'\fR command.
+.IP "default_linemode [\fIpath=regexp\fR | \fItag=tags\fR] \fIlinemodename\fR" 2
+.IX Item "default_linemode [path=regexp | tag=tags] linemodename"
+Sets the default linemode.  See \fIlinemode\fR command.
+.Sp
+Examples:
+.Sp
+Set the global default linemode to \*(L"permissions\*(R":
+ :default_linemode permissions
+.Sp
+Set the default linemode to \*(L"permissions\*(R" for all files tagged with \*(L"p\*(R" or \*(L"P\*(R":
+ :default_linemode tag=pP permissions
+.Sp
+Set the default linemode for all files in ~/books/ to \*(L"papertitle\*(R":
+ :default_linemode path=/home/.*?/books/.* papertitle
 .IP "delete" 2
 .IX Item "delete"
 Destroy all files in the selection with a roundhouse kick.  ranger will ask for
@@ -932,6 +963,16 @@ This command is based on the \fIscout\fR command and supports all of its options
 .IP "grep \fIpattern\fR" 2
 .IX Item "grep pattern"
 Looks for a string in all marked files or directories.
+.IP "linemode \fIlinemodename\fR" 2
+.IX Item "linemode linemodename"
+Sets the linemode of all files in the current directory.  The linemode may be:
+.Sp
+.Vb 4
+\& "filename": display each line as "<basename>...<size>"
+\& "permissions": display each line as "<permissions> <owner> <group> <basename>"
+\& "papertitle": display metadata from .paperinfo files if available, fall back
+\&     to the "filename" linemode if no metadata was found.  See :paper commands.
+.Ve
 .IP "load_copy_buffer" 2
 .IX Item "load_copy_buffer"
 Load the copy buffer from \fI~/.config/ranger/copy_buffer\fR.  This can be used to
@@ -968,6 +1009,24 @@ of applications is generated by the external file opener \*(L"rifle\*(R" and can
 displayed when pressing \*(L"r\*(R" in ranger.
 .Sp
 Note that if you specify an application, the mode is ignored.
+.IP "paper" 2
+.IX Item "paper"
+This command opens a series of commands on the console that will ask the user
+to input metadata about the current file.  This is used by the paper manager
+module of ranger and can be later displayed in ranger, for example by setting
+the option \*(L"linemode\*(R" to \*(L"papertitle\*(R".
+.IP "paper_authors \fIauthors\fR" 2
+.IX Item "paper_authors authors"
+Tells the paper manager to set/update the authors of the current file
+.IP "paper_title \fItitle\fR" 2
+.IX Item "paper_title title"
+Tells the paper manager to set/update the title of the current file
+.IP "paper_url \fIurl\fR" 2
+.IX Item "paper_url url"
+Tells the paper manager to set/update the url of the current file
+.IP "paper_year \fIyear\fR" 2
+.IX Item "paper_year year"
+Tells the paper manager to set/update the year of the current file
 .IP "pmap \fIkey\fR \fIcommand\fR" 2
 .IX Item "pmap key command"
 Binds keys for the pager. Works like the \f(CW\*(C`map\*(C'\fR command.
diff --git a/doc/ranger.pod b/doc/ranger.pod
index d6a6a5b0..211aea11 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -490,6 +490,12 @@ Go to the next or previous tab. You can also use TAB and SHIFT+TAB instead.
 
 Close the current tab.  The last tab cannot be closed this way.
 
+=item M
+
+A key chain that allows you to quickly change the line mode of all the files of
+the current directory.  For a more permanent solution, use the command
+"default_linemode" in your rc.conf.
+
 =back
 
 =head2 READLINE-LIKE BINDINGS IN THE CONSOLE
@@ -674,6 +680,12 @@ How many directory changes should be kept in history?
 
 Enable mouse input?
 
+=item papermanager_deep_search [bool]
+
+When the paper manager module looks for metadata, should it only look for a
+".paperinfo" file in the current directory, or do a deep search and check all
+directories above the current one as well?
+
 =item padding_right [bool]
 
 When collapse_preview is on and there is no preview, should there remain a
@@ -813,19 +825,27 @@ ranger.  For your convenience, this is a list of the "public" commands including
  copypmap key newkey [newkey2...]
  copytmap key newkey [newkey2...]
  cunmap keys...
+ default_linemode [path=regexp | tag=tags] linemodename
  delete
  edit [filename]
  eval [-q] python_code
  filter [string]
  filter_inode_type [dfl]
  find pattern
+ flat level
  grep pattern
+ linemode linemodename
  load_copy_buffer
  map key command
  mark pattern
  mark_tag [tags]
  mkdir dirname
  open_with [application] [flags] [mode]
+ paper
+ paper_authors [authors]
+ paper_title [title]
+ paper_url [url]
+ paper_year [year]
  pmap key command
  punmap keys...
  quit
@@ -928,6 +948,21 @@ See C<copymap>
 
 Removes key mappings of the console. Works like the C<unmap> command.
 
+=item default_linemode [I<path=regexp> | I<tag=tags>] I<linemodename>
+
+Sets the default linemode.  See I<linemode> command.
+
+Examples:
+
+Set the global default linemode to "permissions":
+ :default_linemode permissions
+
+Set the default linemode to "permissions" for all files tagged with "p" or "P":
+ :default_linemode tag=pP permissions
+
+Set the default linemode for all files in ~/books/ to "papertitle":
+ :default_linemode path=/home/.*?/books/.* papertitle
+
 =item delete
 
 Destroy all files in the selection with a roundhouse kick.  ranger will ask for
@@ -971,10 +1006,25 @@ be run immediately. (Or entered, if it's a directory.)
 
 This command is based on the I<scout> command and supports all of its options.
 
+=item flat level
+
+Flattens the directory view up to the specified level. Level -1 means infinite
+level. Level 0 means standard view without flattened directory view. Level
+values -2 and less are invalid.
+
 =item grep I<pattern>
 
 Looks for a string in all marked files or directories.
 
+=item linemode I<linemodename>
+
+Sets the linemode of all files in the current directory.  The linemode may be:
+
+ "filename": display each line as "<basename>...<size>"
+ "permissions": display each line as "<permissions> <owner> <group> <basename>"
+ "papertitle": display metadata from .paperinfo files if available, fall back
+     to the "filename" linemode if no metadata was found.  See :paper commands.
+
 =item load_copy_buffer
 
 Load the copy buffer from F<~/.config/ranger/copy_buffer>.  This can be used to
@@ -1017,6 +1067,29 @@ displayed when pressing "r" in ranger.
 
 Note that if you specify an application, the mode is ignored.
 
+=item paper
+
+This command opens a series of commands on the console that will ask the user
+to input metadata about the current file.  This is used by the paper manager
+module of ranger and can be later displayed in ranger, for example by setting
+the option "linemode" to "papertitle".
+
+=item paper_authors I<authors>
+
+Tells the paper manager to set/update the authors of the current file
+
+=item paper_title I<title>
+
+Tells the paper manager to set/update the title of the current file
+
+=item paper_url I<url>
+
+Tells the paper manager to set/update the url of the current file
+
+=item paper_year I<year>
+
+Tells the paper manager to set/update the year of the current file
+
 =item pmap I<key> I<command>
 
 Binds keys for the pager. Works like the C<map> command.
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 6e641219..1f59ee90 100644
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -381,6 +381,43 @@ class setintag(setlocal):
         self.fm.set_option_from_string(name, value, tags=tags)
 
 
+class default_linemode(Command):
+    def execute(self):
+        import re
+        from ranger.container.fsobject import POSSIBLE_LINEMODES
+
+        if len(self.args) < 2:
+            self.fm.notify("Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
+
+        # Extract options like "path=..." or "tag=..." from the command line
+        arg1 = self.arg(1)
+        method = "always"
+        argument = None
+        if arg1.startswith("path="):
+            method = "path"
+            argument = re.compile(arg1[5:])
+            self.shift()
+        elif arg1.startswith("tag="):
+            method = "tag"
+            argument = arg1[4:]
+            self.shift()
+
+        # Extract and validate the line mode from the command line
+        linemode = self.rest(1)
+        if linemode not in POSSIBLE_LINEMODES:
+            self.fm.notify("Invalid linemode: %s; should be %s" %
+                    (linemode, "/".join(POSSIBLE_LINEMODES)), bad=True)
+
+        # Add the prepared entry to the fm.default_linemodes
+        entry = [method, argument, linemode]
+        self.fm.default_linemodes.appendleft(entry)
+
+        # Redraw the columns
+        if hasattr(self.fm.ui, "browser"):
+            for col in self.fm.ui.browser.columns:
+                col.need_redraw = True
+
+
 class quit(Command):
     """:quit
 
@@ -1150,6 +1187,33 @@ class scout(Command):
         return count == 1
 
 
+class filter_inode_type(Command):
+    """
+    :filter_inode_type [dfl]
+
+    Displays only the files of specified inode type. Parameters
+    can be combined.
+
+        d display directories
+        f display files
+        l display links
+    """
+
+    FILTER_DIRS  = 'd'
+    FILTER_FILES = 'f'
+    FILTER_LINKS = 'l'
+
+    def execute(self):
+        if not self.arg(1):
+            self.fm.thisdir.inode_type_filter = None
+        else:
+            self.fm.thisdir.inode_type_filter = lambda file: (
+                    True if ((self.FILTER_DIRS  in self.arg(1) and file.is_directory) or
+                             (self.FILTER_FILES in self.arg(1) and file.is_file and not file.is_link) or
+                             (self.FILTER_LINKS in self.arg(1) and file.is_link)) else False)
+        self.fm.thisdir.refilter()
+
+
 class grep(Command):
     """:grep <string>
 
@@ -1266,9 +1330,10 @@ class flat(Command):
     """
     :flat <level>
 
-    Flattens the directory view up to level specified.
+    Flattens the directory view up to the specified level.
+
         -1 fully flattened
-        0  remove flattened view
+         0 remove flattened view
     """
 
     def execute(self):
@@ -1277,33 +1342,94 @@ class flat(Command):
             level = int(level)
         except ValueError:
             level = self.quantifier
+        if level < -1:
+            self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
         self.fm.thisdir.unload()
         self.fm.thisdir.flat = level
         self.fm.thisdir.load_content()
 
 
-class filter_inode_type(Command):
+# Papermanager commands
+# --------------------------------
+class paper(Command):
     """
-    :filter_inode_type [dfl]
+    :paper
 
-    Displays only the files of specified inode type. Parameters
-    can be combined.
+    This command opens a series of commands on the console that will ask the
+    user to input metadata about the current file.  This is used by the paper
+    manager module of ranger and can be later displayed in ranger, for example
+    by setting the option "linemode" to "papertitle".
+    """
+    _paper_console_chain = None
+    def execute(self):
+        # TODO: This sets a pseudo-global variable containing a stack of
+        # commands that should be opened in the console next.  It's a
+        # work-around for ranger's lack of inherent console command chaining
+        # and will hopefully be implemented properly in the future.
+        paper._paper_console_chain = ["url", "year", "authors", "title"]
+
+        self._process_command_stack()
+
+    def _process_command_stack(self):
+        if paper._paper_console_chain:
+            key = paper._paper_console_chain.pop()
+            self._paper_fill_console(key)
+        else:
+            for col in self.fm.ui.browser.columns:
+                col.need_redraw = True
 
-        d display directories
-        f display files
-        l display links
+    def _paper_fill_console(self, key):
+        paperinfo = self.fm.papermanager.get_paper_info(self.fm.thisfile.path)
+        if key in paperinfo and paperinfo[key]:
+            existing_value = paperinfo[key]
+        else:
+            existing_value = ""
+        text = "paper_%s %s" % (key, existing_value)
+        self.fm.open_console(text, position=len(text))
+
+
+class paper_title(paper):
     """
+    :paper_title <title>
 
-    FILTER_DIRS  = 'd'
-    FILTER_FILES = 'f'
-    FILTER_LINKS = 'l'
+    Tells the paper manager to set/update the title of the current file
+    """
+    _key = "title"
 
     def execute(self):
-        if not self.arg(1):
-            self.fm.thisdir.inode_type_filter = None
-        else:
-            self.fm.thisdir.inode_type_filter = lambda file: (
-                    True if ((self.FILTER_DIRS  in self.arg(1) and file.is_directory) or
-                             (self.FILTER_FILES in self.arg(1) and file.is_file and not file.is_link) or
-                             (self.FILTER_LINKS in self.arg(1) and file.is_link)) else False)
-        self.fm.thisdir.refilter()
+        update_dict = dict()
+        update_dict[self._key] = self.rest(1)
+        self.fm.papermanager.set_paper_info(self.fm.thisfile.path, update_dict)
+        self._process_command_stack()
+
+    def tab(self):
+        paperinfo = self.fm.papermanager.get_paper_info(self.fm.thisfile.path)
+        if paperinfo[self._key]:
+            return self.arg(0) + " " + paperinfo[self._key]
+
+
+class paper_authors(paper_title):
+    """
+    :paper_authors <authors>
+
+    Tells the paper manager to set/update the authors of the current file
+    """
+    _key = "authors"
+
+
+class paper_url(paper_title):
+    """
+    :paper_url <authors>
+
+    Tells the paper manager to set/update the url of the current file
+    """
+    _key = "url"
+
+
+class paper_year(paper_title):
+    """
+    :paper_year <authors>
+
+    Tells the paper manager to set/update the year of the current file
+    """
+    _key = "year"
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 23cd2e10..12fd2a32 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -174,6 +174,11 @@ set show_selection_in_titlebar true
 # increases CPU load.
 set idle_delay 2000
 
+# When the paper manager module looks for metadata, should it only look for a
+# ".paperinfo" file in the current directory, or do a deep search and check all
+# directories above the current one as well?
+set papermanager_deep_search false
+
 # ===================================================================
 # == Local Options
 # ===================================================================
@@ -232,6 +237,11 @@ map r  chain draw_possible_programs; console open_with
 map f  console find 
 map cd console cd 
 
+# Change the line mode
+map Mf linemode filename
+map Mp linemode permissions
+map Mt linemode papertitle
+
 # Tagging / Marking
 map t       tag_toggle
 map ut      tag_remove
diff --git a/ranger/container/directory.py b/ranger/container/directory.py
index 17cc90b7..7d39daaf 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -20,7 +20,7 @@ from ranger.container.settings import LocalSettings
 
 def sort_by_basename(path):
     """returns path.basename (for sorting)"""
-    return path.basename
+    return path.drawn_basename
 
 def sort_by_basename_icase(path):
     """returns case-insensitive path.basename (for sorting)"""
@@ -50,6 +50,25 @@ def accept_file(file, filters):
             return False
     return True
 
+def walklevel(some_dir, level):
+    some_dir = some_dir.rstrip(os.path.sep)
+    followlinks = True if level > 0 else False
+    assert os.path.isdir(some_dir)
+    num_sep = some_dir.count(os.path.sep)
+    for root, dirs, files in os.walk(some_dir, followlinks=followlinks):
+        yield root, dirs, files
+        num_sep_this = root.count(os.path.sep)
+        if level != -1 and num_sep + level <= num_sep_this:
+            del dirs[:]
+
+def mtimelevel(path, level):
+    mtime = os.stat(path).st_mtime
+    for dirpath, dirnames, filenames in walklevel(path, level):
+        dirlist = [os.path.join("/", dirpath, d) for d in dirnames
+                if level == -1 or dirpath.count(os.path.sep) - path.count(os.path.sep) <= level]
+        mtime = max(mtime, max([-1] + [os.stat(d).st_mtime for d in dirlist]))
+    return mtime
+
 class Directory(FileSystemObject, Accumulator, Loadable):
     is_directory = True
     enterable = False
@@ -191,7 +210,7 @@ class Directory(FileSystemObject, Accumulator, Loadable):
         self.move_to_obj(self.pointed_obj)
 
     # XXX: Check for possible race conditions
-    def load_bit_by_bit(self, flat=0):
+    def load_bit_by_bit(self):
         """An iterator that loads a part on every next() call
 
         Returns a generator which load a part of the directory
@@ -202,17 +221,7 @@ class Directory(FileSystemObject, Accumulator, Loadable):
         self.percent = 0
         self.load_if_outdated()
 
-        basename_is_rel = True if flat else False
-
-        def walklevel(some_dir, level):
-            some_dir = some_dir.rstrip(os.path.sep)
-            assert os.path.isdir(some_dir)
-            num_sep = some_dir.count(os.path.sep)
-            for root, dirs, files in os.walk(some_dir):
-                yield root, dirs, files
-                num_sep_this = root.count(os.path.sep)
-                if level != -1 and num_sep + level <= num_sep_this:
-                    del dirs[:]
+        basename_is_rel_to = self.path if self.flat else None
 
         try:
             if self.runnable:
@@ -221,17 +230,20 @@ class Directory(FileSystemObject, Accumulator, Loadable):
 
                 self.mount_path = mount_path(mypath)
 
-                if flat:
+                if self.flat:
                     filelist = []
-                    for dirpath, dirnames, filenames in walklevel(mypath, flat):
-                        filelist += [os.path.join("/", dirpath, d) for d in dirnames
-                                if dirpath.count(os.path.sep) - mypath.count(os.path.sep) == flat]
+                    for dirpath, dirnames, filenames in walklevel(mypath, self.flat):
+                        dirlist = [os.path.join("/", dirpath, d) for d in dirnames
+                                if self.flat == -1 or dirpath.count(os.path.sep) - mypath.count(os.path.sep) <= self.flat]
+                        filelist += dirlist
                         filelist += [os.path.join("/", dirpath, f) for f in filenames]
-                    filenames = [os.path.relpath(name, mypath) for name in filelist]
+                    filenames = filelist
+                    self.load_content_mtime = mtimelevel(mypath, self.flat)
                 else:
                     filelist = os.listdir(mypath)
                     filenames = [mypath + (mypath == '/' and fname or '/' + fname)
                             for fname in filelist]
+                    self.load_content_mtime = os.stat(mypath).st_mtime
 
                 if self._cumulative_size_calculated:
                     # If self.content_loaded is true, this is not the first
@@ -253,8 +265,6 @@ class Directory(FileSystemObject, Accumulator, Loadable):
 
                 yield
 
-                self.load_content_mtime = os.stat(mypath).st_mtime
-
                 marked_paths = [obj.path for obj in self.marked_items]
 
                 files = []
@@ -277,17 +287,20 @@ class Directory(FileSystemObject, Accumulator, Loadable):
                         stats = None
                         is_a_dir = False
                     if is_a_dir:
-                        try:
-                            item = self.fm.get_directory(name,
-                                    basename_is_rel=basename_is_rel)
-                            item.load_if_outdated()
-                        except:
+                        if self.flat:
                             item = Directory(name, preload=stats, path_is_abs=True,
-                                    basename_is_rel=basename_is_rel)
+                                    basename_is_rel_to=basename_is_rel_to)
                             item.load()
+                        else:
+                            try:
+                                item = self.fm.get_directory(name)
+                                item.load_if_outdated()
+                            except:
+                                item = Directory(name, preload=stats, path_is_abs=True)
+                                item.load()
                     else:
                         item = File(name, preload=stats, path_is_abs=True,
-                                    basename_is_rel=basename_is_rel)
+                                    basename_is_rel_to=basename_is_rel_to)
                         item.load()
                         disk_usage += item.size
 
@@ -358,7 +371,7 @@ class Directory(FileSystemObject, Accumulator, Loadable):
                 schedule = True   # was: self.size > 30
 
             if self.load_generator is None:
-                self.load_generator = self.load_bit_by_bit(flat=self.flat)
+                self.load_generator = self.load_bit_by_bit()
 
                 if schedule and self.fm:
                     self.fm.loader.add(self)
@@ -538,7 +551,10 @@ class Directory(FileSystemObject, Accumulator, Loadable):
             return True
 
         try:
-            real_mtime = os.stat(self.path).st_mtime
+            if self.flat:
+                real_mtime = mtimelevel(self.path, self.flat)
+            else:
+                real_mtime = os.stat(self.path).st_mtime
         except OSError:
             real_mtime = None
             return False
@@ -575,6 +591,10 @@ class Directory(FileSystemObject, Accumulator, Loadable):
         """Is the directory empty?"""
         return self.files is None or len(self.files) == 0
 
+    def _set_linemode_of_children(self, mode):
+        for f in self.files:
+            f._set_linemode(mode)
+
     def __nonzero__(self):
         """Always True"""
         return True
diff --git a/ranger/container/file.py b/ranger/container/file.py
index ab677125..7297dc30 100644
--- a/ranger/container/file.py
+++ b/ranger/container/file.py
@@ -43,6 +43,8 @@ class File(FileSystemObject):
     preview_known = False
     preview_loading = False
 
+    _linemode = "filename"
+
     @property
     def firstbytes(self):
         try:
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index 6f206d0d..020721d8 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -12,9 +12,14 @@ DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits',
 
 BAD_INFO = '?'
 
+POSSIBLE_LINEMODES = ("filename", "papertitle", "permissions")
+DEFAULT_LINEMODE = "filename"
+
 import re
+from grp import getgrgid
 from os import lstat, stat, getcwd
 from os.path import abspath, basename, dirname, realpath, splitext, extsep, relpath
+from pwd import getpwuid
 from ranger.core.shared import FileManagerAware, SettingsAware
 from ranger.ext.shell_escape import shell_escape
 from ranger.ext.spawn import spawn
@@ -78,18 +83,22 @@ class FileSystemObject(FileManagerAware, SettingsAware):
     vcs_outdated = False
     vcs_enabled = False
 
-    basename_is_rel = False
+    basename_is_rel_to = None
+
+    _linemode = DEFAULT_LINEMODE
 
-    def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel=False):
+    def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel_to=None):
         if not path_is_abs:
             path = abspath(path)
         self.path = path
-        self.basename_is_rel = basename_is_rel
-        if not basename_is_rel:
+        self.basename_is_rel_to = basename_is_rel_to
+        if basename_is_rel_to == None:
             self.basename = basename(path)
+            self.drawn_basename = self.basename
         else:
-            self.basename = relpath(path, getcwd())
-        self.basename_lower = self.basename.lower()
+            self.basename = basename(path)
+            self.drawn_basename = relpath(path, basename_is_rel_to)
+        self.basename_lower = self.drawn_basename.lower()
         self.extension = splitext(self.basename)[1].lstrip(extsep) or None
         self.dirname = dirname(path)
         self.preload = preload
@@ -101,6 +110,21 @@ class FileSystemObject(FileManagerAware, SettingsAware):
         except ValueError:
             self.extension = None
 
+        # Set the line mode from fm.default_linemodes
+        for method, argument, linemode in self.fm.default_linemodes:
+            if linemode in POSSIBLE_LINEMODES:
+                if method == "always":
+                    self._linemode = linemode
+                    break
+                if method == "path" and argument.search(path):
+                    self._linemode = linemode
+                    break
+                if method == "tag" and self.realpath in self.fm.tags and \
+                        self.fm.tags.marker(self.realpath) in argument:
+                    self._linemode = linemode
+                    break
+
+
     def __repr__(self):
         return "<{0} {1}>".format(self.__class__.__name__, self.path)
 
@@ -118,7 +142,7 @@ class FileSystemObject(FileManagerAware, SettingsAware):
     @lazy_property
     def basename_natural(self):
         return [c if i % 3 == 1 else (int(c) if c else 0) for i, c in \
-            enumerate(_extract_number_re.split(self.basename))]
+            enumerate(_extract_number_re.split(self.drawn_basename))]
 
     @lazy_property
     def basename_natural_lower(self):
@@ -129,6 +153,19 @@ class FileSystemObject(FileManagerAware, SettingsAware):
     def safe_basename(self):
         return self.basename.translate(_safe_string_table)
 
+    @lazy_property
+    def user(self):
+        try:
+            return getpwuid(self.stat.st_uid)[0]
+        except:
+            return str(self.stat.st_uid)
+
+    @lazy_property
+    def group(self):
+        try:
+            return getgrgid(self.stat.st_gid)[0]
+        except:
+            return str(self.stat.st_gid)
 
     for attr in ('video', 'audio', 'image', 'media', 'document', 'container'):
         exec("%s = lazy_property("
@@ -373,3 +410,6 @@ class FileSystemObject(FileManagerAware, SettingsAware):
                 self.vcs_outdated = True
             return True
         return False
+
+    def _set_linemode(self, mode):
+        self._linemode = mode
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index f75c274f..ad42b13f 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -31,6 +31,7 @@ ALLOWED_SETTINGS = {
     'mouse_enabled': bool,
     'open_all_images': bool,
     'padding_right': bool,
+    'papermanager_deep_search': bool,
     'preview_directories': bool,
     'preview_files': bool,
     'preview_images': bool,
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index ba6b3658..879d1f68 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -27,6 +27,7 @@ from ranger.core.tab import Tab
 from ranger.container.file import File
 from ranger.core.loader import CommandLoader, CopyLoader
 from ranger.container.settings import ALLOWED_SETTINGS
+from ranger.container.fsobject import POSSIBLE_LINEMODES, DEFAULT_LINEMODE
 
 MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
 
@@ -51,6 +52,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
         self.garbage_collect(-1)
         self.enter_dir(old_path)
         self.change_mode('normal')
+        if self.papermanager:
+            self.papermanager.reset()
 
     def change_mode(self, mode):
         if mode == self.mode:
@@ -151,6 +154,50 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
         """Redraw the window"""
         self.ui.redraw_window()
 
+    def linemode(self, mode, directory=None, depth=0):
+        """
+        Change what is displayed as a filename.
+
+        - "mode" may be: "filename", "permissions", "papertitle", the mode "normal"
+          is mapped to "filename".
+        - "directory" specifies the directory. None means the current directory
+        - "depth" specifies the recursion depth
+        """
+
+        assert mode == "normal" or mode in POSSIBLE_LINEMODES
+
+        if mode == "normal":
+            mode = DEFAULT_LINEMODE
+
+        if directory is None:
+            directory = self.fm.thisdir
+
+        directories = set([directory])
+        bucket = set()
+
+        current_depth = 0
+
+        while True:
+            if current_depth >= depth:
+                for direct in directories:
+                    direct._set_linemode_of_children(mode)
+                break
+
+            else:
+                for direct in directories:
+                    direct._set_linemode_of_children(mode)
+                    for file_ in direct.files:
+                        if file_.is_directory:
+                            bucket.add(file_)
+
+                directories, bucket = bucket, directories
+                bucket.clear()
+                current_depth += 1
+
+        # Ask the browsercolumns to redraw
+        for col in self.ui.browser.columns:
+            col.need_redraw = True
+
     def open_console(self, string='', prompt=None, position=None):
         """Open the console"""
         self.change_mode('normal')
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index d567bf24..6bb4fd22 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -20,6 +20,7 @@ from ranger.gui.ui import UI
 from ranger.container.bookmarks import Bookmarks
 from ranger.core.runner import Runner
 from ranger.ext.img_display import ImageDisplayer
+from ranger.ext.papermanager import PaperManager
 from ranger.ext.rifle import Rifle
 from ranger.container.directory import Directory
 from ranger.ext.signals import SignalDispatcher
@@ -56,9 +57,14 @@ class FM(Actions, SignalDispatcher):
         self.restorable_tabs = deque([], ranger.MAX_RESTORABLE_TABS)
         self.py3 = sys.version_info >= (3, )
         self.previews = {}
+        self.default_linemodes = deque()
         self.loader = Loader()
         self.copy_buffer = set()
         self.do_cut = False
+        self.papermanager = PaperManager()
+        self.settings.signal_bind('setopt.papermanager_deep_search',
+                lambda signal: setattr(signal.fm.papermanager, 'deep_search',
+                    signal.value))
 
         try:
             self.username = pwd.getpwuid(os.geteuid()).pw_name
@@ -252,17 +258,13 @@ class FM(Actions, SignalDispatcher):
         """returns the path relative to rangers library directory"""
         return os.path.join(ranger.RANGERDIR, *paths)
 
-    def get_directory(self, path, basename_is_rel = False):
+    def get_directory(self, path):
         """Get the directory object at the given path"""
         path = os.path.abspath(path)
         try:
-            directory = self.directories[path]
-            if directory.basename_is_rel != basename_is_rel:
-                del self.directories[path]
-                raise KeyError
-            return directory
+            return self.directories[path]
         except KeyError:
-            obj = Directory(path, basename_is_rel = basename_is_rel)
+            obj = Directory(path)
             self.directories[path] = obj
             return obj
 
diff --git a/ranger/ext/papermanager.py b/ranger/ext/papermanager.py
new file mode 100644
index 00000000..a6292c49
--- /dev/null
+++ b/ranger/ext/papermanager.py
@@ -0,0 +1,171 @@
+# Copyright (C) 2014  Roman Zimbelmann <hut@lepus.uberspace.de>
+# This software is distributed under the terms of the GNU GPL version 3.
+
+"""
+A Paper Manager that reads metadata information about papers from a file.
+
+The file is named .paperinfo and is formatted as comma-separated values.
+
+The columns are:
+1. Filename
+2. Date
+3. Title
+4. Authors
+5. URL
+"""
+
+PAPERINFO_FILE_NAME = ".paperinfo"
+DEEP_SEARCH_DEFAULT = False
+
+import csv
+from os.path import join, dirname, exists, basename
+
+from ranger.ext.openstruct import OpenStruct
+
+class PaperManager(object):
+    def __init__(self):
+        self.metadata_cache = dict()
+        self.metafile_cache = dict()
+        self.deep_search = DEEP_SEARCH_DEFAULT
+
+    def reset(self):
+        self.metadata_cache.clear()
+        self.metafile_cache.clear()
+
+    def get_paper_info(self, filename):
+        try:
+            return self.metadata_cache[filename]
+        except KeyError:
+            result = OpenStruct(filename=filename, title=None, year=None,
+                    authors=None, url=None)
+
+            valid = (filename, basename(filename))
+            for metafile in self._get_metafile_names(filename):
+                for entry in self._get_metafile_content(metafile):
+                    if entry[0] in valid:
+                        self._fill_ostruct_with_data(result, entry)
+                        self.metadata_cache[filename] = result
+                        return result
+
+            # Cache the value
+            self.metadata_cache[filename] = result
+            return result
+
+    def set_paper_info(self, filename, update_dict):
+        result = None
+        found = False
+        valid = (filename, basename(filename))
+        first_metafile = None
+
+        if not self.deep_search:
+            metafile = next(self._get_metafile_names(filename))
+            return self._set_paper_info_raw(filename, update_dict, metafile)
+
+        for i, metafile in enumerate(self._get_metafile_names(filename)):
+            if i == 0:
+                first_metafile = metafile
+
+            csvfile = None
+            try:
+                csvfile = open(metafile, "r")
+            except:
+                # .paperinfo file doesn't exist... look for another one.
+                pass
+            else:
+                reader = csv.reader(csvfile, skipinitialspace=True)
+                for row in reader:
+                    name, year, title, authors, url = row
+                    if name in valid:
+                        return self._set_paper_info_raw(filename, update_dict,
+                                metafile)
+                self.metadata_cache[filename] = result
+            finally:
+                if csvfile:
+                    csvfile.close()
+
+        # No .paperinfo file found, so let's create a new one in the same path
+        # as the given file.
+        if first_metafile:
+            return self._set_paper_info_raw(filename, update_dict, first_metafile)
+
+    def _set_paper_info_raw(self, filename, update_dict, metafile):
+        valid = (filename, basename(filename))
+        paperinfo = OpenStruct(filename=filename, title=None, year=None,
+                authors=None, url=None)
+
+        try:
+            with open(metafile, "r") as infile:
+                reader = csv.reader(infile, skipinitialspace=True)
+                rows = list(reader)
+        except IOError:
+            rows = []
+
+        with open(metafile, "w") as outfile:
+            writer = csv.writer(outfile)
+            found = False
+
+            # Iterate through all rows and write them back to the file.
+            for row in rows:
+                if not found and row[0] in valid:
+                    # When finding the row that corresponds to the given filename,
+                    # update the items with the information from update_dict.
+                    self._fill_row_with_ostruct(row, update_dict)
+                    self._fill_ostruct_with_data(paperinfo, row)
+                    self.metadata_cache[filename] = paperinfo
+                    found = True
+                writer.writerow(row)
+
+            # If the row was never found, create a new one.
+            if not found:
+                row = [basename(filename), None, None, None, None]
+                self._fill_row_with_ostruct(row, update_dict)
+                self._fill_ostruct_with_data(paperinfo, row)
+                self.metadata_cache[filename] = paperinfo
+                writer.writerow(row)
+
+    def _get_metafile_content(self, metafile):
+        if metafile in self.metafile_cache:
+            return self.metafile_cache[metafile]
+        else:
+            if exists(metafile):
+                reader = csv.reader(open(metafile, "r"), skipinitialspace=True)
+
+                entries = list(entry for entry in reader if len(entry) == 5)
+                self.metafile_cache[metafile] = entries
+                return entries
+            else:
+                return []
+
+    def _get_metafile_names(self, path):
+        # Iterates through the paths of all .paperinfo files that could
+        # influence the metadata of the given file.
+        # When deep_search is deactivated, this only yields the .paperinfo file
+        # in the same directory as the given file.
+
+        base = dirname(path)
+        yield join(base, PAPERINFO_FILE_NAME)
+        if self.deep_search:
+            dirs = base.split("/")[1:]
+            for i in reversed(range(len(dirs))):
+                yield join("/" + "/".join(dirs[0:i]), PAPERINFO_FILE_NAME)
+
+    def _fill_ostruct_with_data(self, ostruct, dataset):
+        # Copy data from a CSV row to a dict/ostruct
+
+        filename, year, title, authors, url = dataset
+        if year:    ostruct['year']    = year
+        if title:   ostruct['title']   = title
+        if authors: ostruct['authors'] = authors
+        if url:     ostruct['url']     = url
+
+    def _fill_row_with_ostruct(self, row, update_dict):
+        # Copy data from a dict/ostruct into a CSV row
+        for key, value in update_dict.items():
+            if key == "year":
+                row[1] = value
+            elif key == "title":
+                row[2] = value
+            elif key == "authors":
+                row[3] = value
+            elif key == "url":
+                row[4] = value
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 0edb9c8d..5eabe2c6 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -248,16 +248,38 @@ class BrowserColumn(Pager):
             else:
                 tagged_marker = " "
 
+            # Extract linemode-related information from the drawn object
+            paperinfo = None
+            use_linemode = drawn._linemode
+            if use_linemode == "papertitle":
+                paperinfo = self.fm.papermanager.get_paper_info(drawn.path)
+                if not paperinfo.title:
+                    use_linemode = "filename"
+
             key = (self.wid, selected_i == i, drawn.marked, self.main_column,
                     drawn.path in copied, tagged_marker, drawn.infostring,
-                    drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut)
+                    drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut,
+                    use_linemode)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
                 self.color_reset()
                 continue
 
-            text = drawn.basename
+
+            # Deal with the line mode
+            if use_linemode == "papertitle":
+                if paperinfo.year:
+                    text = "%s - %s" % (paperinfo.year, paperinfo.title)
+                else:
+                    text = paperinfo.title
+            if use_linemode == "filename":
+                text = drawn.drawn_basename
+            elif use_linemode == "permissions":
+                text = "%s %s %s %s" % (drawn.get_permission_string(),
+                        drawn.user, drawn.group, drawn.drawn_basename)
+
+
             if drawn.marked and (self.main_column or \
                     self.settings.display_tags_in_all_columns):
                 text = " " + text
@@ -285,11 +307,21 @@ class BrowserColumn(Pager):
                 space -= vcsstringlen
 
             # info string
-            infostring = self._draw_infostring_display(drawn, space)
-            infostringlen = self._total_len(infostring)
-            if space - infostringlen > 2:
-                predisplay_right = infostring + predisplay_right
-                space -= infostringlen
+            infostring = []
+            infostringlen = 0
+            if use_linemode == "filename":
+                infostring = self._draw_infostring_display(drawn, space)
+            elif use_linemode == "papertitle":
+                if paperinfo and paperinfo.authors:
+                    authorstring = paperinfo.authors
+                    if ',' in authorstring:
+                        authorstring = authorstring[0:authorstring.find(",")]
+                    infostring.append([" " + authorstring + " ", ["infostring"]])
+            if infostring:
+                infostringlen = self._total_len(infostring)
+                if space - infostringlen > 2:
+                    predisplay_right = infostring + predisplay_right
+                    space -= infostringlen
 
             textstring = self._draw_text_display(text, space)
             textstringlen = self._total_len(textstring)
diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py
index fa10a744..25edd5e4 100644
--- a/ranger/gui/widgets/titlebar.py
+++ b/ranger/gui/widgets/titlebar.py
@@ -117,7 +117,7 @@ class TitleBar(Widget):
 
         if self.fm.thisfile is not None and \
                 self.settings.show_selection_in_titlebar:
-            bar.add(self.fm.thisfile.basename, 'file')
+            bar.add(self.fm.thisfile.drawn_basename, 'file')
 
     def _get_right_part(self, bar):
         # TODO: fix that pressed keys are cut off when chaining CTRL keys