diff options
-rw-r--r-- | alot/buffers/search.py | 32 | ||||
-rw-r--r-- | alot/commands/envelope.py | 10 | ||||
-rw-r--r-- | alot/ui.py | 167 |
3 files changed, 126 insertions, 83 deletions
diff --git a/alot/buffers/search.py b/alot/buffers/search.py index 15e9f41f..7d327f23 100644 --- a/alot/buffers/search.py +++ b/alot/buffers/search.py @@ -87,7 +87,9 @@ class SearchBuffer(Buffer): """shows a result list of threads for a query""" modename = 'search' - threads = [] + + _result_count_val = None + _thread_count_val = None def __init__(self, ui, initialquery='', sort_order=None): self.dbman = ui.dbman @@ -95,8 +97,6 @@ class SearchBuffer(Buffer): self.querystring = initialquery default_order = settings.get('search_threads_sort_order') self.sort_order = sort_order or default_order - self.result_count = 0 - self.thread_count = 0 self.proc = None # process that fills our pipe self.rebuild() Buffer.__init__(self, ui, self.body) @@ -104,15 +104,26 @@ class SearchBuffer(Buffer): def __str__(self): formatstring = '[search] for "%s" (%d message%s in %d thread%s)' return formatstring % (self.querystring, - self.result_count, 's' if self.result_count > 1 else '', - self.thread_count, 's' if self.thread_count > 1 else '') + self._result_count, 's' if self._result_count > 1 else '', + self._thread_count, 's' if self._thread_count > 1 else '') + + @property + def _result_count(self): + if self._result_count_val is None: + self._result_count_val = self.dbman.count_messages(self.querystring) + return self._result_count_val + @property + def _thread_count(self): + if self._thread_count_val is None: + self._thread_count_val = self.dbman.count_threads(self.querystring) + return self._thread_count_val def get_info(self): info = {} info['querystring'] = self.querystring - info['result_count'] = self.result_count - info['thread_count'] = self.thread_count - info['result_count_positive'] = 's' if self.result_count > 1 else '' + info['result_count'] = self._result_count + info['thread_count'] = self._thread_count + info['result_count_positive'] = 's' if info['result_count'] > 1 else '' return info def cleanup(self): @@ -134,9 +145,10 @@ class SearchBuffer(Buffer): if exclude_tags: exclude_tags = [t for t in exclude_tags.split(';') if t] + self._result_count_val = None + self._thread_count_val = None + try: - self.result_count = self.dbman.count_messages(self.querystring) - self.thread_count = self.dbman.count_threads(self.querystring) self.pipe, self.proc = self.dbman.get_threads(self.querystring, self.sort_order, exclude_tags) diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 17d32f89..5aebadc5 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -242,11 +242,11 @@ class SendCommand(Command): self.mail = self.envelope.construct_mail() self.mail = self.mail.as_string(policy=email.policy.SMTP) except GPGProblem as e: - ui.clear_notify([clearme]) + ui.clear_notify(clearme) ui.notify(str(e), priority='error') return - ui.clear_notify([clearme]) + ui.clear_notify(clearme) # determine account to use for sending msg = self.mail @@ -273,12 +273,12 @@ class SendCommand(Command): if self.envelope is not None: self.envelope.account = account self.envelope.sending = False - ui.clear_notify([clearme]) + ui.clear_notify(clearme) logging.error(traceback.format_exc()) errmsg = 'failed to send: {}'.format(e) ui.notify(errmsg, priority='error', block=True) except StoreMailError as e: - ui.clear_notify([clearme]) + ui.clear_notify(clearme) logging.error(traceback.format_exc()) errmsg = 'could not store mail: {}'.format(e) ui.notify(errmsg, priority='error', block=True) @@ -289,7 +289,7 @@ class SendCommand(Command): self.envelope.sent_time = datetime.datetime.now() initial_tags = self.envelope.tags logging.debug('mail sent successfully') - ui.clear_notify([clearme]) + ui.clear_notify(clearme) if self.envelope_buffer is not None: cmd = commands.globals.BufferCloseCommand(self.envelope_buffer) await ui.apply_command(cmd) @@ -1,6 +1,8 @@ # Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com> # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file + +import collections import logging import os import signal @@ -19,7 +21,6 @@ from .commands import commandfactory from .commands import CommandCanceled from .commands import CommandParseError from .helper import split_commandline -from .helper import string_decode from .helper import get_xdg_env from .widgets.globals import CompleteEdit from .widgets.globals import ChoiceWidget @@ -33,6 +34,58 @@ async def periodic(callable_, period, *args, **kwargs): logging.error('error in loop hook %s', str(e)) await asyncio.sleep(period) +class _StatusBar(urwid.WidgetWrap): + + _text = None + _eventloop = None + + _update_task = None + + def __init__(self, eventloop): + self._eventloop = eventloop + + self._text = [] + self._text.append(urwid.Text('', align = 'left')) + self._text.append(urwid.Text('', align = 'right')) + + columns = urwid.Columns([self._text[0], ('pack', self._text[1])]) + + footer_att = settings.get_theming_attribute('global', 'footer') + wgt = urwid.AttrMap(columns, footer_att) + + super().__init__(wgt) + + def update(self, buf, info): + if (self._update_task is not None and + not self._update_task.done()): + self._update_task.cancel() + + if buf is None: + return + + # getting info from a buffer can be a heavy operation, so we first build + # the text with fake <?> values and launch an async task to do the + # update with real values + fmtstrings = settings.get(buf.modename + '_statusbar', ('', '')) + initial_info = collections.defaultdict(lambda: '<?>', info) + logging.info(initial_info['querystring']) + self._do_update(fmtstrings, initial_info) + + async def sb_update(): + buf_info = buf.get_info() + buf_info.update(info) + self._do_update(fmtstrings, buf_info) + + self._eventloop.create_task(sb_update()) + + # XXX + #pending_writes = len(self.dbman.writequeue) + #if pending_writes > 0: + # righttxt = ('|' * pending_writes) + ' ' + righttxt + + def _do_update(self, fmtstrings, info): + for wgt, fmt in zip(self._text, fmtstrings): + wgt.set_text(fmt.format_map(info)) class UI: """ @@ -42,6 +95,16 @@ class UI: responsible for opening, closing and focussing buffers. """ + # (possibly-empty) Pile of notification messages + _notification_bar = None + # _StatusBar if enabled, otherwise None + _status_bar = None + # footer widget - a Pile containing the notification bar and the status bar + _footer_wgt = None + + # running asyncio event loop + _loop = None + def __init__(self, dbman, initialcmdline): """ :param dbman: :class:`~alot.db.DBManager` @@ -71,10 +134,17 @@ class UI: self.last_commandline = None """saves the last executed commandline""" + self._loop = asyncio.get_event_loop() + # define empty notification pile - self._notificationbar = None + self._notification_bar = urwid.Pile([]) + self._footer_wgt = urwid.Pile([self._notification_bar]) + # should we show a status bar? - self._show_statusbar = settings.get('show_statusbar') + if settings.get('show_statusbar'): + self._status_bar = _StatusBar(self._loop) + self._footer_wgt.contents.append((self._status_bar, ('pack', None))) + # pass keypresses to the root widget and never interpret bindings self._passall = False # indicates "input lock": only focus move commands are interpreted @@ -89,8 +159,10 @@ class UI: urwid.set_encoding('utf-8') # create root widget - global_att = settings.get_theming_attribute('global', 'body') mainframe = urwid.Frame(urwid.SolidFill()) + mainframe.set_footer(self._footer_wgt) + + global_att = settings.get_theming_attribute('global', 'body') self.root_widget = urwid.AttrMap(mainframe, global_att) signal.signal(signal.SIGINT, self.handle_signal) @@ -119,15 +191,12 @@ class UI: unhandled_input=self._unhandled_input, input_filter=self._input_filter) - loop = asyncio.get_event_loop() # Create a task for the periodic hook loop_hook = settings.get_hook('loop_hook') if loop_hook: # In Python 3.7 a nice aliase `asyncio.create_task` was added - loop.create_task( - periodic( - loop_hook, settings.get('periodic_hook_frequency'), - ui=self)) + self._loop.create_task( + periodic(loop_hook, settings.get('periodic_hook_frequency'), ui=self)) # set up colours colourmode = int(settings.get('colourmode')) @@ -135,7 +204,7 @@ class UI: self.mainloop.screen.set_terminal_properties(colors=colourmode) logging.debug('fire first command') - loop.create_task(self.apply_commandline(initialcmdline)) + self._loop.create_task(self.apply_commandline(initialcmdline)) # start urwids mainloop self.mainloop.run() @@ -480,22 +549,14 @@ class UI: """ return [x for x in self.buffers if isinstance(x, t)] - def clear_notify(self, messages): + def clear_notify(self, msg): """ Clears notification popups. Call this to ged rid of messages that don't time out. - - :param messages: The popups to remove. This should be exactly - what :meth:`notify` returned when creating the popup """ - newpile = self._notificationbar.widget_list - for l in messages: - if l in newpile: - newpile.remove(l) - if newpile: - self._notificationbar = urwid.Pile(newpile) - else: - self._notificationbar = None + if msg in self._notification_bar.contents: + self._notification_bar.contents.remove(msg) + self.update() def choice(self, message, choices=None, select=None, cancel=None, @@ -589,17 +650,14 @@ class UI: cols = urwid.Columns([urwid.Text(msg)]) att = settings.get_theming_attribute('global', 'notify_' + prio) return urwid.AttrMap(cols, att) - msgs = [build_line(message, priority)] + msg = (build_line(message, priority), ('pack', None)) + + self._notification_bar.contents.append(msg) - if not self._notificationbar: - self._notificationbar = urwid.Pile(msgs) - else: - newpile = self._notificationbar.widget_list + msgs - self._notificationbar = urwid.Pile(newpile) self.update() def clear(*_): - self.clear_notify(msgs) + self.clear_notify(msg) if block: # put "cancel to continue" widget as overlay on main widget @@ -616,7 +674,7 @@ class UI: if timeout == 0: timeout = settings.get('notify_timeout') self.mainloop.set_alarm_in(timeout, clear) - return msgs[0] + return msg def update(self, redraw=True): """redraw interface""" @@ -627,53 +685,26 @@ class UI: if self.current_buffer: mainframe.set_body(self.current_buffer) - # footer - lines = [] - if self._notificationbar: # .get_text()[0] != ' ': - lines.append(self._notificationbar) - if self._show_statusbar: - lines.append(self.build_statusbar()) + self._update_statusbar() - if lines: - mainframe.set_footer(urwid.Pile(lines)) - else: - mainframe.set_footer(None) # force a screen redraw if self.mainloop.screen.started and redraw: self.mainloop.draw_screen() - def build_statusbar(self): + def _update_statusbar(self): """construct and return statusbar widget""" - info = {} + if not self._status_bar: + return + cb = self.current_buffer - btype = None - if cb is not None: - info = cb.get_info() - btype = cb.modename - info['buffer_no'] = self.buffers.index(cb) - info['buffer_type'] = btype + info = {} info['pending_writes'] = len(self.dbman.writequeue) - info['input_queue'] = ' '.join(self.input_queue) - - lefttxt = righttxt = '' - if cb is not None: - lefttxt, righttxt = settings.get(btype + '_statusbar', ('', '')) - lefttxt = string_decode(lefttxt, 'UTF-8') - lefttxt = lefttxt.format(**info) - righttxt = string_decode(righttxt, 'UTF-8') - righttxt = righttxt.format(**info) - - footerleft = urwid.Text(lefttxt, align='left') - pending_writes = len(self.dbman.writequeue) - if pending_writes > 0: - righttxt = ('|' * pending_writes) + ' ' + righttxt - footerright = urwid.Text(righttxt, align='right') - columns = urwid.Columns([ - footerleft, - ('pack', footerright)]) - footer_att = settings.get_theming_attribute('global', 'footer') - return urwid.AttrMap(columns, footer_att) + info['input_queue'] = ' '.join(self.input_queue) + info['buffer_no'] = self.buffers.index(cb) + info['buffer_type'] = cb.modename + + self._status_bar.update(cb, info) async def apply_command(self, cmd): """ |