summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/ranger.pod3
-rw-r--r--ranger/api/__init__.py8
-rw-r--r--ranger/config/commands.py41
-rw-r--r--ranger/config/rc.conf8
-rw-r--r--ranger/container/directory.py2
-rw-r--r--ranger/container/fsobject.py18
-rw-r--r--ranger/core/actions.py56
-rw-r--r--ranger/core/linemode.py82
-rw-r--r--ranger/gui/widgets/browsercolumn.py42
-rw-r--r--ranger/gui/widgets/titlebar.py2
10 files changed, 165 insertions, 97 deletions
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 97a324a8..336a886c 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -1051,6 +1051,9 @@ Sets the linemode of all files in the current directory.  The linemode may be:
      available, fall back to the "filename" linemode if no
      metadata was found.  See :meta command.
 
+The custom linemodes may be added by subclassing the I<LinemodeBase> class.
+See the I<ranger.core.linemode> module for some examples.
+
 =item load_copy_buffer
 
 Load the copy buffer from F<~/.config/ranger/copy_buffer>.  This can be used to
diff --git a/ranger/api/__init__.py b/ranger/api/__init__.py
index a50f706f..572cf1a4 100644
--- a/ranger/api/__init__.py
+++ b/ranger/api/__init__.py
@@ -26,3 +26,11 @@ def hook_ready(fm):
     This hook is executed after the user interface is initialized.  You should
     NOT print anything to stdout anymore from here on.  Use fm.notify instead.
     """
+
+from ranger.core.linemode import LinemodeBase
+
+def register_linemode(linemode_class):
+    """Add a custom linemode class.  See ranger.core.linemode"""
+    from ranger.container.fsobject import FileSystemObject
+    FileSystemObject.linemode_dict[linemode_class.name] = linemode_class()
+    return linemode_class
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 55f6442b..b47cb6ad 100644
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -390,7 +390,7 @@ class setintag(setlocal):
 class default_linemode(Command):
     def execute(self):
         import re
-        from ranger.container.fsobject import POSSIBLE_LINEMODES
+        from ranger.container.fsobject import FileSystemObject
 
         if len(self.args) < 2:
             self.fm.notify("Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
@@ -410,9 +410,9 @@ class default_linemode(Command):
 
         # Extract and validate the line mode from the command line
         linemode = self.rest(1)
-        if linemode not in POSSIBLE_LINEMODES:
+        if linemode not in FileSystemObject.linemode_dict:
             self.fm.notify("Invalid linemode: %s; should be %s" %
-                    (linemode, "/".join(POSSIBLE_LINEMODES)), bad=True)
+                    (linemode, "/".join(FileSystemObject.linemode_dict)), bad=True)
 
         # Add the prepared entry to the fm.default_linemodes
         entry = [method, argument, linemode]
@@ -423,6 +423,11 @@ class default_linemode(Command):
             for col in self.fm.ui.browser.columns:
                 col.need_redraw = True
 
+    def tab(self):
+        mode = self.arg(1)
+        return (self.arg(0) + " " + linemode
+                for linemode in self.fm.thisfile.linemode_dict.keys())
+
 
 class quit(Command):
     """:quit
@@ -793,14 +798,12 @@ class bulkrename(Command):
     def execute(self):
         import sys
         import tempfile
-        from os.path import relpath
         from ranger.container.file import File
         from ranger.ext.shell_escape import shell_escape as esc
         py3 = sys.version_info[0] >= 3
 
         # Create and edit the file list
-        filenames = [relpath(f.path, start=self.fm.thisdir.path)
-                     for f in self.fm.thistab.get_selection()]
+        filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
         listfile = tempfile.NamedTemporaryFile(delete=False)
         listpath = listfile.name
 
@@ -1418,3 +1421,29 @@ class meta(prompt_metadata):
         metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
         if key in metadata and metadata[key]:
             return self.arg(0) + " " + metadata[key]
