summaryrefslogtreecommitdiff
path: root/alot/ui.py
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-04-23 14:33:51 +0200
committerAnton Khirnov <anton@khirnov.net>2020-04-23 14:33:51 +0200
commitcb069bdeab97bbe4be47e2a0041e5a4a404796fd (patch)
tree73c8bf673496cc09f9eb81f45f0f19bd21ad724d /alot/ui.py
parentaa2d9e486219aff9abb9c113ba3e3de19cbda3db (diff)
ui: rewrite notification/status bar handling
Do not recreate all the widgets on every update, just update the widget contents. Make the statusbar update async, since some calls to get_info() can take a long time (especially noticeable for counting threads for searches with many results).
Diffstat (limited to 'alot/ui.py')
-rw-r--r--alot/ui.py167
1 files changed, 99 insertions, 68 deletions
diff --git a/alot/ui.py b/alot/ui.py
index 39999ad8..c38667c4 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -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):
"""