Skip to content
2 changes: 1 addition & 1 deletion alot/buffers/bufferlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class BufferlistBuffer(Buffer):

modename = 'bufferlist'

def __init__(self, ui, filtfun=lambda x: x):
def __init__(self, ui, filtfun=lambda x: True):
self.filtfun = filtfun
self.ui = ui
self.isinitialized = False
Expand Down
3 changes: 1 addition & 2 deletions alot/buffers/namedqueries.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ class NamedQueriesBuffer(Buffer):

modename = 'namedqueries'

def __init__(self, ui, filtfun):
def __init__(self, ui):
self.ui = ui
self.filtfun = filtfun
self.isinitialized = False
self.querylist = None
self.rebuild()
Expand Down
71 changes: 60 additions & 11 deletions alot/buffers/taglist.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file
import urwid
from notmuch2 import NotmuchError

from .buffer import Buffer
from ..settings.const import settings
Expand All @@ -13,45 +14,85 @@ class TagListBuffer(Buffer):

modename = 'taglist'

def __init__(self, ui, alltags=None, filtfun=lambda x: x):
def __init__(self, ui, alltags=None, filtfun=lambda x: True, querystring=None, match=None):
self.filtfun = filtfun
self.ui = ui
self.tags = alltags or []
self.querystring = querystring
self.match = match
self.result_count = 0
self.isinitialized = False
self.rebuild()
Buffer.__init__(self, ui, self.body)

def __str__(self):
formatstring = '[taglist] for "%s matching %s" (%d message%s)'
return formatstring % (self.querystring or '*', self.match or '*', self.result_count,
's' if self.result_count > 1 else '')

def get_info(self):
info = {}
info['querystring'] = self.querystring or '*'
info['match'] = self.match or '*'
info['result_count'] = self.result_count
info['result_count_positive'] = 's' if self.result_count > 1 else ''
return info

def rebuild(self):
if self.isinitialized:
focusposition = self.taglist.get_focus()[1]
else:
focusposition = 0
self.isinitialized = True

lines = list()
displayedtags = sorted((t for t in self.tags if self.filtfun(t)),
key=str.lower)

exclude_tags = settings.get_notmuch_setting('search', 'exclude_tags')
if exclude_tags:
exclude_tags = [t for t in exclude_tags.split(';') if t]

compoundquerystring = ' AND '.join(['(%s)' % q for q in
[self.querystring,
' OR '.join(['tag:"%s"' % t for t
in displayedtags])]
if q])

try:
self.result_count = self.ui.dbman.count_messages(
compoundquerystring or '*')
except NotmuchError:
self.ui.notify('malformed query string: %s' % compoundquerystring,
'error')
self.taglist = urwid.ListBox([])
self.body = self.listbox
return

lines = list()
for (num, b) in enumerate(displayedtags):
if (num % 2) == 0:
attr = settings.get_theming_attribute('taglist', 'line_even')
else:
attr = settings.get_theming_attribute('taglist', 'line_odd')
focus_att = settings.get_theming_attribute('taglist', 'line_focus')

tw = TagWidget(b, attr, focus_att)
rows = [('fixed', tw.width(), tw)]
if tw.hidden:
rows.append(urwid.Text(b + ' [hidden]'))
elif tw.translated is not b:
rows.append(urwid.Text('(%s)' % b))
rows = [TagWidget(b, attr, focus_att, True)]

count = self.ui.dbman.count_messages(' AND '.join(['(%s)' % q for q in
[self.querystring, 'tag:"%s"' % b] if q]))
count_unread = self.ui.dbman.count_messages(' AND '.join(['(%s)' % q for q in
[self.querystring, 'tag:"%s"' % b, 'tag:unread'] if q]))
rows.append(urwid.Text('{0:>7} {1:7}'.
format(count, '({0})'.format(count_unread))))
line = urwid.Columns(rows, dividechars=1)
line = urwid.AttrMap(line, attr, focus_att)
lines.append(line)

self.taglist = urwid.ListBox(urwid.SimpleListWalker(lines))
self.body = self.taglist

self.taglist.set_focus(focusposition % len(displayedtags))
if len(displayedtags):
self.taglist.set_focus(focusposition % len(displayedtags))

def focus_first(self):
"""Focus the first line in the tag list."""
Expand All @@ -63,8 +104,16 @@ def focus_last(self):
lastpos = allpos[0]
self.body.set_focus(lastpos)