+
+class linemode(default_linemode):
+    """
+    :linemode <mode>
+
+    Change what is displayed as a filename.
+
+    - "mode" may be any of the defined linemodes (see: ranger.core.linemode).
+      "normal" is mapped to "filename".
+    """
+
+    def execute(self):
+        mode = self.arg(1)
+
+        if mode == "normal":
+            mode = DEFAULT_LINEMODE
+
+        if mode not in self.fm.thisfile.linemode_dict:
+            self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
+            return
+
+        self.fm.thisdir._set_linemode_of_children(mode)
+
+        # Ask the browsercolumns to redraw
+        for col in self.fm.ui.browser.columns:
+            col.need_redraw = True
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index e55ab2be..ca67c389 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -29,7 +29,7 @@ set hidden_filter ^\.|\.(?:pyc|pyo|bak|swp)$|^lost\+found$|^__(py)?cache__$
 set show_hidden false
 
 # Ask for a confirmation when running the "delete" command?
-# Valid values are "always" (default), "never", "multiple"
+# Valid values are "always", "never", "multiple" (default)
 # With "multiple", ranger will ask only if you delete multiple files at once.
 set confirm_on_delete multiple
 
@@ -81,7 +81,7 @@ set unicode_ellipsis false
 set show_hidden_bookmarks true
 
 # Which colorscheme to use?  These colorschemes are available by default:
-# default, jungle, snow
+# default, jungle, snow, solarized
 set colorscheme default
 
 # Preview files on the rightmost column?
@@ -284,7 +284,7 @@ map <END>      move to=-1
 map <PAGEDOWN> move down=1   pages=True
 map <PAGEUP>   move up=1     pages=True
 map <CR>       move right=1
-map <DELETE>   console delete
+#map <DELETE>   console delete
 map <INSERT>   console touch 
 
 # VIM-like
@@ -348,6 +348,8 @@ map pL paste_symlink relative=True
 map phl paste_hardlink
 map pht paste_hardlinked_subtree
 
+map dD console delete
+
 map dd cut
 map ud uncut
 map da cut mode=add
diff --git a/ranger/container/directory.py b/ranger/container/directory.py
index 71eda763..256dd729 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -21,7 +21,7 @@ from ranger.container.settings import LocalSettings
 
 def sort_by_basename(path):
     """returns path.basename (for sorting)"""
-    return path.drawn_basename
+    return path.relative_path
 
 def sort_by_basename_icase(path):
     """returns case-insensitive path.basename (for sorting)"""
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index 08161c3c..6cdaa727 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -12,14 +12,12 @@ DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits',
 
 BAD_INFO = '?'
 
-POSSIBLE_LINEMODES = ("filename", "metatitle", "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.linemode import *
 from ranger.core.shared import FileManagerAware, SettingsAware
 from ranger.ext.shell_escape import shell_escape
 from ranger.ext.spawn import spawn
@@ -86,6 +84,10 @@ class FileSystemObject(FileManagerAware, SettingsAware):
     basename_is_rel_to = None
 
     _linemode = DEFAULT_LINEMODE
+    linemode_dict = dict(
+        (linemode.name, linemode()) for linemode in
+        [DefaultLinemode, TitleLinemode, PermissionsLinemode]
+    )
 
     def __init__(self, path, preload=None, path_is_abs=False, basename_is_rel_to=None):
         if not path_is_abs:
@@ -94,11 +96,11 @@ class FileSystemObject(FileManagerAware, SettingsAware):
         self.basename_is_rel_to = basename_is_rel_to
         if basename_is_rel_to == None:
             self.basename = basename(path)
-            self.drawn_basename = self.basename
+            self.relative_path = self.basename
         else:
             self.basename = basename(path)
-            self.drawn_basename = relpath(path, basename_is_rel_to)
-        self.basename_lower = self.drawn_basename.lower()
+            self.relative_path = relpath(path, basename_is_rel_to)
+        self.basename_lower = self.relative_path.lower()
         self.extension = splitext(self.basename)[1].lstrip(extsep) or None
         self.dirname = dirname(path)
         self.preload = preload
@@ -112,7 +114,7 @@ class FileSystemObject(FileManagerAware, SettingsAware):
 
         # Set the line mode from fm.default_linemodes
         for method, argument, linemode in self.fm.default_linemodes:
-            if linemode in POSSIBLE_LINEMODES:
+            if linemode in self.linemode_dict:
                 if method == "always":
                     self._linemode = linemode
                     break
@@ -142,7 +144,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.drawn_basename))]
+            enumerate(_extract_number_re.split(self.relative_path))]
 
     @lazy_property
     def basename_natural_lower(self):
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 63c91bcc..d1e383ba 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -27,7 +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
+from ranger.core.linemode import DEFAULT_LINEMODE
 
 MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
 
