about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/ranger.135
-rw-r--r--doc/ranger.pod52
-rwxr-xr-xranger/config/commands.py55
-rw-r--r--ranger/config/rc.conf14
-rw-r--r--ranger/container/directory.py3
-rw-r--r--ranger/core/filter_stack.py134
6 files changed, 293 insertions, 0 deletions
diff --git a/doc/ranger.1 b/doc/ranger.1
index e141011f..060686b8 100644
--- a/doc/ranger.1
+++ b/doc/ranger.1
@@ -657,6 +657,41 @@ Close the current tab.  The last tab cannot be closed this way.
 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.
+.IP ".n" 14
+.IX Item ".n"
+Apply a new filename filter.
+.IP ".d" 14
+.IX Item ".d"
+Apply the typefilter \*(L"directory\*(R".
+.IP ".f" 14
+.IX Item ".f"
+Apply the typefilter \*(L"file\*(R".
+.IP ".l" 14
+.IX Item ".l"
+Apply the typefilter \*(L"symlink\*(R".
+.IP ".|" 14
+Combine the two topmost filters from the filter stack in the \*(L"\s-1OR\*(R"\s0
+relationship, instead of the \*(L"\s-1AND\*(R"\s0 used implicitly.
+.IP ".&" 14
+Explicitly combine the two topmost filters in the \*(L"\s-1AND\*(R"\s0
+relationship. Usually not needed though might be useful in more
+complicated scenarios.
+.IP ".!" 14
+Negate the topmost filter.
+.IP ".r" 14
+.IX Item ".r"
+Rotate the filter stack by N elements. Just confirm with enter to
+rotate by 1, i.e. move the topmost element to the bottom of the stack.
+.IP ".c" 14
+.IX Item ".c"
+Clear the filter stack.
+.IP ".*" 14
+Decompose the topmost filter combinator (e.g. \f(CW\*(C`.!\*(C'\fR, \f(CW\*(C`.|\*(C'\fR).
+.IP ".p" 14
+.IX Item ".p"
+Pop the topmost filter from the filter stack.
+.IP ".." 14
+Show the current filter stack state.
 .SS "READLINE-LIKE \s-1BINDINGS IN THE CONSOLE\s0"
 .IX Subsection "READLINE-LIKE BINDINGS IN THE CONSOLE"
 .IP "^B, ^F" 14
diff --git a/doc/ranger.pod b/doc/ranger.pod
index 3062fef5..48e6a41e 100644
--- a/doc/ranger.pod
+++ b/doc/ranger.pod
@@ -620,6 +620,58 @@ 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.
 
+=item .n
+
+Apply a new filename filter.
+
+=item .d
+
+Apply the typefilter "directory".
+
+=item .f
+
+Apply the typefilter "file".
+
+=item .l
+
+Apply the typefilter "symlink".
+
+=item .|
+
+Combine the two topmost filters from the filter stack in the "OR"
+relationship, instead of the "AND" used implicitly.
+
+=item .&
+
+Explicitly combine the two topmost filters in the "AND"
+relationship. Usually not needed though might be useful in more
+complicated scenarios.
+
+=item .!
+
+Negate the topmost filter.
+
+=item .r
+
+Rotate the filter stack by N elements. Just confirm with enter to
+rotate by 1, i.e. move the topmost element to the bottom of the stack.
+
+=item .c
+
+Clear the filter stack.
+
+=item .*
+
+Decompose the topmost filter combinator (e.g. C<.!>, C<.|>).
+
+=item .p
+
+Pop the topmost filter from the filter stack.
+
+=item ..
+
+Show the current filter stack state.
+
 =back
 
 =head2 READLINE-LIKE BINDINGS IN THE CONSOLE
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 7f5fc764..c136db56 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -1545,6 +1545,61 @@ class filter_inode_type(Command):
         self.fm.thisdir.refilter()
 
 
+class filter_stack(Command):
+    """
+    :filter_stack ...
+
+    Manages the filter stack.
+
+        filter_stack add FILTER_TYPE ARGS...
+        filter_stack pop
+        filter_stack decompose
+        filter_stack rotate [N=1]
+        filter_stack clear
+        filter_stack show
+    """
+    def execute(self):
+        from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS
+
+        subcommand = self.arg(1)
+
+        if subcommand == "add":
+            try:
+                self.fm.thisdir.filter_stack.append(
+                    SIMPLE_FILTERS[self.arg(2)](self.rest(3))
+                )
+            except KeyError:
+                FILTER_COMBINATORS[self.arg(2)](self.fm.thisdir.filter_stack)
+        elif subcommand == "pop":
+            self.fm.thisdir.filter_stack.pop()
+        elif subcommand == "decompose":
+            inner_filters = self.fm.thisdir.filter_stack.pop().decompose()
+            if inner_filters:
+                self.fm.thisdir.filter_stack.extend(inner_filters)
+        elif subcommand == "clear":
+            self.fm.thisdir.filter_stack = []
+        elif subcommand == "rotate":
+            rotate_by = int(self.arg(2) or 1)
+            self.fm.thisdir.filter_stack = (
+                self.fm.thisdir.filter_stack[-rotate_by:]
+                + self.fm.thisdir.filter_stack[:-rotate_by]
+            )
+        elif subcommand == "show":
+            stack = map(str, self.fm.thisdir.filter_stack)
+            pager = self.fm.ui.open_pager()
+            pager.set_source(["Filter stack: "] + stack)
+            pager.move(to=100, percentage=True)
+            return
+        else:
+            self.fm.notify(
+                "Unknown subcommand: {}".format(subcommand),
+                bad=True
+            )
+            return
+
+        self.fm.thisdir.refilter()
+
+
 class grep(Command):
     """:grep <string>
 
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 62331e22..b112dc2c 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -557,6 +557,20 @@ map zv    set use_preview_script!
 map zf    console filter%space
 copymap zf zz
 
+# Filter stack
+map .n console filter_stack add name%space
+map .d filter_stack add type d
+map .f filter_stack add type f
+map .l filter_stack add type l
+map .| filter_stack add or
+map .& filter_stack add and
+map .! filter_stack add not
+map .r console filter_stack rotate
+map .c filter_stack clear
+map .* filter_stack decompose
+map .p filter_stack pop
+map .. filter_stack show
+
 # Bookmarks
 map `<any>  enter_bookmark %any
 map '<any>  enter_bookmark %any
diff --git a/ranger/container/directory.py b/ranger/container/directory.py
index e281e6c9..81dabb24 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -155,6 +155,8 @@ class Directory(  # pylint: disable=too-many-instance-attributes,too-many-public
 
         self.marked_items = []
 
+        self.filter_stack = []
+
         self._signal_functions = []
         func = self.signal_function_factory(self.sort)
         self._signal_functions += [func]
@@ -297,6 +299,7 @@ class Directory(  # pylint: disable=too-many-instance-attributes,too-many-public
         if self.temporary_filter:
             temporary_filter_search = self.temporary_filter.search
             filters.append(lambda fobj: temporary_filter_search(fobj.basename))
+        filters.extend(self.filter_stack)
 
         self.files = [f for f in self.files_all if accept_file(f, filters)]
 
diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py
new file mode 100644
index 00000000..ff9f4080
--- /dev/null
+++ b/ranger/core/filter_stack.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+# Author: Wojciech Siewierski <wojciech.siewierski@onet.pl>, 2018
+
+from __future__ import (absolute_import, division, print_function)
+
+import re
+
+from ranger.container.directory import accept_file, InodeFilterConstants
+
+# pylint: disable=too-few-public-methods
+
+
+class BaseFilter(object):
+    def decompose(self):
+        return [self]
+
+
+SIMPLE_FILTERS = {}
+FILTER_COMBINATORS = {}
+
+
+def stack_filter(filter_name):
+    def decorator(cls):
+        SIMPLE_FILTERS[filter_name] = cls
+        return cls
+    return decorator
+
+
+def filter_combinator(combinator_name):
+    def decorator(cls):
+        FILTER_COMBINATORS[combinator_name] = cls
+        return cls
+    return decorator
+
+
+@stack_filter("name")
+class NameFilter(BaseFilter):
+    def __init__(self, pattern):
+        self.pattern = pattern
+        self.regex = re.compile(pattern)
+
+    def __call__(self, fobj):
+        return self.regex.search(fobj.relative_path)
+
+    def __str__(self):
+        return "<Filter: name =~ /{}/>".format(self.pattern)
+
+
+@stack_filter("type")
+class TypeFilter(BaseFilter):
+    type_to_function = {
+        InodeFilterConstants.DIRS:
+        (lambda fobj: fobj.is_directory),
+        InodeFilterConstants.FILES:
+        (lambda fobj: fobj.is_file and not fobj.is_link),
+        InodeFilterConstants.LINKS:
+        (lambda fobj: fobj.is_link),
+    }
+
+    def __init__(self, filetype):
+        if filetype not in self.type_to_function:
+            raise KeyError(filetype)
+        self.filetype = filetype
+
+    def __call__(self, fobj):
+        return self.type_to_function[self.filetype](fobj)
+
+    def __str__(self):
+        return "<Filter: type == '{}'>".format(self.filetype)
+
+
+@filter_combinator("or")
+class OrFilter(BaseFilter):
+    def __init__(self, stack):
+        self.subfilters = [stack[-2], stack[-1]]
+
+        stack.pop()
+        stack.pop()
+
+        stack.append(self)
+
+    def __call__(self, fobj):
+        # Turn logical AND (accept_file()) into a logical OR with the
+        # De Morgan's laws.
+        return not accept_file(
+            fobj,
+            ((lambda x, f=filt: not f(x))
+             for filt
+             in self.subfilters),
+        )
+
+    def __str__(self):
+        return "<Filter: {}>".format(" or ".join(map(str, self.subfilters)))
+
+    def decompose(self):
+        return self.subfilters
+
+
+@filter_combinator("and")
+class AndFilter(BaseFilter):
+    def __init__(self, stack):
+        self.subfilters = [stack[-2], stack[-1]]
+
+        stack.pop()
+        stack.pop()
+
+        stack.append(self)
+
+    def __call__(self, fobj):
+        return accept_file(fobj, self.subfilters)
+
+    def __str__(self):
+        return "<Filter: {}>".format(" and ".join(map(str, self.subfilters)))
+
+    def decompose(self):
+        return self.subfilters
+
+
+@filter_combinator("not")
+class NotFilter(BaseFilter):
+    def __init__(self, stack):
+        self.subfilter = stack.pop()
+        stack.append(self)
+
+    def __call__(self, fobj):
+        return not self.subfilter(fobj)
+
+    def __str__(self):
+        return "<Filter: not {}>".format(str(self.subfilter))
+
+    def decompose(self):
+        return [self.subfilter]