def get_selected_tag(self):
"""returns selected tagstring"""
def get_selected_tagline(self):
"""
returns curently focussed :class:`urwid.AttrMap` tagline widget
from the result list.
"""
cols, _ = self.taglist.get_focus()
return cols

def get_selected_tag(self):
"""returns selected tagstring or throws AttributeError if none"""
cols = self.get_selected_tagline()
tagwidget = cols.original_widget.get_focus()
return tagwidget.tag
52 changes: 35 additions & 17 deletions alot/commands/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import email
import email.utils
import glob
import re
import logging
import os
import subprocess
Expand Down Expand Up @@ -532,7 +533,7 @@ def apply(self, ui):
class OpenBufferlistCommand(Command):

"""open a list of active buffers"""
def __init__(self, filtfun=lambda x: x, **kwargs):
def __init__(self, filtfun=lambda x: True, **kwargs):
"""
:param filtfun: filter to apply to displayed list
:type filtfun: callable (str->bool)
Expand All @@ -550,45 +551,62 @@ def apply(self, ui):


@registerCommand(MODE, 'taglist', arguments=[
(['--global'], {'action': 'store_true',
'help': 'list all tags globally instead of just those from the buffer',
'dest': 'globally'}),
(['--tags'], {'nargs': '+', 'help': 'tags to display'}),
(['match'], {'nargs': '?',
'help': 'regular expression to match tags against'}),
])
class TagListCommand(Command):

"""opens taglist buffer"""
def __init__(self, filtfun=lambda x: x, tags=None, **kwargs):

def __init__(self, filtfun=lambda x: True, tags=None, match=None, globally=False, **kwargs):
"""
:param filtfun: filter to apply to displayed list
:type filtfun: callable (str->bool)
:param match: regular expression to match tags against
:type match: string
:param globally: list all tags globally instead of just those from the buffer
:type globally: bool
"""
self.filtfun = filtfun
if match:
pattern = re.compile(match)
self.filtfun = lambda x: pattern.search(x) is not None
else:
self.filtfun = filtfun
self.globally = globally
self.match = match
self.tags = tags
Command.__init__(self, **kwargs)

def apply(self, ui):
tags = self.tags or ui.dbman.get_all_tags()
blists = ui.get_buffers_of_type(buffers.TagListBuffer)
if blists:
buf = blists[0]
buf.tags = tags
buf.rebuild()
ui.buffer_focus(buf)
else:
ui.buffer_open(buffers.TagListBuffer(ui, tags, self.filtfun))
querystring = None
if self.tags:
tags = self.tags
elif (not self.globally) and isinstance(ui.current_buffer, buffers.SearchBuffer):
tags = ui.dbman.collect_tags(ui.current_buffer.querystring)
querystring = ui.current_buffer.querystring
elif (not self.globally) and isinstance(ui.current_buffer, buffers.ThreadBuffer):
tags = list(ui.current_buffer.thread.get_tags())
querystring = 'thread:%s' % ui.current_buffer.thread.get_thread_id()
else: # self.globally or otherBuffer
tags = ui.dbman.get_all_tags()
ui.buffer_open(buffers.TagListBuffer(
ui, tags, self.filtfun, querystring, self.match))


@registerCommand(MODE, 'namedqueries')
class NamedQueriesCommand(Command):
"""opens named queries buffer"""
def __init__(self, filtfun=bool, **kwargs):
def __init__(self, **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.NamedQueriesBuffer(ui, self.filtfun))
ui.buffer_open(buffers.NamedQueriesBuffer(ui))


@registerCommand(MODE, 'flush')
Expand Down
70 changes: 68 additions & 2 deletions alot/commands/taglist.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,83 @@
# Copyright © 2018 Dylan Baker
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file
import logging

from . import Command, registerCommand
from .globals import SearchCommand
from .. import commands

MODE = 'taglist'


@registerCommand(MODE, 'select')
class TaglistSelectCommand(Command):

"""search for messages with selected tag within original buffer"""
async def apply(self, ui):
try:
tagstring = ui.current_buffer.get_selected_tag()
except AttributeError:
logging.debug("taglist select without tag selection")
return
if ' ' in tagstring:
tagstring = '"%s"' % tagstring
querystring = ui.current_buffer.querystring
if querystring:
fullquerystring = '(%s) AND tag:%s' % (querystring, tagstring)
else:
fullquerystring = 'tag:%s' % tagstring
cmd = SearchCommand(query=[fullquerystring])
await ui.apply_command(cmd)


@registerCommand(MODE, 'globalselect')
class TaglistGlobalSelectCommand(Command):

"""search for messages with selected tag"""
async def apply(self, ui):
tagstring = ui.current_buffer.get_selected_tag()
cmd = SearchCommand(query=['tag:"%s"' % tagstring])
try:
tagstring = ui.current_buffer.get_selected_tag()
except AttributeError:
logging.debug("taglist globalselect without tag selection")
return
if ' ' in tagstring:
tagstring = '"%s"' % tagstring
cmd = SearchCommand(query=['tag:%s' % tagstring])
await ui.apply_command(cmd)


@registerCommand(MODE, 'untag')
class UntagCommand(Command):

"""remove selected tag from all messages within original buffer"""
async def apply(self, ui):
taglistbuffer = ui.current_buffer
taglinewidget = taglistbuffer.get_selected_tagline()
try:
tag = taglistbuffer.get_selected_tag()
except AttributeError:
logging.debug("taglist untag without tag selection")
return
tagstring = 'tag:"%s"' % tag
querystring = taglistbuffer.querystring
if querystring:
fullquerystring = '(%s) AND %s' % (querystring, tagstring)
else:
fullquerystring = tagstring

def refresh():
if taglinewidget in taglistbuffer.taglist:
taglistbuffer.taglist.remove(taglinewidget)
if tag in taglistbuffer.tags:
taglistbuffer.tags.remove(tag)
taglistbuffer.rebuild()
ui.update()

try:
ui.dbman.untag(fullquerystring, [tag])
except DatabaseROError:
ui.notify('index in read-only mode', priority='error')
return

await ui.apply_command(commands.globals.FlushCommand(callback=refresh))
10 changes: 10 additions & 0 deletions alot/db/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,16 @@ def count_messages(self, querystring):
return db.count_messages(querystring,
exclude_tags=settings.get('exclude_tags'))

def collect_tags(self, querystring):
"""returns tags of messages that match `querystring`"""
db = Database(path=self.path, mode=Database.MODE.READ_ONLY)
tagset = notmuch2._tags.ImmutableTagSet(
db.messages(querystring,
exclude_tags=settings.get('exclude_tags')),
'_iter_p',
notmuch2.capi.lib.notmuch_messages_collect_tags)
return [t for t in tagset]

def count_threads(self, querystring):
"""returns number of threads that match `querystring`"""
db = Database(path=self.path, mode=Database.MODE.READ_ONLY)
Expand Down
11 changes: 8 additions & 3 deletions alot/defaults/alot.rc.spec
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,14 @@ thread_statusbar = mixed_list(string, string, default=list('[{buffer_no}: thread

# Format of the status-bar in taglist 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 <bufferlist-statusbar>`
# that will be substituted accordingly.
taglist_statusbar = mixed_list(string, string, default=list('[{buffer_no}: taglist]','{input_queue} total messages: {total_messages}'))
# Apart from the global variables listed at :ref:`bufferlist_statusbar <bufferlist-statusbar>`
# these strings may contain variables:
#
# * `{querystring}`: search string
# * `{match}`: match expression
# * `{result_count}`: number of matching messages
# * `{result_count_positive}`: 's' if result count is greater than 0.
taglist_statusbar = mixed_list(string, string, default=list('[{buffer_no}: taglist] for "{querystring}" matching "{match}"','{input_queue} {result_count} of {total_messages} messages'))

# Format of the status-bar in named query list mode.
# This is a pair of strings to be left and right aligned in the status-bar.
Expand Down
1 change: 1 addition & 0 deletions alot/defaults/default.bindings
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ q = exit

[taglist]
enter = select
meta enter = globalselect

[namedqueries]
enter = select
Expand Down
11 changes: 9 additions & 2 deletions alot/widgets/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,21 @@ class TagWidget(urwid.AttrMap):
:type tag: str
"""

def __init__(self, tag, fallback_normal=None, fallback_focus=None):
def __init__(self, tag, fallback_normal=None, fallback_focus=None,
amend=False):
self.tag = tag
representation = settings.get_tagstring_representation(tag,
fallback_normal,
fallback_focus)
self.translated = representation['translated']
self.hidden = self.translated == ''
self.txt = urwid.Text(self.translated, wrap='clip')
txt = self.translated
if amend:
if self.hidden:
txt += self.tag + ' [hidden]'
elif self.translated is not self.tag:
txt += ' (%s)' % self.tag
self.txt = urwid.Text(txt, wrap='clip')
self.__hash = hash((self.translated, self.txt))
normal_att = representation['normal']
focus_att = representation['focussed']
Expand Down
Loading