From 3f28ea151703b091d75a06526af7bc60c9f77bb6 Mon Sep 17 00:00:00 2001 From: Michael Krayer Date: Sun, 21 Mar 2021 11:59:59 +0100 Subject: [PATCH 1/5] added new buffer 'directories' which makes search queries based on underlying directory structure. --- alot/__main__.py | 2 +- alot/buffers/__init__.py | 1 + alot/buffers/directories.py | 78 ++++++++++++++++++++++++++++++++++ alot/commands/__init__.py | 1 + alot/commands/directories.py | 29 +++++++++++++ alot/commands/globals.py | 13 ++++++ alot/db/manager.py | 17 ++++++++ alot/defaults/alot.rc.spec | 6 +++ alot/defaults/default.bindings | 3 ++ alot/defaults/theme.spec | 4 ++ alot/widgets/directories.py | 30 +++++++++++++ 11 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 alot/buffers/directories.py create mode 100644 alot/commands/directories.py create mode 100644 alot/widgets/directories.py diff --git a/alot/__main__.py b/alot/__main__.py index c901bb072..8f4adcdc7 100644 --- a/alot/__main__.py +++ b/alot/__main__.py @@ -21,7 +21,7 @@ _SUBCOMMANDS = ['search', 'compose', 'bufferlist', 'taglist', 'namedqueries', - 'pyshell'] + 'pyshell', 'directories'] def parser(): diff --git a/alot/buffers/__init__.py b/alot/buffers/__init__.py index bd503f174..c87a7c6bd 100644 --- a/alot/buffers/__init__.py +++ b/alot/buffers/__init__.py @@ -9,3 +9,4 @@ from .taglist import TagListBuffer from .thread import ThreadBuffer from .namedqueries import NamedQueriesBuffer +from .directories import DirectoriesBuffer diff --git a/alot/buffers/directories.py b/alot/buffers/directories.py new file mode 100644 index 000000000..2bca06a62 --- /dev/null +++ b/alot/buffers/directories.py @@ -0,0 +1,78 @@ +# Copyright (C) 2011-2018 Patrick Totzke +# This file is released under the GNU GPL, version 3 or a later revision. +# For further details see the COPYING file +import urwid + +from .buffer import Buffer +from ..settings.const import settings +from ..widgets.directories import DirectorylineWidget + + +class DirectoriesBuffer(Buffer): + """lists directories in all Maildirs present in the notmuch database""" + + modename = 'directories' + + def __init__(self, ui, filtfun): + self.ui = ui + self.filtfun = filtfun + self.isinitialized = False + self.querylist = None + self.rebuild() + Buffer.__init__(self, ui, self.body) + + def rebuild(self): + self.queries = self.ui.dbman.get_directory_queries() + + if self.isinitialized: + focusposition = self.querylist.get_focus()[1] + else: + focusposition = 0 + + lines = [] + for (num, key) in enumerate(self.queries): + value = self.queries[key] + count = self.ui.dbman.count_messages('%s' % value) + count_unread = self.ui.dbman.count_messages('%s and ' + 'tag:unread' % value) + line = DirectorylineWidget(key, value, count, count_unread) + + if (num % 2) == 0: + attr = settings.get_theming_attribute('directories', + 'line_even') + else: + attr = settings.get_theming_attribute('directories', + 'line_odd') + focus_att = settings.get_theming_attribute('directories', + 'line_focus') + + line = urwid.AttrMap(line, attr, focus_att) + lines.append(line) + + self.querylist = urwid.ListBox(urwid.SimpleListWalker(lines)) + self.body = self.querylist + + self.querylist.set_focus(focusposition % len(self.queries)) + + self.isinitialized = True + + def focus_first(self): + """Focus the first line in the query list.""" + self.body.set_focus(0) + + def focus_last(self): + allpos = self.querylist.body.positions(reverse=True) + if allpos: + lastpos = allpos[0] + self.body.set_focus(lastpos) + + def get_selected_query(self): + """returns selected query""" + return self.querylist.get_focus()[0].original_widget.query + + def get_info(self): + info = {} + + info['query_count'] = len(self.queries) + + return info diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py index 29e5dafd4..650edce48 100644 --- a/alot/commands/__init__.py +++ b/alot/commands/__init__.py @@ -46,6 +46,7 @@ class SequenceCanceled(Exception): 'bufferlist': {}, 'taglist': {}, 'namedqueries': {}, + 'directories': {}, 'thread': {}, 'global': {}, } diff --git a/alot/commands/directories.py b/alot/commands/directories.py new file mode 100644 index 000000000..f5a3fd268 --- /dev/null +++ b/alot/commands/directories.py @@ -0,0 +1,29 @@ +# Copyright (C) 2011-2018 Patrick Totzke +# This file is released under the GNU GPL, version 3 or a later revision. +# For further details see the COPYING file +import argparse + +from . import Command, registerCommand +from .globals import SearchCommand + +MODE = 'directories' + + +@registerCommand(MODE, 'select', arguments=[ + (['filt'], {'nargs': argparse.REMAINDER, + 'help': 'additional filter to apply to query'}), +]) +class DirectoriesSelectCommand(Command): + + """search for messages with selected query""" + def __init__(self, filt=None, **kwargs): + self._filt = filt + Command.__init__(self, **kwargs) + + async def apply(self, ui): + query = [ui.current_buffer.get_selected_query()] + if self._filt: + query.extend(['and'] + self._filt) + + cmd = SearchCommand(query=query) + await ui.apply_command(cmd) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index aa1fd4368..566d1dfcd 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -590,6 +590,19 @@ def __init__(self, filtfun=bool, **kwargs): def apply(self, ui): ui.buffer_open(buffers.NamedQueriesBuffer(ui, self.filtfun)) +@registerCommand(MODE, 'directories') +class DirectoriesCommand(Command): + """opens directories buffer""" + def __init__(self, filtfun=bool, **kwargs): + """ + :param filtfun: filter to apply to displayed list + :type filtfun: callable (str->bool) + """ + self.filtfun = filtfun + Command.__init__(self, **kwargs) + + def apply(self, ui): + ui.buffer_open(buffers.DirectoriesBuffer(ui, self.filtfun)) @registerCommand(MODE, 'flush') class FlushCommand(Command): diff --git a/alot/db/manager.py b/alot/db/manager.py index bd87cf7b6..299b47bfc 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -5,6 +5,7 @@ from collections import deque import contextlib import logging +import os from notmuch2 import Database, NotmuchError, XapianError import notmuch2 @@ -296,6 +297,22 @@ def get_named_queries(self): return {k[6:]: db.config[k] for k in db.config if k.startswith('query.')} + def get_directory_queries(self): + """ + returns the directory structure in the database. + :rtype: dict (str -> str) mapping alias to full query string + """ + queries = {}; + for root, dirs, files in os.walk(self.path): + dirs[:] = [d for d in dirs if not (d[0] == '.' + or d in ('new','cur','tmp'))] + subpath = root[len(self.path):] + if not subpath: + queries['/'] = 'path:**' + else: + queries['/'+subpath] = 'path:'+subpath+'/**' + return queries + def get_threads(self, querystring, sort='newest_first', exclude_tags=None): """ asynchronously look up thread ids matching `querystring`. diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec index 99ef7c97d..b4a4c29eb 100644 --- a/alot/defaults/alot.rc.spec +++ b/alot/defaults/alot.rc.spec @@ -165,6 +165,12 @@ taglist_statusbar = mixed_list(string, string, default=list('[{buffer_no}: tagli # that will be substituted accordingly. namedqueries_statusbar = mixed_list(string, string, default=list('[{buffer_no}: namedqueries]','{query_count} named queries')) +# Format of the status-bar in directory list mode. +# This is a pair of strings to be left and right aligned in the status-bar. +# These strings may contain variables listed at :ref:`bufferlist_statusbar ` +# that will be substituted accordingly. +directories_statusbar = mixed_list(string, string, default=list('[{buffer_no}: directories]','{query_count} directories')) + # Format of the status-bar in envelope mode. # This is a pair of strings to be left and right aligned in the status-bar. # Apart from the global variables listed at :ref:`bufferlist_statusbar ` diff --git a/alot/defaults/default.bindings b/alot/defaults/default.bindings index ffca1bdc9..348ce8631 100644 --- a/alot/defaults/default.bindings +++ b/alot/defaults/default.bindings @@ -61,6 +61,9 @@ q = exit [namedqueries] enter = select +[directories] + enter = select + [thread] enter = select C = fold * diff --git a/alot/defaults/theme.spec b/alot/defaults/theme.spec index 387bbe2a1..2df8b9a18 100644 --- a/alot/defaults/theme.spec +++ b/alot/defaults/theme.spec @@ -26,6 +26,10 @@ line_focus = attrtriple line_even = attrtriple line_odd = attrtriple +[directories] + line_focus = attrtriple + line_even = attrtriple + line_odd = attrtriple [search] [[threadline]] normal = attrtriple diff --git a/alot/widgets/directories.py b/alot/widgets/directories.py new file mode 100644 index 000000000..7b8ec7ea1 --- /dev/null +++ b/alot/widgets/directories.py @@ -0,0 +1,30 @@ +# Copyright (C) 2011-2018 Patrick Totzke +# This file is released under the GNU GPL, version 3 or a later revision. +# For further details see the COPYING file + +""" +Widgets specific to directories mode +""" +import urwid + + +class DirectorylineWidget(urwid.Columns): + def __init__(self, key, value, count, count_unread): + self.query = value + + count_widget = urwid.Text('{0:>7} {1:7}'. + format(count, '({0})'.format(count_unread))) + key_widget = urwid.Text(key) + value_widget = urwid.Text(value) + + urwid.Columns.__init__(self, (key_widget, count_widget, value_widget), + dividechars=1) + + def selectable(self): + return True + + def keypress(self, size, key): + return key + + def get_query(self): + return self.query From f730245ddede25730fa45c8efc1381002f097832 Mon Sep 17 00:00:00 2001 From: Michael Krayer Date: Sun, 21 Mar 2021 12:08:46 +0100 Subject: [PATCH 2/5] added DirectoriesBuffer theming --- extra/themes/mutt | 4 ++++ extra/themes/solarized_dark | 4 ++++ extra/themes/solarized_light | 4 ++++ extra/themes/sup | 4 ++++ extra/themes/tomorrow | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/extra/themes/mutt b/extra/themes/mutt index f011dd195..6dce4b536 100644 --- a/extra/themes/mutt +++ b/extra/themes/mutt @@ -25,6 +25,10 @@ line_even = '','','light gray','black','light gray','black' line_odd = '','','light gray','black','light gray','black' line_focus = 'standout','','black','dark cyan','black','dark cyan' +[directories] + line_even = '','','light gray','black','light gray','black' + line_odd = '','','light gray','black','light gray','black' + line_focus = 'standout','','black','dark cyan','black','dark cyan' [taglist] line_even = '','','light gray','black','light gray','black' line_odd = '','','light gray','black','light gray','black' diff --git a/extra/themes/solarized_dark b/extra/themes/solarized_dark index db0cd090c..ebd88faac 100644 --- a/extra/themes/solarized_dark +++ b/extra/themes/solarized_dark @@ -46,6 +46,10 @@ green = 'dark green' line_even = 'default','default','%(base0)s','%(base02)s','%(base0)s','%(base02)s' line_focus = 'standout','default','%(base1)s','%(base01)s','%(base1)s','%(base01)s' line_odd = 'default','default','%(base0)s','%(base03)s','%(base0)s','%(base03)s' +[directories] + line_even = 'default','default','%(base0)s','%(base02)s','%(base0)s','%(base02)s' + line_focus = 'standout','default','%(base1)s','%(base01)s','%(base1)s','%(base01)s' + line_odd = 'default','default','%(base0)s','%(base03)s','%(base0)s','%(base03)s' [taglist] line_even = 'default','default','%(base0)s','%(base02)s','%(base0)s','%(base02)s' line_focus = 'standout','default','%(base1)s','%(base01)s','%(base1)s','%(base01)s' diff --git a/extra/themes/solarized_light b/extra/themes/solarized_light index ec6b2c936..b12129f00 100644 --- a/extra/themes/solarized_light +++ b/extra/themes/solarized_light @@ -62,6 +62,10 @@ line_focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s' line_even = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s' line_odd = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s' +[directories] + line_focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s' + line_even = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s' + line_odd = 'default','default','%(16_base00)s','%(16_base2)s','%(256_base00)s','%(256_base2)s' [taglist] line_focus = 'standout','default','%(16_base2)s','%(16_yellow)s','%(256_base2)s','%(256_yellow)s' line_even = 'default','default','%(16_base00)s','%(16_base3)s','%(256_base00)s','%(256_base3)s' diff --git a/extra/themes/sup b/extra/themes/sup index 26708b170..abaf98447 100644 --- a/extra/themes/sup +++ b/extra/themes/sup @@ -25,6 +25,10 @@ line_even = '','','light gray','black','light gray','g0' line_odd = '','','light gray','black','light gray','g0' line_focus = 'standout','','black','dark cyan','black','dark cyan' +[directories] + line_even = '','','light gray','black','light gray','g0' + line_odd = '','','light gray','black','light gray','g0' + line_focus = 'standout','','black','dark cyan','black','dark cyan' [taglist] line_even = '','','light gray','black','light gray','g0' line_odd = '','','light gray','black','light gray','g0' diff --git a/extra/themes/tomorrow b/extra/themes/tomorrow index e7c8dc841..77a761b69 100644 --- a/extra/themes/tomorrow +++ b/extra/themes/tomorrow @@ -75,6 +75,10 @@ error = 'dark red' line_focus = 'standout','default','%(16_focus_fg)s','%(16_focus_bg)s','%(256_focus_fg)s','%(256_focus_bg)s' line_even = 'default','default','%(16_text_fg)s','%(16_normal_bg)s','%(256_text_fg)s','%(256_normal_bg)s' line_odd = 'default','default','%(16_text_fg)s','%(16_normal_bg)s','%(256_text_fg)s','%(256_normal_bg)s' +[directories] + line_focus = 'standout','default','%(16_focus_fg)s','%(16_focus_bg)s','%(256_focus_fg)s','%(256_focus_bg)s' + line_even = 'default','default','%(16_text_fg)s','%(16_normal_bg)s','%(256_text_fg)s','%(256_normal_bg)s' + line_odd = 'default','default','%(16_text_fg)s','%(16_normal_bg)s','%(256_text_fg)s','%(256_normal_bg)s' [taglist] line_focus = 'standout','default','%(16_focus_fg)s','%(16_focus_bg)s','%(256_focus_fg)s','%(256_focus_bg)s' line_even = 'default','default','%(16_text_fg)s','%(16_normal_bg)s','%(256_text_fg)s','%(256_normal_bg)s' From 8a187486c3c002f77229d412949626354db07465 Mon Sep 17 00:00:00 2001 From: Michael Krayer Date: Sun, 21 Mar 2021 12:34:34 +0100 Subject: [PATCH 3/5] added DirectoriesBuffer to default theme --- alot/defaults/default.theme | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alot/defaults/default.theme b/alot/defaults/default.theme index b97dac50a..165e66081 100644 --- a/alot/defaults/default.theme +++ b/alot/defaults/default.theme @@ -28,6 +28,10 @@ line_focus = 'standout','','yellow','light gray','#ff8','g58' line_even = 'default','','light gray','black','default','g3' line_odd = 'default','','light gray','black','default','default' +[directories] + line_focus = 'standout','','yellow','light gray','#ff8','g58' + line_even = 'default','','light gray','black','default','g3' + line_odd = 'default','','light gray','black','default','default' [thread] arrow_heads = '','','dark red','','#a00','' arrow_bars = '','','dark red','','#800','' From 7216cd9916f166438a29eaa7dedb01a9cfd0558f Mon Sep 17 00:00:00 2001 From: Michael Krayer Date: Sun, 21 Mar 2021 12:39:03 +0100 Subject: [PATCH 4/5] added DirectoriesBuffer to test theme --- tests/settings/test_theme.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/settings/test_theme.py b/tests/settings/test_theme.py index c74de50e8..f9b05f867 100644 --- a/tests/settings/test_theme.py +++ b/tests/settings/test_theme.py @@ -38,6 +38,10 @@ line_even = '', '', '', '', '', '' line_focus = '', '', '', '', '', '' line_odd = '', '', '', '', '', '' +[directories] + line_even = '', '', '', '', '', '' + line_focus = '', '', '', '', '', '' + line_odd = '', '', '', '', '', '' [search] focus = '', '', '', '', '', '' normal = '', '', '', '', '', '' From e8721e27f6b377bfeebb6b34d8ac1a7a52052fcd Mon Sep 17 00:00:00 2001 From: Michael Krayer Date: Mon, 22 Mar 2021 13:11:16 +0100 Subject: [PATCH 5/5] bugfix: paths with spaces lead to erroneous search queries --- alot/db/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alot/db/manager.py b/alot/db/manager.py index 299b47bfc..a48d58c3e 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -308,9 +308,9 @@ def get_directory_queries(self): or d in ('new','cur','tmp'))] subpath = root[len(self.path):] if not subpath: - queries['/'] = 'path:**' + queries['/'] = 'path:"**"' else: - queries['/'+subpath] = 'path:'+subpath+'/**' + queries['/'+subpath] = 'path:"'+subpath+'/**"' return queries def get_threads(self, querystring, sort='newest_first', exclude_tags=None):