diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2013-10-30 20:57:35 +0000 |
---|---|---|
committer | Patrick Totzke <patricktotzke@gmail.com> | 2013-10-30 20:57:35 +0000 |
commit | fa10bfc2de105da819c8e11e913a44c3c1ac60a4 (patch) | |
tree | 667ce09b20524cbbc9902de64952a876adc6584b | |
parent | 10ad2f1feead866348b5bc0166d019950f334433 (diff) | |
parent | c48863799fdfac0f931d7efa5d7847ccf752f7e6 (diff) |
Merge branch '0.3.5-fix-cmdseqs-629'
-rw-r--r-- | alot/commands/__init__.py | 5 | ||||
-rw-r--r-- | alot/commands/bufferlist.py | 12 | ||||
-rw-r--r-- | alot/commands/globals.py | 47 | ||||
-rw-r--r-- | alot/commands/search.py | 4 | ||||
-rw-r--r-- | alot/commands/thread.py | 9 | ||||
-rw-r--r-- | alot/ui.py | 81 | ||||
-rw-r--r-- | docs/source/configuration/hooks.rst | 6 |
7 files changed, 98 insertions, 66 deletions
diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py index cadb6426..095c068d 100644 --- a/alot/commands/__init__.py +++ b/alot/commands/__init__.py @@ -28,6 +28,11 @@ class Command(object): pass +class CommandCanceled(Exception): + """ Exception triggered when an interactive command has been cancelled + """ + pass + COMMANDS = { 'search': {}, 'envelope': {}, diff --git a/alot/commands/bufferlist.py b/alot/commands/bufferlist.py index c638f8a0..ef58c6fc 100644 --- a/alot/commands/bufferlist.py +++ b/alot/commands/bufferlist.py @@ -21,7 +21,11 @@ class BufferCloseCommand(Command): def apply(self, ui): bufferlist = ui.current_buffer selected = bufferlist.get_selected_buffer() - ui.apply_command(globals.BufferCloseCommand(buffer=selected)) - if bufferlist is not selected: - bufferlist.rebuild() - ui.update() + d = ui.apply_command(globals.BufferCloseCommand(buffer=selected)) + + def cb(ignoreme): + if bufferlist is not selected: + bufferlist.rebuild() + ui.update() + d.addCallback(cb) + return d diff --git a/alot/commands/globals.py b/alot/commands/globals.py index 21b14afc..30331299 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -4,6 +4,7 @@ import os import code from twisted.internet import threads +from twisted.internet import defer import subprocess import email import urwid @@ -17,6 +18,7 @@ from alot.commands import Command, registerCommand from alot.completion import CommandLineCompleter from alot.commands import CommandParseError from alot.commands import commandfactory +from alot.commands import CommandCanceled from alot import buffers from alot.widgets.utils import DialogBox from alot import helper @@ -123,6 +125,8 @@ class PromptCommand(Command): # save into prompt history ui.commandprompthistory.append(cmdline) ui.apply_commandline(cmdline) + else: + raise CommandCanceled() @registerCommand(MODE, 'refresh') @@ -402,7 +406,7 @@ class BufferCloseCommand(Command): select='yes', cancel='no', msg_position='left')) == 'no'): - return + raise CommandCanceled() if len(ui.buffers) == 1: if settings.get('quit_on_last_bclose'): @@ -746,8 +750,8 @@ class ComposeCommand(Command): fromaddress = yield ui.prompt('From', completer=cmpl, tab=1) if fromaddress is None: - ui.notify('canceled') - return + raise CommandCanceled() + self.envelope.add('From', fromaddress) # add signature @@ -802,8 +806,8 @@ class ComposeCommand(Command): to = yield ui.prompt('To', completer=completer) if to is None: - ui.notify('canceled') - return + raise CommandCanceled() + self.envelope.add('To', to.strip(' \t\n,')) if settings.get('ask_subject') and \ @@ -811,8 +815,8 @@ class ComposeCommand(Command): subject = yield ui.prompt('Subject') logging.debug('SUBJECT: "%s"' % subject) if subject is None: - ui.notify('canceled') - return + raise CommandCanceled() + self.envelope.add('Subject', subject) if settings.get('compose_ask_tags'): @@ -820,8 +824,8 @@ class ComposeCommand(Command): tagsstring = yield ui.prompt('Tags', completer=comp) tags = filter(lambda x: x, tagsstring.split(',')) if tags is None: - ui.notify('canceled') - return + raise CommandCanceled() + self.envelope.tags = tags if self.attach: @@ -868,28 +872,3 @@ class MoveCommand(Command): else: ui.notify('unknown movement: ' + self.movement, priority='error') - - -class CommandSequenceCommand(Command): - - """Meta-Command that just applies a sequence of given Commands in order""" - - def __init__(self, cmdline='', **kwargs): - Command.__init__(self, **kwargs) - self.cmdline = cmdline.strip() - - @inlineCallbacks - def apply(self, ui): - # split commandline if necessary - for cmdstring in split_commandline(self.cmdline): - logging.debug('CMDSEQ: apply %s' % str(cmdstring)) - # translate cmdstring into :class:`Command` - try: - cmd = commandfactory(cmdstring, ui.mode) - # store cmdline for use with 'repeat' command - if cmd.repeatable: - ui.last_commandline = self.cmdline.lstrip() - except CommandParseError as e: - ui.notify(e.message, priority='error') - return - yield ui.apply_command(cmd) diff --git a/alot/commands/search.py b/alot/commands/search.py index ead500a6..e5307394 100644 --- a/alot/commands/search.py +++ b/alot/commands/search.py @@ -88,7 +88,7 @@ class RefinePromptCommand(Command): def apply(self, ui): sbuffer = ui.current_buffer oldquery = sbuffer.querystring - ui.apply_command(PromptCommand('refine ' + oldquery)) + return ui.apply_command(PromptCommand('refine ' + oldquery)) @registerCommand(MODE, 'retagprompt') @@ -107,7 +107,7 @@ class RetagPromptCommand(Command): elif tag: tags.append(tag) initial_tagstring = ','.join(sorted(tags)) + ',' - ui.apply_command(PromptCommand('retag ' + initial_tagstring)) + return ui.apply_command(PromptCommand('retag ' + initial_tagstring)) @registerCommand(MODE, 'tag', forced={'action': 'add'}, arguments=[ diff --git a/alot/commands/thread.py b/alot/commands/thread.py index d09008d2..3c03644e 100644 --- a/alot/commands/thread.py +++ b/alot/commands/thread.py @@ -17,6 +17,7 @@ from alot.commands.globals import ExternalCommand from alot.commands.globals import FlushCommand from alot.commands.globals import ComposeCommand from alot.commands.globals import MoveCommand +from alot.commands.globals import CommandCanceled from alot.commands.envelope import SendCommand from alot import completion from alot.db.utils import decode_header @@ -385,8 +386,8 @@ class BounceMailCommand(Command): completer = None to = yield ui.prompt('To', completer=completer) if to is None: - ui.notify('canceled') - return + raise CommandCanceled() + mail['Resent-To'] = to.strip(' \t\n,') logging.debug("bouncing mail") @@ -823,7 +824,7 @@ class SaveAttachmentCommand(Command): ui.notify('not a directory: %s' % self.path, priority='error') else: - ui.notify('canceled') + raise CommandCanceled() else: # save focussed attachment focus = ui.get_deep_focus() if isinstance(focus, AttachmentWidget): @@ -842,7 +843,7 @@ class SaveAttachmentCommand(Command): except (IOError, OSError) as e: ui.notify(str(e), priority='error') else: - ui.notify('canceled') + raise CommandCanceled() class OpenAttachmentCommand(Command): @@ -7,8 +7,10 @@ from twisted.internet import reactor, defer from settings import settings from buffers import BufferlistBuffer +from commands import commandfactory +from commands import CommandCanceled from alot.commands import CommandParseError -from alot.commands.globals import CommandSequenceCommand +from alot.helper import split_commandline from alot.helper import string_decode from alot.widgets.globals import CompleteEdit from alot.widgets.globals import ChoiceWidget @@ -162,15 +164,63 @@ class UI(object): def apply_commandline(self, cmdline): """ - Dispatches the interpretation of the command line string to - :class:`CommandSequenceCommand - <alot.commands.globals.CommandSequenceCommand>`. + interprets a command line string + + i.e., splits it into separate command strings, + instanciates :class:`Commands <alot.commands.Command>` + accordingly and applies then in sequence. :param cmdline: command line to interpret :type cmdline: str """ - cmd = CommandSequenceCommand(cmdline) - self.apply_command(cmd) + # remove initial spaces + cmdline = cmdline.lstrip() + + # we pass Commands one by one to `self.apply_command`. + # To properly call them in sequence, even if they trigger asyncronous + # code (return Deferreds), these applications happen in individual + # callback functions which are then used as callback chain to some + # trivial Deferred that immediately calls its first callback. This way, + # one callback may return a Deferred and thus postpone the application + # of the next callback (and thus Command-application) + + def apply_this_command(ignored, cmdstring): + logging.debug('%s command string: "%s"' % (self.mode, + str(cmdstring))) + #logging.debug('CMDSEQ: apply %s' % str(cmdstring)) + # translate cmdstring into :class:`Command` + #try: + cmd = commandfactory(cmdstring, self.mode) + #except CommandParseError, e: + # self.notify(e.message, priority='error') + # return + # store cmdline for use with 'repeat' command + if cmd.repeatable: + self.last_commandline = cmdline + return self.apply_command(cmd) + + # we initialize a deferred which is already triggered + # so that the first callbacks will be called immediately + d = defer.succeed(None) + + # split commandline if necessary + for cmdstring in split_commandline(cmdline): + d.addCallback(apply_this_command, cmdstring) + + # add sequence-wide error handler + def errorHandler(failure): + if failure.check(CommandParseError): + self.notify(failure.getErrorMessage(), priority='error') + elif failure.check(CommandCanceled): + self.notify("operation cancelled", priority='error') + else: + logging.error(failure.getTraceback()) + errmsg = failure.getErrorMessage() + if errmsg: + msg = "%s\n(check the log for details)" + self.notify(msg % errmsg, priority='error') + d.addErrback(errorHandler) + return d def _unhandeled_input(self, key): """ @@ -578,26 +628,17 @@ class UI(object): def call_posthook(retval_from_apply): if cmd.posthook: logging.info('calling post-hook') - return defer.maybeDeferred(cmd.posthook, ui=self, - dbm=self.dbman) - - # define error handler for Failures/Exceptions - # raised in cmd.apply() - def errorHandler(failure): - logging.error(failure.getTraceback()) - errmsg = failure.getErrorMessage() - if errmsg: - msg = "%s\n(check the log for details)" - self.notify( - msg % failure.getErrorMessage(), priority='error') + return defer.maybeDeferred(cmd.posthook, + ui=self, + dbm=self.dbman, + cmd=cmd) # call cmd.apply def call_apply(ignored): return defer.maybeDeferred(cmd.apply, self) prehook = cmd.prehook or (lambda **kwargs: None) - d = defer.maybeDeferred(prehook, ui=self, dbm=self.dbman) + d = defer.maybeDeferred(prehook, ui=self, dbm=self.dbman, cmd=cmd) d.addCallback(call_apply) d.addCallback(call_posthook) - d.addErrback(errorHandler) return d diff --git a/docs/source/configuration/hooks.rst b/docs/source/configuration/hooks.rst index 90b4fc30..e364d6f3 100644 --- a/docs/source/configuration/hooks.rst +++ b/docs/source/configuration/hooks.rst @@ -11,18 +11,20 @@ For every :ref:`COMMAND <usage.commands>` in mode :ref:`MODE <modes>`, the calla -- if defined -- will be called before and after the command is applied respectively. The signature for the pre-`send` hook in envelope mode for example looks like this: -.. py:function:: pre_envelope_send(ui=None, dbm=None) +.. py:function:: pre_envelope_send(ui=None, dbm=None, cmd=None) :param ui: the main user interface :type ui: :class:`alot.ui.UI` :param dbm: a database manager :type dbm: :class:`alot.db.manager.DBManager` + :param cmd: the Command instance that is being called + :type cmd: :class:`alot.commands.Command` Consider this pre-hook for the exit command, that logs a personalized goodbye message:: import logging from alot.settings import settings - def pre_global_exit(ui, dbm): + def pre_global_exit(**kwargs): accounts = settings.get_accounts() if accounts: logging.info('goodbye, %s!' % accounts[0].realname) |