diff options
-rwxr-xr-x | ranger/config/commands.py | 48 | ||||
-rw-r--r-- | ranger/config/rc.conf | 12 | ||||
-rw-r--r-- | ranger/container/directory.py | 3 | ||||
-rw-r--r-- | ranger/core/filter_stack.py | 114 |
4 files changed, 177 insertions, 0 deletions
diff --git a/ranger/config/commands.py b/ranger/config/commands.py index 7f5fc764..35aa3ffb 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1545,6 +1545,54 @@ 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 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 == "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..4d8f1f54 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -557,6 +557,18 @@ 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 not +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..f5b00014 --- /dev/null +++ b/ranger/core/filter_stack.py @@ -0,0 +1,114 @@ +# -*- 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("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] |