diff --git a/catcli/catcli.py b/catcli/catcli.py index 2c9aa62..f0a9b8b 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -27,6 +27,7 @@ CATALOGPATH = '{}.catalog'.format(NAME) GRAPHPATH = '/tmp/{}.dot'.format(NAME) SEPARATOR = '/' WILD = '*' +FORMATS = ['native', 'csv', 'fzf-native', 'fzf-csv'] BANNER = """ +-+-+-+-+-+-+ |c|a|t|c|l|i| @@ -37,8 +38,8 @@ USAGE = """ Usage: {1} ls [--catalog=] [--format=] [-aBCrVSs] [] - {1} find [--catalog=] [--format=] [-aBCbdVsP] [--path=] - {1} tree [--catalog=] [--format=] [-aBCVSsH] [] + {1} find [--catalog=] [--format=] [-aBCbdVsP] [--path=] [] + {1} tree [--catalog=] [-aBCVSsH] [] {1} index [--catalog=] [--meta=...] [-aBCcfnV] {1} update [--catalog=] [-aBCcfnV] [--lpath=] {1} rm [--catalog=] [-BCfV] @@ -174,14 +175,15 @@ def cmd_find(args, noder, top): startpath = args['--path'] fmt = args['--format'] raw = args['--raw-size'] - return noder.find_name(top, args[''], script=args['--script'], + script = args['--script'] + search_for = args[''] + return noder.find_name(top, search_for, script=script, startpath=startpath, directory=directory, parentfromtree=fromtree, fmt=fmt, raw=raw) def cmd_tree(args, noder, top): path = args[''] - fmt = args['--format'] hdr = args['--header'] raw = args['--raw-size'] @@ -192,7 +194,7 @@ def cmd_tree(args, noder, top): if node: # print the tree - noder.print_tree(node, fmt=fmt, header=hdr, raw=raw) + noder.print_tree(top, node, header=hdr, raw=raw) def cmd_graph(args, noder, top): @@ -240,6 +242,14 @@ def banner(): Logger.out_err("") +def print_supported_formats(): + print('"native" : native format') + print('"csv" : CSV format') + print(' {}'.format(Noder.CSV_HEADER)) + print('"fzf-native" : fzf with native output for selected entries') + print('"fzf-csv" : fzf with native output for selected entries') + + def main(): args = docopt(USAGE, version=VERSION) @@ -248,15 +258,14 @@ def main(): return True if args['print_supported_formats']: - print('"native": native format') - print('"csv" : CSV format') - print(' {}'.format(Noder.CSV_HEADER)) + print_supported_formats() return True # check format fmt = args['--format'] - if fmt != 'native' and fmt != 'csv': + if fmt not in FORMATS: Logger.err('bad format: {}'.format(fmt)) + print_supported_formats() return False if args['--verbose']: diff --git a/catcli/noder.py b/catcli/noder.py index 4997b9c..87ecbf5 100644 --- a/catcli/noder.py +++ b/catcli/noder.py @@ -9,6 +9,7 @@ import os import anytree import shutil import time +from pyfzf.pyfzf import FzfPrompt # local imports from . import __version__ as VERSION @@ -435,10 +436,10 @@ class Noder: else: Logger.err('bad node encountered: {}'.format(node)) - def print_tree(self, node, style=anytree.ContRoundStyle(), + def print_tree(self, top, node, style=anytree.ContRoundStyle(), fmt='native', header=False, raw=False): ''' - print the tree similar to unix tool "tree" + print the tree in different format @node: start node @style: when fmt=native, defines the tree style @fmt: output format @@ -446,11 +447,16 @@ class Noder: @raw: print the raw size rather than human readable ''' if fmt == 'native': + # "tree" style rend = anytree.RenderTree(node, childiter=self._sort_tree) for pre, fill, node in rend: self._print_node(node, pre=pre, withdepth=True, raw=raw) elif fmt == 'csv': + # csv output self._to_csv(node, with_header=header, raw=raw) + elif fmt.startswith('fzf'): + # flat + self._to_fzf(top, node) def _to_csv(self, node, with_header=False, raw=False): '''print the tree to csv''' @@ -460,6 +466,41 @@ class Noder: for _, _, node in rend: self._node_to_csv(node, raw=raw) + def _fzf_prompt(self, strings): + # prompt with fzf + fzf = FzfPrompt() + selected = fzf.prompt(strings) + return selected + + def _to_fzf(self, top, node, fmt): + """ + print node to fzf + @top: top node + @node: node to start with + @fmt: output format for selected nodes + """ + rend = anytree.RenderTree(node, childiter=self._sort_tree) + nodes = dict() + # construct node names list + for _, _, node in rend: + if not node: + continue + parents = self._get_parents(node) + storage = self._get_storage(node) + fullpath = os.path.join(storage.name, parents) + nodes[fullpath] = node + # prompt with fzf + self._fzf_prompt(nodes.keys()) + # print the resulting tree + subfmt = fmt.replace('fzf-', '') + for path in paths: + if not path: + continue + if path not in nodes: + continue + node = nodes[path] + self.print_tree(top, node, fmt=subfmt) + def to_dot(self, node, path='tree.dot'): '''export to dot for graphing''' anytree.exporter.DotExporter(node).to_dotfile(path) @@ -469,25 +510,34 @@ class Noder: ############################################################### # searching ############################################################### - def find_name(self, root, key, + def find_name(self, top, key, script=False, directory=False, startpath=None, parentfromtree=False, fmt='native', raw=False): ''' find files based on their names + @top: top node + @key: term to search for @script: output script @directory: only search for directories @startpath: node to start with @parentfromtree: get path from parent instead of stored relpath @fmt: output format + @raw: raw size output ''' self._debug('searching for \"{}\"'.format(key)) - start = root + if not key: + # nothing to search for + return None + start = top if startpath: - start = self.get_node(root, startpath) - self.term = key - found = anytree.findall(start, filter_=self._find_name) - paths = [] + start = self.get_node(top, startpath) + found = anytree.findall(start, filter_=self._callback_find_name(key)) + self._debug(f'found {len(found)} node(s)') + + # compile found nodes + paths = dict() + nodes = [] for f in found: if f.type == self.TYPE_STORAGE: # ignore storage nodes @@ -495,58 +545,74 @@ class Noder: if directory and f.type != self.TYPE_DIR: # ignore non directory continue + nodes.append(f) - # print the node - if fmt == 'native': - self._print_node(f, withpath=True, + if parentfromtree: + paths[self._get_parents(f)] = f + else: + paths[f.relpath] = f + + if fmt == 'native': + for n in nodes: + self._print_node(n, withpath=True, withdepth=True, withstorage=True, recalcparent=parentfromtree, raw=raw) - elif fmt == 'csv': - self._node_to_csv(f, raw=raw) - - if parentfromtree: - paths.append(self._get_parents(f)) - else: - paths.append(f.relpath) + elif fmt == 'csv': + for n in nodes: + self._node_to_csv(n, raw=raw) + elif fmt.startswith('fzf'): + selected = self._fzf_prompt(paths) + newpaths = dict() + subfmt = fmt.replace('fzf-', '') + for s in selected: + if s not in paths: + continue + newpaths[s] = paths[s] + self.print_tree(top, newpaths[s], fmt=subfmt) + paths = newpaths if script: - tmp = ['${source}/' + x for x in paths] + tmp = ['${source}/' + x for x in paths.keys()] cmd = 'op=file; source=/media/mnt; $op {}'.format(' '.join(tmp)) Logger.info(cmd) return found - def _find_name(self, node): + def _callback_find_name(self, term): '''callback for finding files''' - if self.term.lower() in node.name.lower(): - return True - return False + def find_name(node): + if term.lower() in node.name.lower(): + return True + return False + return find_name ############################################################### # climbing ############################################################### - def walk(self, root, path, rec=False, fmt='native', raw=False): + def walk(self, top, path, rec=False, fmt='native', + raw=False): ''' walk the tree for ls based on names - @root: start node + @top: start node @rec: recursive walk @fmt: output format + @raw: print raw size ''' self._debug('walking path: \"{}\"'.format(path)) r = anytree.resolver.Resolver('name') found = [] try: - found = r.glob(root, path) + found = r.glob(top, path) if len(found) < 1: # nothing found return [] if rec: # print the entire tree - self.print_tree(found[0].parent, fmt=fmt, raw=raw) + self.print_tree(top, found[0].parent, fmt=fmt, raw=raw) return found # sort found nodes @@ -558,6 +624,8 @@ class Noder: withpath=False, withdepth=True, raw=raw) elif fmt == 'csv': self._node_to_csv(found[0].parent, raw=raw) + elif fmt == 'fzf': + pass # print all found nodes for f in found: @@ -568,6 +636,8 @@ class Noder: raw=raw) elif fmt == 'csv': self._node_to_csv(f, raw=raw) + elif fmt == 'fzf': + self._to_fzf(top, f) except anytree.resolver.ChildResolverError: pass @@ -638,6 +708,8 @@ class Noder: '''get all parents recursively''' if node.type == self.TYPE_STORAGE: return '' + if node.type == self.TYPE_TOP: + return '' parent = self._get_parents(node.parent) if parent: return os.sep.join([parent, node.name]) diff --git a/requirements.txt b/requirements.txt index 6b7fb29..936d33b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ docopt; python_version >= '3.0' anytree; python_version >= '3.0' +pyfzf; python_version >= '3.0' diff --git a/tests/test_update.py b/tests/test_update.py index 5d4d4de..86130c7 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -76,7 +76,7 @@ class TestIndexing(unittest.TestCase): self.assertTrue(nod.md5 == f4_md5) # print catalog - noder.print_tree(top) + noder.print_tree(top, top) # add some files and directories new1 = create_rnd_file(d1, 'newf1') @@ -118,7 +118,7 @@ class TestIndexing(unittest.TestCase): # print catalog # print(read_from_file(catalogpath)) - noder.print_tree(top) + noder.print_tree(top, top) # explore the top node to find all nodes self.assertTrue(len(top.children) == 1)