Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFD: Refine taglist #1546

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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