summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/ranger.161
-rw-r--r--doc/ranger.pod66
-rw-r--r--ranger/config/commands.py122
-rw-r--r--ranger/config/rc.conf10
-rw-r--r--ranger/container/directory.py4
-rw-r--r--ranger/container/file.py2
-rw-r--r--ranger/container/fsobject.py38
-rw-r--r--ranger/container/settings.py1
-rw-r--r--ranger/core/actions.py47
-rw-r--r--ranger/core/fm.py6
-rw-r--r--ranger/ext/papermanager.py171
-rw-r--r--ranger/gui/widgets/browsercolumn.py46
12 files changed, 566 insertions, 8 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 eb13d2b8..c556c63c 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,6 +825,7 @@ 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
@@ -820,12 +833,18 @@ ranger.  For your convenience, this is a list of the "public" commands including
  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 +947,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
@@ -974,6 +1008,15 @@ values -2 and less are invalid.
 
 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
@@ -1016,6 +1059,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 b0089e59..4e82fb0f 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
 
@@ -1284,3 +1321,88 @@ class flat(Command):
         self.fm.thisdir.flat = level
         self.fm.thisdir.load_content()
 
+
+# Papermanager commands
+# --------------------------------
+class paper(Command):
+    """
+    :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".
+    """
+    _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
+
+    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>
+
+    Tells the paper manager to set/update the title of the current file
+    """
+    _key = "title"
+
+    def execute(self):
+        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 2f1e5e11..7bef379d 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -583,6 +583,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 ef1938ff..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
@@ -80,6 +85,8 @@ class FileSystemObject(FileManagerAware, SettingsAware):
 
     basename_is_rel_to = None
 
+    _linemode = DEFAULT_LINEMODE
+
     def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel_to=None):
         if not path_is_abs:
             path = abspath(path)
@@ -103,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)
 
@@ -131,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("
@@ -375,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 2a9ce315..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
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 55601130..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.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)