From 73b98e7d74411a525d82051929ef72dce0e03c1e Mon Sep 17 00:00:00 2001 From: arkedos Date: Sat, 7 Dec 2019 22:02:24 +0100 Subject: Added a filter for unique files by md5 hash --- ranger/core/filter_stack.py | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index 2ca2b1c5..4c00e09b 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -9,6 +9,7 @@ import re import mimetypes from ranger.container.directory import accept_file, InodeFilterConstants +from ranger.core.shared import FileManagerAware # pylint: disable=too-few-public-methods @@ -65,6 +66,55 @@ class MimeFilter(BaseFilter): return "".format(self.pattern) +@stack_filter("hash") +class HashFilter(BaseFilter): + + def __init__(self, *args): + self.args = list(*args) + self.hasher = None + self.hashes = {} + self.duplicates = {} + + def __call__(self, fobj): + file_paths = [item.basename for item in + FileManagerAware.fm.thisdir.files_all if item.is_file] + if not self.args: + self.duplicates = self.get_duplicates(file_paths) + return fobj.basename not in self.duplicates + elif self.args[0].strip() == 'd': + self.duplicates = self.get_duplicates(file_paths) + return fobj.basename in self.duplicates + # return nothing if wrong args are passed + return None + + def __str__(self): + return "".format(self.args) + + def get_hash(self, file_basename): + import hashlib + self.hasher = hashlib.md5() + data = open(file_basename, 'rb') + buff = data.read() + self.hasher.update(buff) + data.close() + return self.hasher.hexdigest() + + def get_duplicates(self, file_paths): + for file_base in file_paths: + hash_value = self.get_hash(file_base) + self.hashes[file_base] = hash_value + + for key, value in self.hashes.items(): + for file_name, hash_value in self.hashes.items(): + # do nothing if it checking for the same files + if key == file_name: + pass + elif value == hash_value: + self.duplicates[key] = value + + return self.duplicates + + @stack_filter("type") class TypeFilter(BaseFilter): type_to_function = { -- cgit 1.4.1-2-gfad0 From ce0753b6a7eedb4ef616e9d9433bd73a0a617226 Mon Sep 17 00:00:00 2001 From: arkedos Date: Sun, 8 Dec 2019 21:55:39 +0100 Subject: Replacing md5 with blake2b --- ranger/core/filter_stack.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index 4c00e09b..0e0d4807 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -91,8 +91,13 @@ class HashFilter(BaseFilter): return "".format(self.args) def get_hash(self, file_basename): - import hashlib - self.hasher = hashlib.md5() + import sys + if sys.version_info.major < 3: + from hashlib import md5 + self.hasher = md5() + elif sys.version_info.major == 3: + import hashlib + self.hasher = hashlib.blake2b() data = open(file_basename, 'rb') buff = data.read() self.hasher.update(buff) -- cgit 1.4.1-2-gfad0 From 815e31aee01f813adf0516d82142ac011dc394e2 Mon Sep 17 00:00:00 2001 From: arkedos Date: Sun, 8 Dec 2019 22:14:19 +0100 Subject: Replacing md5 with sha256 --- ranger/core/filter_stack.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index 0e0d4807..5c1297e5 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -91,13 +91,8 @@ class HashFilter(BaseFilter): return "".format(self.args) def get_hash(self, file_basename): - import sys - if sys.version_info.major < 3: - from hashlib import md5 - self.hasher = md5() - elif sys.version_info.major == 3: - import hashlib - self.hasher = hashlib.blake2b() + from hashlib import sha256 + self.hasher = sha256() data = open(file_basename, 'rb') buff = data.read() self.hasher.update(buff) -- cgit 1.4.1-2-gfad0 From acd95a386383a7df2eb1d2ea7557f4a31efb8fcc Mon Sep 17 00:00:00 2001 From: toonn Date: Mon, 23 Dec 2019 20:34:02 +0100 Subject: Change hash filter to match on hash The `hash` filter for `filter_stack` requires a path as argument, defaulting to the currently selected file. It filters out any files or directories with a different hash. --- ranger/config/rc.conf | 1 + ranger/core/filter_stack.py | 82 ++++++++++++++++++++++----------------------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index e557d4de..666a99cf 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -595,6 +595,7 @@ copymap zf zz # Filter stack map .n console filter_stack add name%space map .m console filter_stack add mime%space +map .# console filter_stack add hash%space map .d filter_stack add type d map .f filter_stack add type f map .l filter_stack add type l diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index 5c1297e5..5d40825f 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -7,6 +7,15 @@ from __future__ import (absolute_import, division, print_function) import re import mimetypes +# pylint: disable=invalid-name +try: + from itertools import izip_longest + zip_longest = izip_longest +except ImportError: + from itertools import zip_longest +# pylint: enable=invalid-name +from os import listdir +from os.path import abspath, isdir from ranger.container.directory import accept_file, InodeFilterConstants from ranger.core.shared import FileManagerAware @@ -67,52 +76,43 @@ class MimeFilter(BaseFilter): @stack_filter("hash") -class HashFilter(BaseFilter): - - def __init__(self, *args): - self.args = list(*args) - self.hasher = None - self.hashes = {} - self.duplicates = {} - +class HashFilter(BaseFilter, FileManagerAware): + def __init__(self, filepath): + self.filepath = filepath if filepath else self.fm.thisfile.path + if not self.filepath: + self.fm.notify("Error: No file selected for hashing!", bad=True) + # TODO: Lazily generated list would be more efficient, a generator + # isn't enough because this object is reused for every fsobject + # in the current directory. + self.filehash = list(self.hash_chunks(abspath(self.filepath))) + + # pylint: disable=invalid-name def __call__(self, fobj): - file_paths = [item.basename for item in - FileManagerAware.fm.thisdir.files_all if item.is_file] - if not self.args: - self.duplicates = self.get_duplicates(file_paths) - return fobj.basename not in self.duplicates - elif self.args[0].strip() == 'd': - self.duplicates = self.get_duplicates(file_paths) - return fobj.basename in self.duplicates - # return nothing if wrong args are passed - return None + for (c1, c2) in zip_longest(self.filehash, + self.hash_chunks(fobj.path), + fillvalue=''): + if c1 != c2: + return False + return True def __str__(self): - return "".format(self.args) + return "".format(self.filepath) - def get_hash(self, file_basename): + def hash_chunks(self, filepath): from hashlib import sha256 - self.hasher = sha256() - data = open(file_basename, 'rb') - buff = data.read() - self.hasher.update(buff) - data.close() - return self.hasher.hexdigest() - - def get_duplicates(self, file_paths): - for file_base in file_paths: - hash_value = self.get_hash(file_base) - self.hashes[file_base] = hash_value - - for key, value in self.hashes.items(): - for file_name, hash_value in self.hashes.items(): - # do nothing if it checking for the same files - if key == file_name: - pass - elif value == hash_value: - self.duplicates[key] = value - - return self.duplicates + if isdir(filepath): + yield filepath + for fp in listdir(filepath): + self.hash_chunks(fp) + else: + with open(filepath, 'rb') as f: + h = sha256() + # Read the file in ~64KiB chunks (multiple of sha256's block + # size that works well enough with HDDs and SSDs) + for chunk in iter(lambda: f.read(h.block_size * 1024), b''): + h.update(chunk) + yield h.hexdigest() + # pylint: enable=invalid-name @stack_filter("type") -- cgit 1.4.1-2-gfad0 From e3fd4eeefd8e1c1bf559361b111bec2cc1c70d74 Mon Sep 17 00:00:00 2001 From: toonn Date: Mon, 23 Dec 2019 23:21:11 +0100 Subject: Extract hash_chunks to ranger.ext `hash_chunks` now returns a hash even for 0 byte files. --- ranger/core/filter_stack.py | 31 +++++++------------------------ ranger/ext/hash.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 ranger/ext/hash.py diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index 5d40825f..9b4d2779 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -14,11 +14,11 @@ try: except ImportError: from itertools import zip_longest # pylint: enable=invalid-name -from os import listdir -from os.path import abspath, isdir +from os.path import abspath from ranger.container.directory import accept_file, InodeFilterConstants from ranger.core.shared import FileManagerAware +from ranger.ext.hash import hash_chunks # pylint: disable=too-few-public-methods @@ -84,36 +84,19 @@ class HashFilter(BaseFilter, FileManagerAware): # TODO: Lazily generated list would be more efficient, a generator # isn't enough because this object is reused for every fsobject # in the current directory. - self.filehash = list(self.hash_chunks(abspath(self.filepath))) + self.filehash = list(hash_chunks(abspath(self.filepath))) - # pylint: disable=invalid-name def __call__(self, fobj): - for (c1, c2) in zip_longest(self.filehash, - self.hash_chunks(fobj.path), - fillvalue=''): - if c1 != c2: + for (chunk1, chunk2) in zip_longest(self.filehash, + hash_chunks(fobj.path), + fillvalue=''): + if chunk1 != chunk2: return False return True def __str__(self): return "".format(self.filepath) - def hash_chunks(self, filepath): - from hashlib import sha256 - if isdir(filepath): - yield filepath - for fp in listdir(filepath): - self.hash_chunks(fp) - else: - with open(filepath, 'rb') as f: - h = sha256() - # Read the file in ~64KiB chunks (multiple of sha256's block - # size that works well enough with HDDs and SSDs) - for chunk in iter(lambda: f.read(h.block_size * 1024), b''): - h.update(chunk) - yield h.hexdigest() - # pylint: enable=invalid-name - @stack_filter("type") class TypeFilter(BaseFilter): diff --git a/ranger/ext/hash.py b/ranger/ext/hash.py new file mode 100644 index 00000000..20059dbf --- /dev/null +++ b/ranger/ext/hash.py @@ -0,0 +1,29 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + +from __future__ import (absolute_import, division, print_function) + +from os import listdir +from os.path import getsize, isdir +from hashlib import sha256 + +# pylint: disable=invalid-name + + +def hash_chunks(filepath, h=None): + if not h: + h = sha256() + if isdir(filepath): + h.update(filepath) + yield h.hexdigest() + for fp in listdir(filepath): + hash_chunks(fp, h=h) + elif getsize(filepath) == 0: + yield h.hexdigest() + else: + with open(filepath, 'rb') as f: + # Read the file in ~64KiB chunks (multiple of sha256's block + # size that works well enough with HDDs and SSDs) + for chunk in iter(lambda: f.read(h.block_size * 1024), b''): + h.update(chunk) + yield h.hexdigest() -- cgit 1.4.1-2-gfad0 From ce3de17e70f9e6c0e5dc30c9879a5e8d43b19b9c Mon Sep 17 00:00:00 2001 From: toonn Date: Mon, 23 Dec 2019 23:23:38 +0100 Subject: Add duplicate filter to filter_stack --- ranger/core/filter_stack.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index 9b4d2779..a479b6ed 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -98,6 +98,47 @@ class HashFilter(BaseFilter, FileManagerAware): return "".format(self.filepath) +@stack_filter("duplicate") +class DuplicateFilter(BaseFilter, FileManagerAware): + def __init__(self, _): + self.duplicates = self.get_duplicates(self.fm.thisdir.files_all) + + def __call__(self, fobj): + return fobj in self.duplicates + + def __str__(self): + return "" + + def get_duplicates(self, fsobjects): + hashes = {} + for fobj in fsobjects: + chunks = hash_chunks(fobj.path) + chunk = next(chunks) + while chunk in hashes: + for dup in hashes[chunk]: + _, dup_chunks = dup + try: + hashes[next(dup_chunks)] = [dup] + hashes[chunk].remove(dup) + except StopIteration: + pass + try: + chunk = next(chunks) + except StopIteration: + hashes[chunk].append((fobj, chunks)) + break + else: + hashes[chunk] = [(fobj, chunks)] + + duplicates = set() + for dups in hashes.values(): + if len(dups) >= 2: + for (dup, _) in dups: + duplicates.add(dup) + + return duplicates + + @stack_filter("type") class TypeFilter(BaseFilter): type_to_function = { -- cgit 1.4.1-2-gfad0 From da29ef8d6ae6848a8c0c0df277a2da0de337b532 Mon Sep 17 00:00:00 2001 From: toonn Date: Mon, 23 Dec 2019 23:57:00 +0100 Subject: Add unique filter to filter_stack Extracted helper function to `group_by_hash` for both duplicate and unique filters. --- ranger/core/filter_stack.py | 83 +++++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index a479b6ed..ca8810e8 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -98,10 +98,42 @@ class HashFilter(BaseFilter, FileManagerAware): return "".format(self.filepath) +def group_by_hash(fsobjects): + hashes = {} + for fobj in fsobjects: + chunks = hash_chunks(fobj.path) + chunk = next(chunks) + while chunk in hashes: + for dup in hashes[chunk]: + _, dup_chunks = dup + try: + hashes[next(dup_chunks)] = [dup] + hashes[chunk].remove(dup) + except StopIteration: + pass + try: + chunk = next(chunks) + except StopIteration: + hashes[chunk].append((fobj, chunks)) + break + else: + hashes[chunk] = [(fobj, chunks)] + + groups = [] + for dups in hashes.values(): + group = [] + for (dup, _) in dups: + group.append(dup) + if group: + groups.append(group) + + return groups + + @stack_filter("duplicate") class DuplicateFilter(BaseFilter, FileManagerAware): def __init__(self, _): - self.duplicates = self.get_duplicates(self.fm.thisdir.files_all) + self.duplicates = self.get_duplicates() def __call__(self, fobj): return fobj in self.duplicates @@ -109,36 +141,35 @@ class DuplicateFilter(BaseFilter, FileManagerAware): def __str__(self): return "" - def get_duplicates(self, fsobjects): - hashes = {} - for fobj in fsobjects: - chunks = hash_chunks(fobj.path) - chunk = next(chunks) - while chunk in hashes: - for dup in hashes[chunk]: - _, dup_chunks = dup - try: - hashes[next(dup_chunks)] = [dup] - hashes[chunk].remove(dup) - except StopIteration: - pass - try: - chunk = next(chunks) - except StopIteration: - hashes[chunk].append((fobj, chunks)) - break - else: - hashes[chunk] = [(fobj, chunks)] - + def get_duplicates(self): duplicates = set() - for dups in hashes.values(): + for dups in group_by_hash(self.fm.thisdir.files_all): if len(dups) >= 2: - for (dup, _) in dups: - duplicates.add(dup) - + duplicates.update(dups) return duplicates +@stack_filter("unique") +class UniqueFilter(BaseFilter, FileManagerAware): + def __init__(self, _): + self.unique = self.get_unique() + + def __call__(self, fobj): + return fobj in self.unique + + def __str__(self): + return "" + + def get_unique(self): + unique = set() + for dups in group_by_hash(self.fm.thisdir.files_all): + try: + unique.add(min(dups, key=lambda fobj: fobj.stat.st_ctime)) + except ValueError: + pass + return unique + + @stack_filter("type") class TypeFilter(BaseFilter): type_to_function = { -- cgit 1.4.1-2-gfad0 From 5581c896a97ee7740dd1b142a053fc24681c1f72 Mon Sep 17 00:00:00 2001 From: toonn Date: Fri, 27 Dec 2019 18:15:16 +0100 Subject: Improve filter_stack documentation Add an entry about `filter_stack` to the man page, documenting the various subcommands and available filters. Add documentation about the new mappings. Add functionality to the `rotate` subcommand so you can pass the argument as a `quantifier`, i.e., a numeric prefix. Change the `.r` mapping to rely on this behavior accordingly, reducing the amount of unnecessary return pressing. --- doc/ranger.1 | 122 +++++++++++++++++++++++++++++++++++----- doc/ranger.pod | 140 +++++++++++++++++++++++++++++++++++++++++----- ranger/config/commands.py | 2 +- ranger/config/rc.conf | 11 ++-- 4 files changed, 240 insertions(+), 35 deletions(-) diff --git a/doc/ranger.1 b/doc/ranger.1 index 3521d762..54d35c8c 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35) +.\" Automatically generated by Pod::Man 4.11 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.9.2" "2019-10-02" "ranger manual" +.TH RANGER 1 "ranger-1.9.2" "2019-12-27" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -717,12 +717,6 @@ 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 ".m" 14 -.IX Item ".m" -Apply a new mimetype filter. .IP ".d" 14 .IX Item ".d" Apply the typefilter \*(L"directory\*(R". @@ -732,19 +726,32 @@ Apply the typefilter \*(L"file\*(R". .IP ".l" 14 .IX Item ".l" Apply the typefilter \*(L"symlink\*(R". +.IP ".m" 14 +.IX Item ".m" +Apply a new mimetype filter. +.IP ".n" 14 +.IX Item ".n" +Apply a new filename filter. +.IP ".#" 14 +Apply a new hash filter. +.IP ".""" 14 +Apply a new duplicate filter. +.IP ".'" 14 +Apply a new unique filter. .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. +Combine the two topmost filters from the filter stack in the \f(CW\*(C`OR\*(C'\fR +relationship, instead of the \f(CW\*(C`AND\*(C'\fR 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. +Explicitly combine the two topmost filters in the \f(CW\*(C`AND\*(C'\fR relationship. +Usually not needed because filters are implicitly in this relationship 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. +Rotate the filter stack by N elements. Where N is provided as a numeric prefix +like vim's \fIcount\fR and defaults to 1, i.e. move the topmost element to the +bottom of the stack. .IP ".c" 14 .IX Item ".c" Clear the filter stack. @@ -1326,6 +1333,91 @@ Displays only the files of specified inode type. To display only directories, use the 'd' parameter. To display only files, use the 'f' parameter. To display only links, use the 'l' parameter. Parameters can be combined. To remove this filter, use no parameter. +.IP "filter_stack [\fIcommand\fR [\fIargs\fR]]" 2 +.IX Item "filter_stack [command [args]]" +Manage the filter stack, adding, removing and manipulating filters. For +example, to show only duplicate files and symlinks: +.Sp +.Vb 5 +\& :filter_stack add type f +\& :filter_stack add duplicate +\& :filter_stack add and +\& :filter_stack add type l +\& :filter_stack add or +.Ve +.Sp +Or using the mapped keys: +.Sp +.Vb 1 +\& .f ." .& .l .| +.Ve +.Sp +Available subcommands: +.RS 2 +.IP "add \s-1FILTER_TYPE\s0 [\s-1ARGS...\s0]" 2 +.IX Item "add FILTER_TYPE [ARGS...]" +Add a new filter to the top of the filter stack. Each filter on the stack is +applied in turn, resulting in an implicit logical \f(CW\*(C`AND\*(C'\fR relation. The +following \f(CW\*(C`FILTER_TYPE\*(C'\fRs are available: +.RS 2 +.IP "duplicate" 2 +.IX Item "duplicate" +Filter files so only files that have duplicates in the same directory are +shown. Useful when cleaning up identical songs and memes that were saved using +distinct file names. +.IP "filename \s-1NAME\s0" 2 +.IX Item "filename NAME" +Filter files that contain \s-1NAME\s0 in the filename, regular expression syntax is +allowed. +.IP "hash \s-1PATH\s0" 2 +.IX Item "hash PATH" +Filter files so only files with the same hash as \s-1PATH\s0 are shown. +.IP "mimetype \s-1TYPE\s0" 2 +.IX Item "mimetype TYPE" +Filter files of a certain \s-1MIME\s0 type, regular expression syntax is allowed. +.IP "typefilter [d|f|l]" 2 +.IX Item "typefilter [d|f|l]" +Filter files of a certain type, \f(CW\*(C`d\*(C'\fR for directories, \f(CW\*(C`f\*(C'\fR for files and \f(CW\*(C`l\*(C'\fR +for symlinks. +.IP "unique" 2 +.IX Item "unique" +Filter files so only unique files and the oldest file of every set of +duplicates is shown. +.IP "and" 2 +.IX Item "and" +Explicitly combine the two topmost filters in the \*(L"\s-1AND\*(R"\s0 relationship. +Usually not needed because filters are implicitly in this relationship though +might be useful in more complicated scenarios. +.IP "not" 2 +.IX Item "not" +Negate the topmost filter. +.IP "or" 2 +.IX Item "or" +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. +.RE +.RS 2 +.RE +.IP "pop" 2 +.IX Item "pop" +Pop the topmost filter from the filter stack. +.IP "decompose" 2 +.IX Item "decompose" +Decompose the topmost filter combinator (e.g. \f(CW\*(C`.!\*(C'\fR, \f(CW\*(C`.|\*(C'\fR). +.IP "rotate [N=1]" 2 +.IX Item "rotate [N=1]" +Rotate the filter stack by N elements. Where N is passed as argument or as a +numeric prefix like vim's \fIcount\fR, default to 1, i.e. move the topmost element +to the bottom of the stack. +.IP "clear" 2 +.IX Item "clear" +Clear the filter stack. +.IP "show" 2 +.IX Item "show" +Show the current filter stack state. +.RE +.RS 2 +.RE .IP "find \fIpattern\fR" 2 .IX Item "find pattern" Search files in the current directory that contain the given (case-insensitive) diff --git a/doc/ranger.pod b/doc/ranger.pod index be964b37..ed3e98e1 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -671,14 +671,6 @@ 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 .m - -Apply a new mimetype filter. - =item .d Apply the typefilter "directory". @@ -691,16 +683,36 @@ Apply the typefilter "file". Apply the typefilter "symlink". +=item .m + +Apply a new mimetype filter. + +=item .n + +Apply a new filename filter. + +=item .# + +Apply a new hash filter. + +=item ." + +Apply a new duplicate filter. + +=item .' + +Apply a new unique filter. + =item .| -Combine the two topmost filters from the filter stack in the "OR" -relationship, instead of the "AND" used implicitly. +Combine the two topmost filters from the filter stack in the C +relationship, instead of the C used implicitly. =item .& -Explicitly combine the two topmost filters in the "AND" -relationship. Usually not needed though might be useful in more -complicated scenarios. +Explicitly combine the two topmost filters in the C relationship. +Usually not needed because filters are implicitly in this relationship though +might be useful in more complicated scenarios. =item .! @@ -708,8 +720,9 @@ 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. +Rotate the filter stack by N elements. Where N is provided as a numeric prefix +like vim's I and defaults to 1, i.e. move the topmost element to the +bottom of the stack. =item .c @@ -1394,6 +1407,103 @@ use the 'd' parameter. To display only files, use the 'f' parameter. To display only links, use the 'l' parameter. Parameters can be combined. To remove this filter, use no parameter. +=item filter_stack [I [I]] + +Manage the filter stack, adding, removing and manipulating filters. For +example, to show only duplicate files and symlinks: + + :filter_stack add type f + :filter_stack add duplicate + :filter_stack add and + :filter_stack add type l + :filter_stack add or + +Or using the mapped keys: + + .f ." .& .l .| + +Available subcommands: + +=over 2 + +=item add FILTER_TYPE [ARGS...] + +Add a new filter to the top of the filter stack. Each filter on the stack is +applied in turn, resulting in an implicit logical C relation. The +following Cs are available: + +=over 2 + +=item duplicate + +Filter files so only files that have duplicates in the same directory are +shown. Useful when cleaning up identical songs and memes that were saved using +distinct file names. + +=item filename NAME + +Filter files that contain NAME in the filename, regular expression syntax is +allowed. + +=item hash PATH + +Filter files so only files with the same hash as PATH are shown. + +=item mimetype TYPE + +Filter files of a certain MIME type, regular expression syntax is allowed. + +=item typefilter [d|f|l] + +Filter files of a certain type, C for directories, C for files and C +for symlinks. + +=item unique + +Filter files so only unique files and the oldest file of every set of +duplicates is shown. + +=item and + +Explicitly combine the two topmost filters in the "AND" relationship. +Usually not needed because filters are implicitly in this relationship though +might be useful in more complicated scenarios. + +=item not + +Negate the topmost filter. + +=item or + +Combine the two topmost filters from the filter stack in the "OR" +relationship, instead of the "AND" used implicitly. + +=back + +=item pop + +Pop the topmost filter from the filter stack. + +=item decompose + +Decompose the topmost filter combinator (e.g. C<.!>, C<.|>). + +=item rotate [N=1] + +Rotate the filter stack by N elements. Where N is passed as argument or as a +numeric prefix like vim's I, default to 1, i.e. move the topmost element +to the bottom of the stack. + +=item clear + +Clear the filter stack. + +=item show + +Show the current filter stack state. + +=back + =item find I Search files in the current directory that contain the given (case-insensitive) diff --git a/ranger/config/commands.py b/ranger/config/commands.py index cb6fa132..cbff8098 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1690,7 +1690,7 @@ class filter_stack(Command): elif subcommand == "clear": self.fm.thisdir.filter_stack = [] elif subcommand == "rotate": - rotate_by = int(self.arg(2) or 1) + rotate_by = int(self.arg(2) or self.quantifier or 1) self.fm.thisdir.filter_stack = ( self.fm.thisdir.filter_stack[-rotate_by:] + self.fm.thisdir.filter_stack[:-rotate_by] diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index 666a99cf..e62f4ecd 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -593,16 +593,19 @@ map zf console filter%space copymap zf zz # Filter stack -map .n console filter_stack add name%space -map .m console filter_stack add mime%space -map .# console filter_stack add hash%space map .d filter_stack add type d map .f filter_stack add type f map .l filter_stack add type l +map .m console filter_stack add mime%space +map .n console filter_stack add name%space +map .# console filter_stack add hash%space +# alternatively .: and .;? +map ." filter_stack add duplicate +map .' filter_stack add unique map .| filter_stack add or map .& filter_stack add and map .! filter_stack add not -map .r console filter_stack rotate +map .r filter_stack rotate map .c filter_stack clear map .* filter_stack decompose map .p filter_stack pop -- cgit 1.4.1-2-gfad0 From 99870addf7e23cd8bb34463c43dfc7ccd38b9545 Mon Sep 17 00:00:00 2001 From: toonn Date: Sat, 28 Dec 2019 18:16:50 +0100 Subject: Fix hash_chunks generator --- ranger/core/filter_stack.py | 12 +++++++----- ranger/ext/hash.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ranger/core/filter_stack.py b/ranger/core/filter_stack.py index ca8810e8..59495437 100644 --- a/ranger/core/filter_stack.py +++ b/ranger/core/filter_stack.py @@ -9,8 +9,7 @@ import re import mimetypes # pylint: disable=invalid-name try: - from itertools import izip_longest - zip_longest = izip_longest + from itertools import izip_longest as zip_longest except ImportError: from itertools import zip_longest # pylint: enable=invalid-name @@ -77,9 +76,12 @@ class MimeFilter(BaseFilter): @stack_filter("hash") class HashFilter(BaseFilter, FileManagerAware): - def __init__(self, filepath): - self.filepath = filepath if filepath else self.fm.thisfile.path - if not self.filepath: + def __init__(self, filepath = None): + if filepath is None: + self.filepath = self.fm.thisfile.path + else: + self.filepath = filepath + if self.filepath is None: self.fm.notify("Error: No file selected for hashing!", bad=True) # TODO: Lazily generated list would be more efficient, a generator # isn't enough because this object is reused for every fsobject diff --git a/ranger/ext/hash.py b/ranger/ext/hash.py index 20059dbf..1ed21a71 100644 --- a/ranger/ext/hash.py +++ b/ranger/ext/hash.py @@ -17,7 +17,8 @@ def hash_chunks(filepath, h=None): h.update(filepath) yield h.hexdigest() for fp in listdir(filepath): - hash_chunks(fp, h=h) + for fp_chunk in hash_chunks(fp, h=h): + yield fp_chunk elif getsize(filepath) == 0: yield h.hexdigest() else: -- cgit 1.4.1-2-gfad0 From 81d61afdeb25702ff836723da4e9dc5315a217d1 Mon Sep 17 00:00:00 2001 From: toonn Date: Sun, 29 Dec 2019 12:48:10 +0100 Subject: Remove comment and doubled up space --- ranger/config/rc.conf | 1 - ranger/ext/hash.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index e62f4ecd..ac6402e9 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -599,7 +599,6 @@ map .l filter_stack add type l map .m console filter_stack add mime%space map .n console filter_stack add name%space map .# console filter_stack add hash%space -# alternatively .: and .;? map ." filter_stack add duplicate map .' filter_stack add unique map .| filter_stack add or diff --git a/ranger/ext/hash.py b/ranger/ext/hash.py index 1ed21a71..d9b2234b 100644 --- a/ranger/ext/hash.py +++ b/ranger/ext/hash.py @@ -17,7 +17,7 @@ def hash_chunks(filepath, h=None): h.update(filepath) yield h.hexdigest() for fp in listdir(filepath): - for fp_chunk in hash_chunks(fp, h=h): + for fp_chunk in hash_chunks(fp, h=h): yield fp_chunk elif getsize(filepath) == 0: yield h.hexdigest() -- cgit 1.4.1-2-gfad0