diff options
-rw-r--r-- | doc/ranger.1 | 35 | ||||
-rw-r--r-- | doc/ranger.pod | 52 | ||||
-rwxr-xr-x | ranger/config/commands.py | 55 | ||||
-rw-r--r-- | ranger/config/rc.conf | 14 | ||||
-rw-r--r-- | ranger/container/directory.py | 3 | ||||
-rw-r--r-- | ranger/core/filter_stack.py | 134 |
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] |