diff --git a/README.md b/README.md index 8df9e5e..b63a9a8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ Though a terminal utility, it's possible to add bookmarks to `buku` without touc ## Features -- Add, open, tag, comment on, search, update, remove, shorten URLs +- Add, open, tag, comment on, update, remove, shorten URLs +- Multiple search options, continuous search at prompt - Portable, merge-able database, to sync between systems - Import/export bookmarks in markdown or HTML (FF, Chrome compatible) - Fetch page title from web, refresh all titles in a go @@ -169,7 +170,7 @@ Shell completion scripts for Bash, Fish and Zsh can be found in respective subdi "immutable": entries with locked title --deep match substrings ('pen' matches 'opened') --sreg expr run a regex search - --stag [...] search bookmarks by tag + --stag [...] search bookmarks by a tag list tags alphabetically, if no arguments encryption options: @@ -206,7 +207,7 @@ Shell completion scripts for Bash, Fish and Zsh can be found in respective subdi prompt keys: 1-N browse search result indices and/or ranges - double Enter exit buku + q, double Enter exit buku symbols: > title @@ -239,7 +240,7 @@ Shell completion scripts for Bash, Fish and Zsh can be found in respective subdi - --sall : match all the keywords in URL, title or tags. - --deep : match **substrings** (`match` matches `rematched`) in URL, title and tags. - --sreg : match a regular expression (ignores --deep). - - --stag : search bookmarks by tag, or show all tags alphabetically. + - --stag : search bookmarks by a tag, or show all tags alphabetically (if no arguments). - Search results are indexed serially. This index is different from actual database index of a bookmark record which is shown in bold within `[]` after the URL. - **Encryption** is optional and manual. AES256 algorithm is used. To use encryption, the database file should be unlocked (-k) before using buku and locked (-l) afterwards. Between these 2 operations, the database file lies unencrypted on the disk, and NOT in memory. Also, note that the database file is *unencrypted on creation*. diff --git a/buku.1 b/buku.1 index 3f91f65..61bd03e 100644 --- a/buku.1 +++ b/buku.1 @@ -9,7 +9,8 @@ is a command-line tool to save, tag and search bookmarks. .PP .B Features .PP - * Add, open, tag, comment on, search, update, remove, shorten URLs + * Add, open, tag, comment on, update, remove, shorten URLs + * Multiple search options, continuous search at prompt * Portable merge-able database, to sync between systems * Import/export bookmarks in markdown or HTML (FF, Chrome compatible) * Fetch page title from web, refresh all titles in a go @@ -52,7 +53,7 @@ Bookmarks with immutable titles are listed with bold '(L)' after the URL. - --sall : match all the keywords in URL, title or tags. - --deep : match \fBsubstrings\fR (`match` matches `rematched`) in URL, title and tags. - --sreg : match a regular expression (ignores --deep). - - --stag : search bookmarks by tag, or show all tags alphabetically. + - --stag : search bookmarks by a tag, or show all tags alphabetically (if no arguments). - Search results are indexed serially. This index is different from actual database index of a bookmark record which is shown in bold within '[]' after the URL. .PP \fIEncryption\fR is optional and manual. AES256 algorithm is used. To use encryption, the database file should be unlocked (-k) before using buku and locked (-l) afterwards. Between these 2 operations, the database file lies unencrypted on the disk, and NOT in memory. Also, note that the database file is \fBunencrypted on creation\fR. @@ -110,7 +111,7 @@ Scan for a regular expression match. Search modifier to match substrings. Works with --sany, --sall. .TP .BI \--stag " [...]" -Search bookmarks by tag. List all tags alphabetically, if no arguments. +Search bookmarks by a tag. List all tags alphabetically, if no arguments. .SH ENCRYPTION OPTIONS .TP .BI \-l " " \--lock " [N]" @@ -195,7 +196,7 @@ Open the .I Nth search result in browser. Multiple bookmarks are opened if ranges or space-separated result indices are specified. .TP -.BI "double Enter" +.BI "q, double Enter" Exit buku. .SH ENVIRONMENT .TP diff --git a/buku.py b/buku.py index 511e659..4eca72e 100755 --- a/buku.py +++ b/buku.py @@ -353,6 +353,7 @@ class BukuDb: self.field_filter = field_filter self.immutable = immutable self.chatty = chatty + self.deep_search = False # Is deep search opted @staticmethod def get_default_dbdir(): @@ -812,35 +813,37 @@ class BukuDb: arguments = [] query = 'SELECT id, url, metadata, tags, desc FROM bookmarks WHERE' - # Non-deep query string - q1 = '(tags REGEXP ? OR URL REGEXP ? OR metadata REGEXP ? OR desc \ - REGEXP ?)' # Deep query string - q2 = "(tags LIKE ('%' || ? || '%') OR URL LIKE ('%' || ? || '%') OR \ + q1 = "(tags LIKE ('%' || ? || '%') OR URL LIKE ('%' || ? || '%') OR \ metadata LIKE ('%' || ? || '%') OR desc LIKE ('%' || ? || '%'))" + # Non-deep query string + q2 = '(tags REGEXP ? OR URL REGEXP ? OR metadata REGEXP ? OR desc \ + REGEXP ?)' if regex: for token in keywords: - query = '%s %s OR' % (query, q1) + query = '%s %s OR' % (query, q2) arguments += (token, token, token, token) query = query[:-3] elif all_keywords: for token in keywords: - if not deep: - token = '\\b' + token + '\\b' + if deep: query = '%s %s AND' % (query, q1) + self.deep_search = True else: + token = '\\b' + token + '\\b' query = '%s %s AND' % (query, q2) arguments += (token, token, token, token) query = query[:-4] elif not all_keywords: for token in keywords: - if not deep: - token = '\\b' + token + '\\b' + if deep: query = '%s %s OR' % (query, q1) + self.deep_search = True else: + token = '\\b' + token + '\\b' query = '%s %s OR' % (query, q2) arguments += (token, token, token, token) @@ -863,10 +866,11 @@ class BukuDb: def search_by_tag(self, tag): '''Search and list bookmarks with a tag - :param tag: tag to search + :param tag: a tag to search as string :return: search results, or None, if no matches ''' + tag = '%s%s%s' % (DELIM, tag.strip(DELIM), DELIM) query = "SELECT id, url, metadata, tags, desc FROM bookmarks \ WHERE tags LIKE '%' || ? || '%' ORDER BY id ASC" logger.debug('query: "%s", args: %s', query, tag) @@ -1603,21 +1607,31 @@ def parse_tags(keywords=None): return '%s%s%s' % (DELIM, DELIM.join(sorted_tags), DELIM) -def prompt(results, noninteractive=False): +def prompt(obj, results, noninteractive=False): '''Show each matching result from a search and prompt + :param obj: a valid instance of BukuDb class + :param results: result set from a DB query :param noninteractive: do not seek user input ''' - count = 0 - for row in results: - count += 1 - print_record(row, count) - - if noninteractive: + new_results = True + if not type(obj) is BukuDb: + logger.error('Not a BukuDb instance') return while True: + if results and new_results: + count = 0 + print() + + for row in results: + count += 1 + print_record(row, count) + + if noninteractive: + return + try: nav = input('Results, ranges (x-y,(a)ll) to open: ') if not nav: @@ -1628,7 +1642,60 @@ def prompt(results, noninteractive=False): except EOFError: return - # open all results and re-prompt if 'a' is pressed + # search ANY match with new keywords + if nav.startswith('s ') and len(nav) > 2: + results = obj.searchdb(nav[2:].split(), False, obj.deep_search) + new_results = True + continue + + # search ALL match with new keywords + if nav.startswith('S ') and len(nav) > 2: + results = obj.searchdb(nav[2:].split(), True, obj.deep_search) + new_results = True + continue + + # regular expressions search with new keywords + if nav.startswith('r ') and len(nav) > 2: + results = obj.searchdb(nav[2:].split(), True, regex=True) + new_results = True + continue + + # tag search with new keywords + if nav.startswith('t ') and len(nav) > 2: + results = obj.search_by_tag(nav[2:]) + new_results = True + continue + + # list tags with 't' + if nav == 't': + obj.list_tags() + results = None + new_results = False + continue + + # quit with 'q' + if nav == 'q': + return + + # toggle deep search with 'd' + if nav == 'd': + obj.deep_search = not obj.deep_search + if obj.deep_search: + print('deep search on') + else: + print('deep search off') + + new_results = False + continue + + new_results = False + + # Nothing to browse if there are no results + if not results: + print('Not in a search context') + continue + + # open all results and re-prompt with 'a' if nav == 'a': for index in range(0, count): try: @@ -1665,6 +1732,7 @@ def prompt(results, noninteractive=False): logger.error('%s(), ln %d: %s', func, linenumber, e) else: + print('Invalid input') break @@ -1888,7 +1956,7 @@ class ExtendedArgumentParser(argparse.ArgumentParser): file.write(''' prompt keys: 1-N browse search result indices and/or ranges - double Enter exit buku + q, double Enter exit buku symbols: > title @@ -2009,7 +2077,7 @@ def main(): "immutable": entries with locked title --deep match substrings ('pen' matches 'opened') --sreg expr run a regex search ---stag [...] search bookmarks by tag +--stag [...] search bookmarks by a tag list tags alphabetically, if no arguments''') addarg = search_grp.add_argument addarg('-s', '--sany', nargs='+', help=HIDE) @@ -2215,8 +2283,7 @@ def main(): elif tagsearch: search_opted = True if len(args.stag) > 0: - tag = '%s%s%s' % (DELIM, ' '.join(args.stag).strip(DELIM), DELIM) - search_results = bdb.search_by_tag(tag) + search_results = bdb.search_by_tag(' '.join(args.stag)) else: bdb.list_tags() @@ -2227,8 +2294,9 @@ def main(): oneshot = True if not args.json: - prompt(search_results, oneshot) + prompt(bdb, search_results, oneshot) else: + # Printing in Json format is non-interactive print(format_json(search_results, field_filter=args.format)) # Delete search results if opted