From bf3456ef4e3744725b198a8540ffa8b9620fb58c Mon Sep 17 00:00:00 2001 From: Wojciech Siewierski Date: Wed, 4 Jul 2018 21:58:29 +0200 Subject: Implement the filter stack Inspired by https://github.com/Fuco1/dired-hacks#dired-filter --- ranger/config/commands.py | 48 ++++++++++++++++++ ranger/config/rc.conf | 12 +++++ ranger/container/directory.py | 3 ++ ranger/core/filter_stack.py | 114 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 ranger/core/filter_stack.py 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 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 ` enter_bookmark %any map ' 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 , 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 "".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 "".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 "".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 "".format(str(self.subfilter)) + + def decompose(self): + return [self.subfilter] -- cgit 1.4.1-2-gfad0