summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2013-10-30 20:57:35 +0000
committerPatrick Totzke <patricktotzke@gmail.com>2013-10-30 20:57:35 +0000
commitfa10bfc2de105da819c8e11e913a44c3c1ac60a4 (patch)
tree667ce09b20524cbbc9902de64952a876adc6584b
parent10ad2f1feead866348b5bc0166d019950f334433 (diff)
parentc48863799fdfac0f931d7efa5d7847ccf752f7e6 (diff)
Merge branch '0.3.5-fix-cmdseqs-629'
-rw-r--r--alot/commands/__init__.py5
-rw-r--r--alot/commands/bufferlist.py12
-rw-r--r--alot/commands/globals.py47
-rw-r--r--alot/commands/search.py4
-rw-r--r--alot/commands/thread.py9
-rw-r--r--alot/ui.py81
-rw-r--r--docs/source/configuration/hooks.rst6
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):
diff --git a/alot/ui.py b/alot/ui.py
index 971345ac..cc578725 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -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)