@@ -154,52 +154,6 @@ 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", "metatitle", the mode
-          "normal" is mapped to "filename".
-        - "directory" specifies the directory. None means the current directory
-        - "depth" specifies the recursion depth
-        """
-
-        if mode == "normal":
-            mode = DEFAULT_LINEMODE
-
-        if mode not in POSSIBLE_LINEMODES:
-            self.notify("Unhandled linemode: `%s'" % mode, bad=True)
-            return
-
-        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')
@@ -258,12 +212,12 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
         macros['rangerdir'] = ranger.RANGERDIR
 
         if self.fm.thisfile:
-            macros['f'] = self.fm.thisfile.basename
+            macros['f'] = self.fm.thisfile.relative_path
         else:
             macros['f'] = MACRO_FAIL
 
         if self.fm.thistab.get_selection:
-            macros['s'] = [fl.basename for fl in self.fm.thistab.get_selection()]
+            macros['s'] = [fl.relative_path for fl in self.fm.thistab.get_selection()]
         else:
             macros['s'] = MACRO_FAIL
 
@@ -273,7 +227,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
             macros['c'] = MACRO_FAIL
 
         if self.fm.thisdir.files:
-            macros['t'] = [fl.basename for fl in self.fm.thisdir.files
+            macros['t'] = [fl.relative_path for fl in self.fm.thisdir.files
                     if fl.realpath in (self.fm.tags or [])]
         else:
             macros['t'] = MACRO_FAIL
@@ -1290,7 +1244,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware):
 
     def mkdir(self, name):
         try:
-            os.mkdirs(os.path.join(self.thisdir.path, name))
+            os.makedirs(os.path.join(self.thisdir.path, name))
         except OSError as err:
             self.notify(err)
 
diff --git a/ranger/core/linemode.py b/ranger/core/linemode.py
new file mode 100644
index 00000000..166829cf
--- /dev/null
+++ b/ranger/core/linemode.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+
+from abc import *
+
+DEFAULT_LINEMODE = "filename"
+
+
+class LinemodeBase(object):
+    """Supplies the file line contents for BrowserColumn.
+
+    Attributes:
+        name (required!) - Name by which the linemode is referred to by the user
+
+        uses_metadata - True if metadata should to be loaded for this linemode
+
+        required_metadata -
+            If any of these metadata fields are absent, fall back to
+            the default linemode
+    """
+    __metaclass__ = ABCMeta
+
+    uses_metadata = False
+    required_metadata = []
+
+    name = abstractproperty()
+
+    @abstractmethod
+    def filetitle(self, file, metadata):
+        """The left-aligned part of the line."""
+        raise NotImplementedError
+
+    def infostring(self, file, metadata):
+        """The right-aligned part of the line.
+
+        If `NotImplementedError' is raised (e.g. this method is just
+        not implemented in the actual linemode), the caller should
+        provide its own implementation, which in this case means
+        displaying the hardlink count of the directories. Useful
+        because only the caller (BrowserColumn) possesses the data
+        necessary to display that information.
+
+        """
+        raise NotImplementedError
+
+
+class DefaultLinemode(LinemodeBase):
+    name = "filename"
+
+    def filetitle(self, file, metadata):
+        return file.relative_path
+
+
+class TitleLinemode(LinemodeBase):
+    name = "metatitle"
+    uses_metadata = True
+    required_metadata = ["title"]
+
+    def filetitle(self, file, metadata):
+        name = metadata.title or file.basename
+        if metadata.year:
+            return "%s - %s" % (metadata.year, name)
+        else:
+            return name
+
+    def infostring(self, file, metadata):
+        if metadata.authors:
+            authorstring = metadata.authors
+            if ',' in authorstring:
+                authorstring = authorstring[0:authorstring.find(",")]
+            return authorstring
+        return ""
+
+
+class PermissionsLinemode(LinemodeBase):
+    name = "permissions"
+
+    def filetitle(self, file, metadata):
+        return "%s %s %s %s" % (file.get_permission_string(),
+                file.user, file.group, file.relative_path)
+
+    def infostring(self, file, metadata):
+        return ""
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 564e9a5f..0340b565 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -12,6 +12,8 @@ from . import Widget
 from .pager import Pager
 from ranger.ext.widestring import WideString
 
+from ranger.core import linemode
+
 from ranger.gui.color import *
 
 class BrowserColumn(Pager):
@@ -250,38 +252,25 @@ class BrowserColumn(Pager):
 
             # Extract linemode-related information from the drawn object
             metadata = None
-            use_linemode = drawn._linemode
-            if use_linemode == "metatitle":
+            current_linemode = drawn.linemode_dict[drawn._linemode]
+            if current_linemode.uses_metadata:
                 metadata = self.fm.metadata.get_metadata(drawn.path)
-                if not metadata.title:
-                    use_linemode = "filename"
+                if not all(getattr(metadata, tag)
+                           for tag in current_linemode.required_metadata):
+                    current_linemode = drawn.linemode_dict[linemode.DEFAULT_LINEMODE]
 
             metakey = hash(repr(sorted(metadata.items()))) if metadata else 0
             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,
-                    use_linemode, metakey)
+                    current_linemode.name, metakey)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
                 self.color_reset()
                 continue
 
-
-            # Deal with the line mode
-            text = ""
-            if use_linemode == "metatitle":
-                assert metadata.title, "Ensure that metadata.title is set!"
-                if metadata.year:
-                    text = "%s - %s" % (metadata.year, metadata.title)
-                else:
-                    text = metadata.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)
-
+            text = current_linemode.filetitle(drawn, metadata)
 
             if drawn.marked and (self.main_column or \
                     self.settings.display_tags_in_all_columns):
@@ -312,14 +301,13 @@ class BrowserColumn(Pager):
             # info string
             infostring = []
             infostringlen = 0
-            if use_linemode == "filename":
+            try:
+                infostringdata = current_linemode.infostring(drawn, metadata)
+                if infostringdata:
+                    infostring.append([" " + infostringdata + " ",
+                                       ["infostring"]])
+            except NotImplementedError:
                 infostring = self._draw_infostring_display(drawn, space)
-            elif use_linemode == "metatitle":
-                if metadata.authors:
-                    authorstring = metadata.authors
-                    if ',' in authorstring:
-                        authorstring = authorstring[0:authorstring.find(",")]
-                    infostring.append([" " + authorstring + " ", ["infostring"]])
             if infostring:
                 infostringlen = self._total_len(infostring)
                 if space - infostringlen > 2:
diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py
index 55cc25e6..52726b1c 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.drawn_basename, 'file')
+            bar.add(self.fm.thisfile.relative_path, 'file')
 
     def _get_right_part(self, bar):
         # TODO: fix that pressed keys are cut off when chaining CTRL keys