summary refs log tree commit diff stats
path: root/ranger
diff options
context:
space:
mode:
authorhut <hut@lepus.uberspace.de>2014-12-05 01:46:11 +0100
committerhut <hut@lepus.uberspace.de>2014-12-05 01:46:11 +0100
commit807402c260afede5822c6c5edcf6b9f40d11ef86 (patch)
treef4b372a378007f46a6a94598395c70b87dbc4741 /ranger
parent5c97716aa0e260ec2b0cd6cffee1ea74fd9ff8f8 (diff)
parent8e0e465774d541181f27cd9e92a79ed01c84d115 (diff)
downloadranger-807402c260afede5822c6c5edcf6b9f40d11ef86.tar.gz
Merge branch 'linemode'
Diffstat (limited to 'ranger')
-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
10 files changed, 440 insertions, 7 deletions
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 953e2cc3..49d8f213 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
 
@@ -1281,3 +1318,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 c6987a76..c331053a 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -567,6 +567,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..59e23f6b 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 = False
 
+    _linemode = DEFAULT_LINEMODE
+
     def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel=False):
         if not path_is_abs:
             path = abspath(path)
@@ -101,6 +108,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)
 
@@ -129,6 +151,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 +408,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..3497f20c 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 0edb9c8d..b2159339 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.basename
+            elif use_linemode == "permissions":
+                text = "%s %s %s %s" % (drawn.get_permission_string(),
+                        drawn.user, drawn.group, 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)