summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--alot/__init__.py1
-rw-r--r--alot/buffers.py (renamed from alot/buffer.py)4
-rw-r--r--alot/command.py1314
-rw-r--r--alot/commands/__init__.py142
-rw-r--r--alot/commands/bufferlist.py1
-rw-r--r--alot/commands/envelope.py225
-rw-r--r--alot/commands/globals.py462
-rw-r--r--alot/commands/search.py150
-rw-r--r--alot/commands/taglist.py12
-rw-r--r--alot/commands/thread.py412
-rw-r--r--alot/completion.py6
-rw-r--r--alot/defaults/alot.rc11
-rwxr-xr-xalot/init.py21
-rw-r--r--alot/ui.py29
-rwxr-xr-xsetup.py2
15 files changed, 1446 insertions, 1346 deletions
diff --git a/alot/__init__.py b/alot/__init__.py
index 4a84dc9b..8d2b1835 100644
--- a/alot/__init__.py
+++ b/alot/__init__.py
@@ -24,3 +24,4 @@ __author_email__ = "patricktotzke@gmail.com"
__description__ = "Command-line MUA using notmuch mail"
__url__ = "https://github.com/pazz/alot"
__license__ = "Licensed under the GNU GPL v3+."
+
diff --git a/alot/buffer.py b/alot/buffers.py
index b313c231..57fe9ecb 100644
--- a/alot/buffer.py
+++ b/alot/buffers.py
@@ -22,7 +22,7 @@ from notmuch.globals import NotmuchError
import widgets
import settings
-import command
+import commands
from walker import IteratorWalker
from message import decode_header
@@ -252,7 +252,7 @@ class ThreadBuffer(Buffer):
mw.fold(visible=True)
if 'unread' in msg.get_tags():
msg.remove_tags(['unread'])
- self.ui.apply_command(command.FlushCommand())
+ self.ui.apply_command(commands.globals.FlushCommand())
class TagListBuffer(Buffer):
diff --git a/alot/command.py b/alot/command.py
deleted file mode 100644
index 34c1928a..00000000
--- a/alot/command.py
+++ /dev/null
@@ -1,1314 +0,0 @@
-"""
-This file is part of alot.
-
-Alot is free software: you can redistribute it and/or modify it
-under the terms of the GNU General Public License as published by the
-Free Software Foundation, either version 3 of the License, or (at your
-option) any later version.
-
-Alot is distributed in the hope that it will be useful, but WITHOUT
-ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-for more details.
-
-You should have received a copy of the GNU General Public License
-along with notmuch. If not, see <http://www.gnu.org/licenses/>.
-
-Copyright (C) 2011 Patrick Totzke <patricktotzke@gmail.com>
-"""
-import os
-import re
-import code
-import glob
-import logging
-import threading
-import subprocess
-import shlex
-import email
-import tempfile
-from email import Charset
-from email.header import Header
-from email.mime.text import MIMEText
-from email.mime.multipart import MIMEMultipart
-import urwid
-from twisted.internet import defer
-
-import buffer
-import settings
-import widgets
-import completion
-import helper
-from db import DatabaseROError
-from db import DatabaseLockedError
-from completion import ContactsCompleter
-from completion import AccountCompleter
-from message import decode_to_unicode
-from message import decode_header
-from message import encode_header
-
-
-class Command(object):
- """base class for commands"""
- def __init__(self, prehook=None, posthook=None):
- self.prehook = prehook
- self.posthook = posthook
- self.undoable = False
- self.help = self.__doc__
-
- def apply(self, caller):
- pass
-
-
-class ExitCommand(Command):
- """shuts the MUA down cleanly"""
- @defer.inlineCallbacks
- def apply(self, ui):
- if settings.config.getboolean('general', 'bug_on_exit'):
- if (yield ui.choice('realy quit?', select='yes', cancel='no',
- msg_position='left')) == 'no':
- return
- ui.exit()
-
-
-class OpenThreadCommand(Command):
- """open a new thread-view buffer"""
- def __init__(self, thread=None, **kwargs):
- self.thread = thread
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- if not self.thread:
- self.thread = ui.current_buffer.get_selected_thread()
- if self.thread:
- query = ui.current_buffer.querystring
- ui.logger.info('open thread view for %s' % self.thread)
-
- sb = buffer.ThreadBuffer(ui, self.thread)
- ui.buffer_open(sb)
- sb.unfold_matching(query)
-
-
-class SearchCommand(Command):
- """open a new search buffer"""
- def __init__(self, query, **kwargs):
- """
- :param query: initial querystring
- """
- self.query = query
- Command.__init__(self, **kwargs)
-
- @defer.inlineCallbacks
- def apply(self, ui):
- if self.query:
- if self.query == '*' and ui.current_buffer:
- s = 'really search for all threads? This takes a while..'
- if (yield ui.choice(s, select='yes', cancel='no')) == 'no':
- return
- open_searches = ui.get_buffers_of_type(buffer.SearchBuffer)
- to_be_focused = None
- for sb in open_searches:
- if sb.querystring == self.query:
- to_be_focused = sb
- if to_be_focused:
- ui.buffer_focus(to_be_focused)
- else:
- ui.buffer_open(buffer.SearchBuffer(ui, self.query))
- else:
- ui.notify('empty query string')
-
-
-class PromptCommand(Command):
- """starts commandprompt"""
- def __init__(self, startstring=u'', **kwargs):
- self.startstring = startstring
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- ui.commandprompt(self.startstring)
-
-
-class RefreshCommand(Command):
- """refreshes the current buffer"""
- def apply(self, ui):
- ui.current_buffer.rebuild()
- ui.update()
-
-
-class ExternalCommand(Command):
- """calls external command"""
- def __init__(self, commandstring, path=None, spawn=False, refocus=True,
- in_thread=False, on_success=None, **kwargs):
- """
- :param commandstring: the command to call
- :type commandstring: str
- :param path: a path to a file (or None)
- :type path: str
- :param spawn: run command in a new terminal
- :type spawn: boolean
- :param in_thread: run asynchronously, don't block alot
- :type in_thread: boolean
- :param refocus: refocus calling buffer after cmd termination
- :type refocus: boolean
- :param on_success: code to execute after command successfully exited
- :type on_success: callable
- """
- self.commandstring = commandstring
- self.path = path
- self.spawn = spawn
- self.refocus = refocus
- self.in_thread = in_thread
- self.on_success = on_success
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- callerbuffer = ui.current_buffer
-
- def afterwards(data):
- if callable(self.on_success) and data == 'success':
- self.on_success()
- if self.refocus and callerbuffer in ui.buffers:
- ui.logger.info('refocussing')
- ui.buffer_focus(callerbuffer)
-
- write_fd = ui.mainloop.watch_pipe(afterwards)
-
- def thread_code(*args):
- if self.path:
- if '{}' in self.commandstring:
- cmd = self.commandstring.replace('{}',
- helper.shell_quote(self.path))
- else:
- cmd = '%s %s' % (self.commandstring,
- helper.shell_quote(self.path))
- else:
- cmd = self.commandstring
-
- if self.spawn:
- cmd = '%s %s' % (settings.config.get('general',
- 'terminal_cmd'),
- cmd)
- cmd = cmd.encode('utf-8', errors='ignore')
- ui.logger.info('calling external command: %s' % cmd)
- returncode = subprocess.call(shlex.split(cmd))
- if returncode == 0:
- os.write(write_fd, 'success')
-
- if self.in_thread:
- thread = threading.Thread(target=thread_code)
- thread.start()
- else:
- ui.mainloop.screen.stop()
- thread_code()
- ui.mainloop.screen.start()
-
-
-class EditCommand(ExternalCommand):
- def __init__(self, path, spawn=None, **kwargs):
- self.path = path
- if spawn != None:
- self.spawn = spawn
- else:
- self.spawn = settings.config.getboolean('general', 'spawn_editor')
- editor_cmd = settings.config.get('general', 'editor_cmd')
-
- ExternalCommand.__init__(self, editor_cmd, path=self.path,
- spawn=self.spawn, in_thread=self.spawn,
- **kwargs)
-
-
-class PythonShellCommand(Command):
- """opens an interactive shell for introspection"""
- def apply(self, ui):
- ui.mainloop.screen.stop()
- code.interact(local=locals())
- ui.mainloop.screen.start()
-
-
-class BufferCloseCommand(Command):
- """close a buffer"""
- def __init__(self, buffer=None, focussed=False, **kwargs):
- """
- :param buffer: the selected buffer
- :type buffer: `alot.buffer.Buffer`
- """
- self.buffer = buffer
- self.focussed = focussed
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- if self.focussed:
- #if in bufferlist, this is ugly.
- self.buffer = ui.current_buffer.get_selected_buffer()
- elif not self.buffer:
- self.buffer = ui.current_buffer
- ui.buffer_close(self.buffer)
- ui.buffer_focus(ui.current_buffer)
-
-
-class BufferFocusCommand(Command):
- """focus a buffer"""
- def __init__(self, buffer=None, offset=0, **kwargs):
- """
- :param buffer: the buffer to focus
- :type buffer: `alot.buffer.Buffer`
- """
- self.buffer = buffer
- self.offset = offset
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- if self.offset:
- idx = ui.buffers.index(ui.current_buffer)
- num = len(ui.buffers)
- self.buffer = ui.buffers[(idx + self.offset) % num]
- else:
- if not self.buffer:
- self.buffer = ui.current_buffer.get_selected_buffer()
- ui.buffer_focus(self.buffer)
-
-
-class OpenBufferlistCommand(Command):
- """open a bufferlist buffer"""
- def __init__(self, filtfun=None, **kwargs):
- self.filtfun = filtfun
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- blists = ui.get_buffers_of_type(buffer.BufferlistBuffer)
- if blists:
- ui.buffer_focus(blists[0])
- else:
- ui.buffer_open(buffer.BufferlistBuffer(ui, self.filtfun))
-
-
-class TagListCommand(Command):
- """open a taglisat buffer"""
- def __init__(self, filtfun=None, **kwargs):
- self.filtfun = filtfun
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- tags = ui.dbman.get_all_tags()
- buf = buffer.TagListBuffer(ui, tags, self.filtfun)
- ui.buffers.append(buf)
- buf.rebuild()
- ui.buffer_focus(buf)
-
-
-class FlushCommand(Command):
- """Flushes writes to the index. Retries until committed"""
- def apply(self, ui):
- try:
- ui.dbman.flush()
- except DatabaseLockedError:
- timeout = settings.config.getint('general', 'flush_retry_timeout')
-
- def f(*args):
- self.apply(ui)
- ui.mainloop.set_alarm_in(timeout, f)
- ui.notify('index locked, will try again in %d secs' % timeout)
- ui.update()
- return
-
-
-class ToggleThreadTagCommand(Command):
- """toggles tag in given or currently selected thread"""
- def __init__(self, tags, thread=None, **kwargs):
- assert tags
- self.thread = thread
- self.tags = set(tags)
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- if not self.thread:
- self.thread = ui.current_buffer.get_selected_thread()
- if not self.thread:
- return
- try:
- self.thread.set_tags(set(self.thread.get_tags()) ^ self.tags)
- except DatabaseROError:
- ui.notify('index in read-only mode', priority='error')
- return
-
- # flush index
- ui.apply_command(FlushCommand())
-
- # update current buffer
- # TODO: what if changes not yet flushed?
- cb = ui.current_buffer
- if isinstance(cb, buffer.SearchBuffer):
- # refresh selected threadline
- threadwidget = cb.get_selected_threadline()
- threadwidget.rebuild() # rebuild and redraw the line
- #remove line from searchlist if thread doesn't match the query
- qs = "(%s) AND thread:%s" % (cb.querystring,
- self.thread.get_thread_id())
- if ui.dbman.count_messages(qs) == 0:
- ui.logger.debug('remove: %s' % self.thread)
- cb.threadlist.remove(threadwidget)
- cb.result_count -= self.thread.get_total_messages()
- ui.update()
- elif isinstance(cb, buffer.ThreadBuffer):
- pass
-
-
-class ComposeCommand(Command):
- """compose a new email and open an envelope for it"""
- def __init__(self, mail=None, headers={}, **kwargs):
- Command.__init__(self, **kwargs)
- if not mail:
- self.mail = MIMEMultipart()
- self.mail.attach(MIMEText('', 'plain', 'UTF-8'))
- else:
- self.mail = mail
- for key, value in headers.items():
- self.mail[key] = encode_header(key, value)
-
- @defer.inlineCallbacks
- def apply(self, ui):
- # TODO: fill with default header (per account)
- # get From header
- if not 'From' in self.mail:
- accounts = ui.accountman.get_accounts()
- if len(accounts) == 0:
- ui.notify('no accounts set')
- return
- elif len(accounts) == 1:
- a = accounts[0]
- else:
- cmpl = AccountCompleter(ui.accountman)
- fromaddress = yield ui.prompt(prefix='From>', completer=cmpl,
- tab=1)
- validaddresses = [a.address for a in accounts] + [None]
- while fromaddress not in validaddresses: # TODO: not cool
- ui.notify('no account for this address. (<esc> cancels)')
- fromaddress = yield ui.prompt(prefix='From>',
- completer=cmpl)
- if not fromaddress:
- ui.notify('canceled')
- return
- a = ui.accountman.get_account_by_address(fromaddress)
- self.mail['From'] = "%s <%s>" % (a.realname, a.address)
-
- #get To header
- if 'To' not in self.mail:
- name, addr = email.Utils.parseaddr(unicode(self.mail.get('From')))
- a = ui.accountman.get_account_by_address(addr)
-
- allbooks = not settings.config.getboolean('general',
- 'complete_matching_abook_only')
- ui.logger.debug(allbooks)
- abooks = ui.accountman.get_addressbooks(order=[a],
- append_remaining=allbooks)
- ui.logger.debug(abooks)
- to = yield ui.prompt(prefix='To>',
- completer=ContactsCompleter(abooks))
- if to == None:
- ui.notify('canceled')
- return
- self.mail['To'] = encode_header('to', to)
- if settings.config.getboolean('general', 'ask_subject') and \
- not 'Subject' in self.mail:
- subject = yield ui.prompt(prefix='Subject>')
- if subject == None:
- ui.notify('canceled')
- return
- self.mail['Subject'] = encode_header('subject', subject)
-
- ui.apply_command(EnvelopeEditCommand(mail=self.mail))
-
-
-# SEARCH
-class RetagPromptCommand(Command):
- """start a commandprompt to retag selected threads' tags
- this is needed to fill the prompt with the current tags..
- """
- def apply(self, ui):
- thread = ui.current_buffer.get_selected_thread()
- if not thread:
- return
- initial_tagstring = ','.join(thread.get_tags())
- ui.commandprompt('retag ' + initial_tagstring)
-
-
-class RetagCommand(Command):
- """tag selected thread"""
- def __init__(self, tagsstring=u'', thread=None, **kwargs):
- self.tagsstring = tagsstring
- self.thread = thread
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- if not self.thread:
- self.thread = ui.current_buffer.get_selected_thread()
- if not self.thread:
- return
- tags = filter(lambda x: x, self.tagsstring.split(','))
- ui.logger.info("got %s:%s" % (self.tagsstring, tags))
- try:
- self.thread.set_tags(tags)
- except DatabaseROError:
- ui.notify('index in read-only mode', priority='error')
- return
-
- # flush index
- ui.apply_command(FlushCommand())
-
- # refresh selected threadline
- sbuffer = ui.current_buffer
- threadwidget = sbuffer.get_selected_threadline()
- threadwidget.rebuild() # rebuild and redraw the line
-
-
-class RefineCommand(Command):
- """refine the query of the currently open searchbuffer"""
- def __init__(self, query=None, **kwargs):
- self.querystring = query
- Command.__init__(self, **kwargs)
-
- @defer.inlineCallbacks
- def apply(self, ui):
- if self.querystring:
- if self.querystring == '*':
- s = 'really search for all threads? This takes a while..'
- if (yield ui.choice(s, select='yes', cancel='no')) == 'no':
- return
- sbuffer = ui.current_buffer
- oldquery = sbuffer.querystring
- if self.querystring not in [None, oldquery]:
- sbuffer.querystring = self.querystring
- sbuffer = ui.current_buffer
- sbuffer.rebuild()
- ui.update()
- else:
- ui.notify('empty query string')
-
-
-class RefinePromptCommand(Command):
- """prompt to change current search buffers query"""
- def apply(self, ui):
- sbuffer = ui.current_buffer
- oldquery = sbuffer.querystring
- ui.commandprompt('refine ' + oldquery)
-
-
-# THREAD
-class ReplyCommand(Command):
- """format reply for currently selected message and open envelope for it"""
- def __init__(self, message=None, groupreply=False, **kwargs):
- """
- :param message: the original message to reply to
- :type message: `alot.message.Message`
- :param groupreply: copy other recipients from Bcc/Cc/To to the reply
- :type groupreply: boolean
- """
- self.message = message
- self.groupreply = groupreply
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- if not self.message:
- self.message = ui.current_buffer.get_selected_message()
- mail = self.message.get_email()
- # set body text
- name, address = self.message.get_author()
- timestamp = self.message.get_date()
- qf = settings.hooks.get('reply_prefix')
- if qf:
- quotestring = qf(name, address, timestamp,
- ui=ui, dbm=ui.dbman, aman=ui.accountman,
- log=ui.logger, config=settings.config)
- else:
- quotestring = 'Quoting %s (%s)\n' % (name, timestamp)
- mailcontent = quotestring
- for line in self.message.accumulate_body().splitlines():
- mailcontent += '>' + line + '\n'
-
- Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
- bodypart = MIMEText(mailcontent.encode('utf-8'), 'plain', 'UTF-8')
- reply = MIMEMultipart()
- reply.attach(bodypart)
-
- # copy subject
- subject = mail.get('Subject', '')
- if not subject.startswith('Re:'):
- subject = 'Re: ' + subject
- reply['Subject'] = Header(subject.encode('utf-8'), 'UTF-8').encode()
-
- # set From
- my_addresses = ui.accountman.get_addresses()
- matched_address = ''
- in_to = [a for a in my_addresses if a in mail.get('To', '')]
- if in_to:
- matched_address = in_to[0]
- else:
- cc = mail.get('Cc', '') + mail.get('Bcc', '')
- in_cc = [a for a in my_addresses if a in cc]
- if in_cc:
- matched_address = in_cc[0]
- if matched_address:
- account = ui.accountman.get_account_by_address(matched_address)
- fromstring = '%s <%s>' % (account.realname, account.address)
- reply['From'] = encode_header('From', fromstring)
-
- # set To
- del(reply['To'])
- if self.groupreply:
- cleared = self.clear_my_address(my_addresses, mail.get('To', ''))
- if cleared:
- logging.info(mail['From'] + ', ' + cleared)
- to = mail['From'] + ', ' + cleared
- reply['To'] = encode_header('To', to)
- logging.info(reply['To'])
- else:
- reply['To'] = encode_header('To', mail['From'])
- # copy cc and bcc for group-replies
- if 'Cc' in mail:
- cc = self.clear_my_address(my_addresses, mail['Cc'])
- reply['Cc'] = encode_header('Cc', cc)
- if 'Bcc' in mail:
- bcc = self.clear_my_address(my_addresses, mail['Bcc'])
- reply['Bcc'] = encode_header('Bcc', bcc)
- else:
- reply['To'] = encode_header('To', mail['From'])
-
- # set In-Reply-To header
- del(reply['In-Reply-To'])
- reply['In-Reply-To'] = '<%s>' % self.message.get_message_id()
-
- # set References header
- old_references = mail.get('References', '')
- if old_references:
- old_references = old_references.split()
- references = old_references[-8:]
- if len(old_references) > 8:
- references = old_references[:1] + references
- references.append('<%s>' % self.message.get_message_id())
- reply['References'] = ' '.join(references)
- else:
- reply['References'] = '<%s>' % self.message.get_message_id()
-
- ui.apply_command(ComposeCommand(mail=reply))
-
- def clear_my_address(self, my_addresses, value):
- new_value = []
- for entry in value.split(','):
- if not [a for a in my_addresses if a in entry]:
- new_value.append(entry.strip())
- return ', '.join(new_value)
-
-
-class ForwardCommand(Command):
- def __init__(self, message=None, inline=False, **kwargs):
- """
- :param message: the original message to forward. If None, the currently
- selected one is used
- :type message: `alot.message.Message`
- :param inline: Copy originals body text instead of attaching the whole
- mail
- :type inline: boolean
- """
- self.message = message
- self.inline = inline
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- if not self.message:
- self.message = ui.current_buffer.get_selected_message()
- mail = self.message.get_email()
-
- reply = MIMEMultipart()
- Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
- if self.inline: # inline mode
- # set body text
- name, address = self.message.get_author()
- timestamp = self.message.get_date()
- qf = settings.hooks.get('forward_prefix')
- if qf:
- quote = qf(name, address, timestamp,
- ui=ui, dbm=ui.dbman, aman=ui.accountman,
- log=ui.logger, config=settings.config)
- else:
- quote = 'Forwarded message from %s (%s):\n' % (name, timestamp)
- mailcontent = quote
- for line in self.message.accumulate_body().splitlines():
- mailcontent += '>' + line + '\n'
-
- bodypart = MIMEText(mailcontent.encode('utf-8'), 'plain', 'UTF-8')
- reply.attach(bodypart)
-
- else: # attach original mode
- # create empty text msg
- bodypart = MIMEText('', 'plain', 'UTF-8')
- reply.attach(bodypart)
- # attach original msg
- reply.attach(mail)
-
- # copy subject
- subject = mail.get('Subject', '')
- subject = 'Fwd: ' + subject
- reply['Subject'] = Header(subject.encode('utf-8'), 'UTF-8').encode()
-
- # set From
- my_addresses = ui.accountman.get_addresses()
- matched_address = ''
- in_to = [a for a in my_addresses if a in mail.get('To', '')]
- if in_to:
- matched_address = in_to[0]
- else:
- cc = mail.get('Cc', '') + mail.get('Bcc', '')
- in_cc = [a for a in my_addresses if a in cc]
- if in_cc:
- matched_address = in_cc[0]
- if matched_address:
- account = ui.accountman.get_account_by_address(matched_address)
- fromstring = '%s <%s>' % (account.realname, account.address)
- reply['From'] = encode_header('From', fromstring)
- ui.apply_command(ComposeCommand(mail=reply))
-
-
-class FoldMessagesCommand(Command):
- def __init__(self, all=False, visible=None, **kwargs):
- self.all = all
- self.visible = visible
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- lines = []
- if not self.all:
- lines.append(ui.current_buffer.get_selection())
- else:
- lines = ui.current_buffer.get_message_widgets()
-
- for widget in lines:
- # in case the thread is yet unread, remove this tag
- msg = widget.get_message()
- if self.visible or (self.visible == None and widget.folded):
- if 'unread' in msg.get_tags():
- msg.remove_tags(['unread'])
- ui.apply_command(FlushCommand())
- widget.rebuild()
- widget.fold(visible=True)
- else:
- widget.fold(visible=False)
-
-
-class ToggleHeaderCommand(Command):
- def apply(self, ui):
- msgw = ui.current_buffer.get_selection()
- msgw.toggle_full_header()
-
-
-class PipeCommand(Command):
- def __init__(self, command, whole_thread=False, separately=False,
- noop_msg='no command specified', confirm_msg='',
- done_msg='done', **kwargs):
- Command.__init__(self, **kwargs)
- self.cmd = command
- self.whole_thread = whole_thread
- self.separately = separately
- self.noop_msg = noop_msg
- self.confirm_msg = confirm_msg
- self.done_msg = done_msg
-
- @defer.inlineCallbacks
- def apply(self, ui):
- # abort if command unset
- if not self.cmd:
- ui.notify(self.noop_msg, priority='error')
- return
-
- # get messages to pipe
- if self.whole_thread:
- thread = ui.current_buffer.get_selected_thread()
- if not thread:
- return
- to_print = thread.get_messages().keys()
- else:
- to_print = [ui.current_buffer.get_selected_message()]
-
- # ask for confirmation if needed
- if self.confirm_msg:
- if (yield ui.choice(self.confirm_msg, select='yes',
- cancel='no')) == 'no':
- return
-
- # prepare message sources
- mailstrings = [m.get_email().as_string() for m in to_print]
- if not self.separately:
- mailstrings = ['\n\n'.join(mailstrings)]
-
- # do teh monkey
- for mail in mailstrings:
- out, err = helper.pipe_to_command(self.cmd, mail)
- if err:
- ui.notify(err, priority='error')
- return
-
- # display 'done' message
- if self.done_msg:
- ui.notify(self.done_msg)
-
-
-class PrintCommand(PipeCommand):
- def __init__(self, whole_thread=False, separately=False, **kwargs):
- # get print command
- cmd = settings.config.get('general', 'print_cmd', fallback='')
-
- # set up notification strings
- if whole_thread:
- confirm_msg = 'print all messages in thread?'
- ok_msg = 'printed thread using %s' % cmd
- else:
- confirm_msg = 'print selected message?'
- ok_msg = 'printed message using %s' % cmd
-
- # no print cmd set
- noop_msg = 'no print command specified. Set "print_cmd" in the '\
- 'global section.'
- PipeCommand.__init__(self, cmd, whole_thread=whole_thread,
- separately=separately,
- noop_msg=noop_msg, confirm_msg=confirm_msg,
- done_msg=ok_msg, **kwargs)
-
-
-class SaveAttachmentCommand(Command):
- def __init__(self, all=False, path=None, **kwargs):
- Command.__init__(self, **kwargs)
- self.all = all
- self.path = path
-
- @defer.inlineCallbacks
- def apply(self, ui):
- pcomplete = completion.PathCompleter()
- if self.all:
- msg = ui.current_buffer.get_selected_message()
- if not self.path:
- self.path = yield ui.prompt(prefix='save attachments to:',
- text=os.path.join('~', ''),
- completer=pcomplete)
- if self.path:
- if os.path.isdir(os.path.expanduser(self.path)):
- for a in msg.get_attachments():
- dest = a.save(self.path)
- name = a.get_filename()
- if name:
- ui.notify('saved %s as: %s' % (name, dest))
- else:
- ui.notify('saved attachment as: %s' % dest)
- else:
- ui.notify('not a directory: %s' % self.path,
- priority='error')
- else:
- ui.notify('canceled')
- else: # save focussed attachment
- focus = ui.get_deep_focus()
- if isinstance(focus, widgets.AttachmentWidget):
- attachment = focus.get_attachment()
- filename = attachment.get_filename()
- if not self.path:
- msg = 'save attachment (%s) to:' % filename
- initialtext = os.path.join('~', filename)
- self.path = yield ui.prompt(prefix=msg,
- completer=pcomplete,
- text=initialtext)
- if self.path:
- try:
- dest = attachment.save(self.path)
- ui.notify('saved attachment as: %s' % dest)
- except (IOError, OSError), e:
- ui.notify(str(e), priority='error')
- else:
- ui.notify('canceled')
-
-
-class OpenAttachmentCommand(Command):
- """displays an attachment according to mailcap"""
- def __init__(self, attachment, **kwargs):
- Command.__init__(self, **kwargs)
- self.attachment = attachment
-
- def apply(self, ui):
- logging.info('open attachment')
- mimetype = self.attachment.get_content_type()
- handler = settings.get_mime_handler(mimetype)
- if handler:
- path = self.attachment.save(tempfile.gettempdir())
- handler = handler.replace('%s', '{}')
-
- def afterwards():
- os.remove(path)
- ui.apply_command(ExternalCommand(handler, path=path,
- on_success=afterwards,
- in_thread=True))
- else:
- ui.notify('unknown mime type')
-
-
-class ThreadSelectCommand(Command):
- def apply(self, ui):
- focus = ui.get_deep_focus()
- if isinstance(focus, widgets.MessageSummaryWidget):
- ui.apply_command(FoldMessagesCommand())
- elif isinstance(focus, widgets.AttachmentWidget):
- logging.info('open attachment')
- ui.apply_command(OpenAttachmentCommand(focus.get_attachment()))
- else:
- logging.info('unknown widget %s' % focus)
-
-
-### ENVELOPE
-class EnvelopeOpenCommand(Command):
- """open a new envelope buffer"""
- def __init__(self, mail=None, **kwargs):
- self.mail = mail
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- ui.buffer_open(buffer.EnvelopeBuffer(ui, mail=self.mail))
-
-
-class EnvelopeEditCommand(Command):
- """re-edits mail in from envelope buffer"""
- def __init__(self, mail=None, **kwargs):
- self.mail = mail
- self.openNew = (mail != None)
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
- if not self.mail:
- self.mail = ui.current_buffer.get_email()
-
- def openEnvelopeFromTmpfile():
- # This parses the input from the tempfile.
- # we do this ourselves here because we want to be able to
- # just type utf-8 encoded stuff into the tempfile and let alot
- # worry about encodings.
-
- # get input
- f = open(tf.name)
- enc = settings.config.get('general', 'editor_writes_encoding')
- editor_input = f.read().decode(enc)
- headertext, bodytext = editor_input.split('\n\n', 1)
-
- # call post-edit translate hook
- translate = settings.hooks.get('post_edit_translate')
- if translate:
- bodytext = translate(bodytext, ui=ui, dbm=ui.dbman,
- aman=ui.accountman, log=ui.logger,
- config=settings.config)
-
- # go through multiline, utf-8 encoded headers
- key = value = None
- for line in headertext.splitlines():
- if re.match('\w+:', line): # new k/v pair
- if key and value: # save old one from stack
- del self.mail[key] # ensure unique values in mails
- self.mail[key] = encode_header(key, value) # save
- key, value = line.strip().split(':', 1) # parse new pair
- elif key and value: # append new line without key prefix
- value += line
- if key and value: # save last one if present
- del self.mail[key]
- self.mail[key] = encode_header(key, value)
-
- if self.mail.is_multipart():
- for part in self.mail.walk():
- if part.get_content_maintype() == 'text':
- if 'Content-Transfer-Encoding' in part:
- del(part['Content-Transfer-Encoding'])
- part.set_payload(bodytext, 'utf-8')
- break
-
- f.close()
- os.unlink(tf.name)
- if self.openNew:
- ui.apply_command(EnvelopeOpenCommand(mail=self.mail))
- else:
- ui.current_buffer.set_email(self.mail)
-
- # decode header
- edit_headers = ['Subject', 'To', 'From']
- headertext = u''
- for key in edit_headers:
- value = u''
- if key in self.mail:
- value = decode_header(self.mail.get(key, ''))
- headertext += '%s: %s\n' % (key, value)
-
- if self.mail.is_multipart():
- for part in self.mail.walk():
- if part.get_content_maintype() == 'text':
- bodytext = decode_to_unicode(part)
- break
- else:
- bodytext = decode_to_unicode(self.mail)
-
- # call pre-edit translate hook
- translate = settings.hooks.get('pre_edit_translate')
- if translate:
- bodytext = translate(bodytext, ui=ui, dbm=ui.dbman,
- aman=ui.accountman, log=ui.logger,
- config=settings.config)
-
- #write stuff to tempfile
- tf = tempfile.NamedTemporaryFile(delete=False)
- content = '%s\n\n%s' % (headertext,
- bodytext)
- tf.write(content.encode('utf-8'))
- tf.flush()
- tf.close()
- cmd = EditCommand(tf.name, on_success=openEnvelopeFromTmpfile,
- refocus=False)
- ui.apply_command(cmd)
-
-
-class EnvelopeSetCommand(Command):
- """sets header fields of mail open in envelope buffer"""
-
- def __init__(self, key='', value=u'', replace=True, **kwargs):
- self.key = key
- self.value = encode_header(key, value)
- self.replace = replace
- Command.__init__(self, **kwargs)
-
- def apply(self, ui):
- envelope = ui.current_buffer
- mail = envelope.get_email()
- if self.replace:
- del(mail[self.key])
- mail[self.key] = self.value
- envelope.rebuild()
-
-
-class EnvelopeRefineCommand(Command):
- """prompt to change current value of header field"""
-
- def __init__(self, key='', **kwargs):
- Command.__init__(self, **kwargs)
- self.key = key
-
- def apply(self, ui):
- mail = ui.current_buffer.get_email()
- value = decode_header(mail.get(self.key, ''))
- ui.commandprompt('set %s %s' % (self.key, value))
-
-
-class EnvelopeSendCommand(Command):
- @defer.inlineCallbacks
- def apply(self, ui):
- envelope = ui.current_buffer
- mail = envelope.get_email()
- frm = decode_header(mail.get('From'))
- sname, saddr = email.Utils.parseaddr(frm)
- account = ui.accountman.get_account_by_address(saddr)
- if account:
- # attach signature file if present
- if account.signature:
- sig = os.path.expanduser(account.signature)
- if os.path.isfile(sig):
- if account.signature_filename:
- name = account.signature_filename
- else:
- name = None
- helper.attach(sig, mail, filename=name)
- else:
- ui.notify('could not locate signature: %s' % sig,
- priority='error')
- if (yield ui.choice('send without signature',
- select='yes', cancel='no')) == 'no':
- return
-
- clearme = ui.notify('sending..', timeout=-1, block=False)
- reason = account.send_mail(mail)
- ui.clear_notify([clearme])
- if not reason: # sucessfully send mail
- cmd = BufferCloseCommand(buffer=envelope)
- ui.apply_command(cmd)
- ui.notify('mail send successful')
- else:
- ui.notify('failed to send: %s' % reason, priority='error')
- else:
- ui.notify('failed to send: no account set up for %s' % saddr,
- priority='error')
-
-
-class EnvelopeAttachCommand(Command):
- def __init__(self, path=None, mail=None, **kwargs):
- Command.__init__(self, **kwargs)
- self.mail = mail
- self.path = path
-
- def apply(self, ui):
- msg = self.mail
- if not msg:
- msg = ui.current_buffer.get_email()
-
- if self.path:
- files = filter(os.path.isfile,
- glob.glob(os.path.expanduser(self.path)))
- if not files:
- ui.notify('no matches, abort')
- return
- else:
- ui.notify('no files specified, abort')
-
- logging.info("attaching: %s" % files)
- for path in files:
- helper.attach(path, msg)
-
- if not self.mail: # set the envelope msg iff we got it from there
- ui.current_buffer.set_email(msg)
-
-
-# TAGLIST
-class TaglistSelectCommand(Command):
- def apply(self, ui):
- tagstring = ui.current_buffer.get_selected_tag()
- cmd = SearchCommand(query='tag:%s' % tagstring)
- ui.apply_command(cmd)
-
-
-class SendKeypressCommand(Command):
- def __init__(self, key, **kwargs):
- Command.__init__(self, **kwargs)
- self.key = key
-
- def apply(self, ui):
- ui.keypress(self.key)
-
-
-class HelpCommand(Command):
- def __init__(self, commandline='', **kwargs):
- Command.__init__(self, **kwargs)
- self.commandline = commandline.strip()
-
- def apply(self, ui):
- if self.commandline:
- cmd = self.commandline.split(' ', 1)[0]
- # TODO: how to I access COMMANDS from below?
- ui.notify('no help for \'%s\'' % cmd, priority='error')
- titletext = 'help for %s' % cmd
- body = urwid.Text('helpstring')
- return
- else:
- # get mappings
- modemaps = dict(settings.config.items('%s-maps' % ui.mode))
- globalmaps = dict(settings.config.items('global-maps'))
-
- # build table
- maxkeylength = len(max((modemaps).keys() + globalmaps.keys(),
- key=len))
- keycolumnwidth = maxkeylength + 2
-
- linewidgets = []
- # mode specific maps
- linewidgets.append(urwid.Text(('helptexth1',
- '\n%s-mode specific maps' % ui.mode)))
- for (k, v) in modemaps.items():
- line = urwid.Columns([('fixed', keycolumnwidth, urwid.Text(k)),
- urwid.Text(v)])
- linewidgets.append(line)
-
- # global maps
- linewidgets.append(urwid.Text(('helptexth1',
- '\nglobal maps')))
- for (k, v) in globalmaps.items():
- if k not in modemaps:
- line = urwid.Columns(
- [('fixed', keycolumnwidth, urwid.Text(k)),
- urwid.Text(v)])
- linewidgets.append(line)
-
- body = urwid.ListBox(linewidgets)
- ckey = 'cancel'
- titletext = 'Bindings Help (%s cancels)' % ckey
-
- box = widgets.DialogBox(body, titletext,
- bodyattr='helptext',
- titleattr='helptitle')
-
- # put promptwidget as overlay on main widget
- overlay = urwid.Overlay(box, ui.mainframe, 'center',
- ('relative', 70), 'middle',
- ('relative', 70))
- ui.show_as_root_until_keypress(overlay, 'cancel')
-
-
-COMMANDS = {
- 'search': {
- 'refine': (RefineCommand, {}),
- 'refineprompt': (RefinePromptCommand, {}),
- 'openthread': (OpenThreadCommand, {}),
- 'toggletag': (ToggleThreadTagCommand, {'tags': ['inbox']}),
- 'retag': (RetagCommand, {}),
- 'retagprompt': (RetagPromptCommand, {}),
- },
- 'envelope': {
- 'attach': (EnvelopeAttachCommand, {}),
- 'send': (EnvelopeSendCommand, {}),
- 'reedit': (EnvelopeEditCommand, {}),
- 'refine': (EnvelopeRefineCommand, {}),
- 'set': (EnvelopeSetCommand, {}),
- },
- 'bufferlist': {
- 'closefocussed': (BufferCloseCommand, {'focussed': True}),
- 'openfocussed': (BufferFocusCommand, {}),
- },
- 'taglist': {
- 'select': (TaglistSelectCommand, {}),
- },
- 'thread': {
- 'reply': (ReplyCommand, {}),
- 'groupreply': (ReplyCommand, {'groupreply': True}),
- 'forward': (ForwardCommand, {}),
- 'fold': (FoldMessagesCommand, {'visible': False}),
- 'pipeto': (PipeCommand, {}),
- 'print': (PrintCommand, {}),
- 'unfold': (FoldMessagesCommand, {'visible': True}),
- 'select': (ThreadSelectCommand, {}),
- 'save': (SaveAttachmentCommand, {}),
- 'toggleheaders': (ToggleHeaderCommand, {}),
- },
- 'global': {
- 'move': (SendKeypressCommand, {}),
- 'cancel': (SendKeypressCommand, {'key': 'cancel'}),
- 'select': (SendKeypressCommand, {'key': 'select'}),
- 'sendkey': (SendKeypressCommand, {}),
- 'bnext': (BufferFocusCommand, {'offset': 1}),
- 'bprevious': (BufferFocusCommand, {'offset': -1}),
- 'bufferlist': (OpenBufferlistCommand, {}),
- 'bclose': (BufferCloseCommand, {}),
- 'compose': (ComposeCommand, {}),
- 'edit': (EditCommand, {}),
- 'exit': (ExitCommand, {}),
- 'flush': (FlushCommand, {}),
- 'prompt': (PromptCommand, {}),
- 'pyshell': (PythonShellCommand, {}),
- 'refresh': (RefreshCommand, {}),
- 'search': (SearchCommand, {}),
- 'shellescape': (ExternalCommand, {}),
- 'taglist': (TagListCommand, {}),
- 'help': (HelpCommand, {}),
- }
-}
-
-
-def commandfactory(cmdname, mode='global', **kwargs):
- if cmdname in COMMANDS[mode]:
- (cmdclass, parms) = COMMANDS[mode][cmdname]
- elif cmdname in COMMANDS['global']:
- (cmdclass, parms) = COMMANDS['global'][cmdname]
- else:
- logging.error('there is no command %s' % cmdname)
- parms = parms.copy()
- parms.update(kwargs)
- for (key, value) in kwargs.items():
- if callable(value):
- parms[key] = value()
- else:
- parms[key] = value
-
- parms['prehook'] = settings.hooks.get('pre_' + cmdname)
- parms['posthook'] = settings.hooks.get('post_' + cmdname)
-
- logging.debug('cmd parms %s' % parms)
- return cmdclass(**parms)
-
-
-def interpret_commandline(cmdline, mode):
- # TODO: use argparser here!
- if not cmdline:
- return None
- logging.debug('mode:%s got commandline "%s"' % (mode, cmdline))
- args = cmdline.split(' ', 1)
- cmd = args[0]
- if args[1:]:
- params = args[1]
- else:
- params = ''
-
- # unfold aliases
- if settings.config.has_option('command-aliases', cmd):
- cmd = settings.config.get('command-aliases', cmd)
-
- # allow to shellescape without a space after '!'
- if cmd.startswith('!'):
- params = cmd[1:] + ' ' + params
- cmd = 'shellescape'
-
- # check if this command makes sense in current mode
- if cmd not in COMMANDS[mode] and cmd not in COMMANDS['global']:
- logging.debug('unknown command: %s' % (cmd))
- return None
-
- if cmd == 'search':
- return commandfactory(cmd, mode=mode, query=params)
- if cmd in ['move', 'sendkey']:
- return commandfactory(cmd, mode=mode, key=params)
- elif cmd == 'compose':
- h = {}
- if params:
- h = {'To': params}
- return commandfactory(cmd, mode=mode, headers=h)
- elif cmd == 'attach':
- return commandfactory(cmd, mode=mode, path=params)
- elif cmd == 'help':
- return commandfactory(cmd, mode=mode, commandline=params)
- elif cmd == 'forward':
- return commandfactory(cmd, mode=mode, inline=(params == '--inline'))
- elif cmd == 'prompt':
- return commandfactory(cmd, mode=mode, startstring=params)
- elif cmd == 'refine':
- if mode == 'search':
- return commandfactory(cmd, mode=mode, query=params)
- elif mode == 'envelope':
- return commandfactory(cmd, mode=mode, key=params)
-
- elif cmd == 'retag':
- return commandfactory(cmd, mode=mode, tagsstring=params)
- elif cmd == 'shellescape':
- return commandfactory(cmd, mode=mode, commandstring=params)
- elif cmd == 'set':
- key, value = params.split(' ', 1)
- return commandfactory(cmd, mode=mode, key=key, value=value)
- elif cmd == 'toggletag':
- return commandfactory(cmd, mode=mode, tags=params.split())
- elif cmd == 'fold':
- return commandfactory(cmd, mode=mode, all=(params == '--all'))
- elif cmd == 'unfold':
- return commandfactory(cmd, mode=mode, all=(params == '--all'))
- elif cmd == 'save':
- args = params.split(' ')
- allset = False
- pathset = None
- if args:
- if args[0] == '--all':
- allset = True
- pathset = ' '.join(args[1:])
- else:
- pathset = params
- return commandfactory(cmd, mode=mode, all=allset, path=pathset)
- elif cmd == 'edit':
- filepath = os.path.expanduser(params)
- if os.path.isfile(filepath):
- return commandfactory(cmd, mode=mode, path=filepath)
- elif cmd == 'print':
- args = [a.strip() for a in params.split()]
- return commandfactory(cmd, mode=mode,
- whole_thread=('--thread' in args),
- separately=('--separately' in args))
- elif cmd == 'pipeto':
- return commandfactory(cmd, mode=mode, command=params)
-
- elif not params and cmd in ['exit', 'flush', 'pyshell', 'taglist',
- 'bclose', 'compose', 'openfocussed',
- 'closefocussed', 'bnext', 'bprevious', 'retag',
- 'refresh', 'bufferlist', 'refineprompt',
- 'reply', 'open', 'groupreply', 'bounce',
- 'openthread', 'toggleheaders', 'send',
- 'cancel', 'reedit', 'select', 'retagprompt']:
- return commandfactory(cmd, mode=mode)
- else:
- return None
diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py
new file mode 100644
index 00000000..b374d317
--- /dev/null
+++ b/alot/commands/__init__.py
@@ -0,0 +1,142 @@
+import os
+import sys
+import glob
+import shlex
+import logging
+import argparse
+import cStringIO
+
+import alot.settings
+
+
+class Command(object):
+ """base class for commands"""
+ def __init__(self, prehook=None, posthook=None):
+ self.prehook = prehook
+ self.posthook = posthook
+ self.undoable = False
+ self.help = self.__doc__
+
+ def apply(self, caller):
+ pass
+
+ @classmethod
+ def get_helpstring(cls):
+ return cls.__doc__
+
+
+COMMANDS = {
+ 'search': {},
+ 'envelope': {},
+ 'bufferlist': {},
+ 'taglist': {},
+ 'thread': {},
+ 'global': {},
+}
+
+
+def lookup_command(cmdname, mode):
+ """returns commandclass, argparser and forcedparams
+ for `cmdname` in `mode`"""
+ if cmdname in COMMANDS[mode]:
+ return COMMANDS[mode][cmdname]
+ elif cmdname in COMMANDS['global']:
+ return COMMANDS['global'][cmdname]
+ else:
+ return None, None, None
+
+
+def lookup_parser(cmdname, mode):
+ return lookup_command(cmdname, mode)[1]
+
+
+class CommandParseError(Exception):
+ pass
+
+
+class CommandArgumentParser(argparse.ArgumentParser):
+ """ArgumentParser that raises `CommandParseError`
+ instead of printing to sys.stderr"""
+ def exit(self, message):
+ raise CommandParseError(message)
+
+ def error(self, message):
+ raise CommandParseError(message)
+
+
+class registerCommand(object):
+ def __init__(self, mode, name, help=None, usage=None,
+ forced={}, arguments=[]):
+ self.mode = mode
+ self.name = name
+ self.help = help
+ self.usage = usage
+ self.forced = forced
+ self.arguments = arguments
+
+ def __call__(self, klass):
+ argparser = CommandArgumentParser(description=self.help,
+ usage=self.usage,
+ prog=self.name, add_help=False)
+ for args, kwargs in self.arguments:
+ argparser.add_argument(*args, **kwargs)
+ COMMANDS[self.mode][self.name] = (klass, argparser, self.forced)
+ return klass
+
+
+def commandfactory(cmdline, mode='global'):
+ # split commandname and parameters
+ if not cmdline:
+ return None
+ logging.debug('mode:%s got commandline "%s"' % (mode, cmdline))
+ # allow to shellescape without a space after '!'
+ if cmdline.startswith('!'):
+ cmdline = 'shellescape \'%s\'' % cmdline[1:]
+ args = shlex.split(cmdline.encode('utf-8'))
+ args = map(lambda x: x.decode('utf-8'), args) # get unicode strings
+ logging.debug('ARGS: %s' % args)
+ cmdname = args[0]
+ args = args[1:]
+
+ # unfold aliases
+ if alot.settings.config.has_option('command-aliases', cmdname):
+ cmdname = alot.settings.config.get('command-aliases', cmdname)
+
+
+ # get class, argparser and forced parameter
+ (cmdclass, parser, forcedparms) = lookup_command(cmdname, mode)
+ if cmdclass is None:
+ msg = 'unknown command: %s' % cmdname
+ logging.debug(msg)
+ raise CommandParseError(msg)
+
+ #logging.debug(parser)
+ parms = vars(parser.parse_args(args))
+ logging.debug('PARMS: %s' % parms)
+ logging.debug(parms)
+
+ parms.update(forcedparms)
+ # still needed?
+ #for (key, value) in kwargs.items():
+ # if callable(value):
+ # parms[key] = value()
+ # else:
+ # parms[key] = value
+
+ parms['prehook'] = alot.settings.hooks.get('pre_' + cmdname)
+ parms['posthook'] = alot.settings.hooks.get('post_' + cmdname)
+
+ logging.debug('cmd parms %s' % parms)
+ return cmdclass(**parms)
+
+
+#def interpret_commandline(cmdline, mode):
+#
+# elif cmd == 'shellescape':
+# return commandfactory(cmd, mode=mode, commandstring=params)
+# elif cmd == 'edit':
+# filepath = os.path.expanduser(params)
+# if os.path.isfile(filepath):
+# return commandfactory(cmd, mode=mode, path=filepath)
+
+__all__ = list(filename[:-3] for filename in glob.glob1(os.path.dirname(__file__), '*.py'))
diff --git a/alot/commands/bufferlist.py b/alot/commands/bufferlist.py
new file mode 100644
index 00000000..7fa2607f
--- /dev/null
+++ b/alot/commands/bufferlist.py
@@ -0,0 +1 @@
+MODE = 'bufferlist'
diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py
new file mode 100644
index 00000000..c5f5c96a
--- /dev/null
+++ b/alot/commands/envelope.py
@@ -0,0 +1,225 @@
+import os
+import re
+import glob
+import logging
+import email
+import tempfile
+from email import Charset
+from twisted.internet import defer
+
+from alot.commands import Command, registerCommand
+from alot import settings
+from alot import helper
+from alot.message import decode_to_unicode
+from alot.message import decode_header
+from alot.message import encode_header
+from alot.commands.globals import EditCommand
+from alot.commands.globals import BufferCloseCommand
+from alot.commands.globals import EnvelopeOpenCommand
+
+
+MODE = 'envelope'
+
+
+@registerCommand(MODE, 'attach', help='attach files to the mail', arguments=[
+ (['path'], {'help':'file(s) to attach (accepts wildcads)'})])
+class EnvelopeAttachCommand(Command):
+ def __init__(self, path=None, mail=None, **kwargs):
+ Command.__init__(self, **kwargs)
+ self.mail = mail
+ self.path = path
+
+ def apply(self, ui):
+ msg = self.mail
+ if not msg:
+ msg = ui.current_buffer.get_email()
+
+ if self.path:
+ files = filter(os.path.isfile,
+ glob.glob(os.path.expanduser(self.path)))
+ if not files:
+ ui.notify('no matches, abort')
+ return
+ else:
+ ui.notify('no files specified, abort')
+
+ logging.info("attaching: %s" % files)
+ for path in files:
+ helper.attach(path, msg)
+
+ if not self.mail: # set the envelope msg iff we got it from there
+ ui.current_buffer.set_email(msg)
+
+
+@registerCommand(MODE, 'refine', help='prompt to change the value of a header',
+ arguments=[
+ (['key'], {'help':'header to refine'})])
+class EnvelopeRefineCommand(Command):
+
+ def __init__(self, key='', **kwargs):
+ Command.__init__(self, **kwargs)
+ self.key = key
+
+ def apply(self, ui):
+ mail = ui.current_buffer.get_email()
+ value = decode_header(mail.get(self.key, ''))
+ ui.commandprompt('set %s %s' % (self.key, value))
+
+
+@registerCommand(MODE, 'send', help='sends mail')
+class EnvelopeSendCommand(Command):
+ @defer.inlineCallbacks
+ def apply(self, ui):
+ envelope = ui.current_buffer
+ mail = envelope.get_email()
+ frm = decode_header(mail.get('From'))
+ sname, saddr = email.Utils.parseaddr(frm)
+ account = ui.accountman.get_account_by_address(saddr)
+ if account:
+ # attach signature file if present
+ if account.signature:
+ sig = os.path.expanduser(account.signature)
+ if os.path.isfile(sig):
+ if account.signature_filename:
+ name = account.signature_filename
+ else:
+ name = None
+ helper.attach(sig, mail, filename=name)
+ else:
+ ui.notify('could not locate signature: %s' % sig,
+ priority='error')
+ if (yield ui.choice('send without signature',
+ select='yes', cancel='no')) == 'no':
+ return
+
+ clearme = ui.notify('sending..', timeout=-1, block=False)
+ reason = account.send_mail(mail)
+ ui.clear_notify([clearme])
+ if not reason: # sucessfully send mail
+ cmd = BufferCloseCommand(buffer=envelope)
+ ui.apply_command(cmd)
+ ui.notify('mail send successful')
+ else:
+ ui.notify('failed to send: %s' % reason, priority='error')
+ else:
+ ui.notify('failed to send: no account set up for %s' % saddr,
+ priority='error')
+
+
+@registerCommand(MODE, 'reedit', help='edit currently open mail')
+class EnvelopeEditCommand(Command):
+ def __init__(self, mail=None, **kwargs):
+ self.mail = mail
+ self.openNew = (mail != None)
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
+ if not self.mail:
+ self.mail = ui.current_buffer.get_email()
+
+ def openEnvelopeFromTmpfile():
+ # This parses the input from the tempfile.
+ # we do this ourselves here because we want to be able to
+ # just type utf-8 encoded stuff into the tempfile and let alot
+ # worry about encodings.
+
+ # get input
+ f = open(tf.name)
+ enc = settings.config.get('general', 'editor_writes_encoding')
+ editor_input = f.read().decode(enc)
+ headertext, bodytext = editor_input.split('\n\n', 1)
+
+ # call post-edit translate hook
+ translate = settings.hooks.get('post_edit_translate')
+ if translate:
+ bodytext = translate(bodytext, ui=ui, dbm=ui.dbman,
+ aman=ui.accountman, log=ui.logger,
+ config=settings.config)
+
+ # go through multiline, utf-8 encoded headers
+ key = value = None
+ for line in headertext.splitlines():
+ if re.match('\w+:', line): # new k/v pair
+ if key and value: # save old one from stack
+ del self.mail[key] # ensure unique values in mails
+ self.mail[key] = encode_header(key, value) # save
+ key, value = line.strip().split(':', 1) # parse new pair
+ elif key and value: # append new line without key prefix
+ value += line
+ if key and value: # save last one if present
+ del self.mail[key]
+ self.mail[key] = encode_header(key, value)
+
+ if self.mail.is_multipart():
+ for part in self.mail.walk():
+ if part.get_content_maintype() == 'text':
+ if 'Content-Transfer-Encoding' in part:
+ del(part['Content-Transfer-Encoding'])
+ part.set_payload(bodytext, 'utf-8')
+ break
+
+ f.close()
+ os.unlink(tf.name)
+ if self.openNew:
+ ui.apply_command(EnvelopeOpenCommand(mail=self.mail))
+ else:
+ ui.current_buffer.set_email(self.mail)
+
+ # decode header
+ edit_headers = ['Subject', 'To', 'From']
+ headertext = u''
+ for key in edit_headers:
+ value = u''
+ if key in self.mail:
+ value = decode_header(self.mail.get(key, ''))
+ headertext += '%s: %s\n' % (key, value)
+
+ if self.mail.is_multipart():
+ for part in self.mail.walk():
+ if part.get_content_maintype() == 'text':
+ bodytext = decode_to_unicode(part)
+ break
+ else:
+ bodytext = decode_to_unicode(self.mail)
+
+ # call pre-edit translate hook
+ translate = settings.hooks.get('pre_edit_translate')
+ if translate:
+ bodytext = translate(bodytext, ui=ui, dbm=ui.dbman,
+ aman=ui.accountman, log=ui.logger,
+ config=settings.config)
+
+ #write stuff to tempfile
+ tf = tempfile.NamedTemporaryFile(delete=False)
+ content = '%s\n\n%s' % (headertext,
+ bodytext)
+ tf.write(content.encode('utf-8'))
+ tf.flush()
+ tf.close()
+ cmd = EditCommand(tf.name, on_success=openEnvelopeFromTmpfile,
+ refocus=False)
+ ui.apply_command(cmd)
+
+
+@registerCommand(MODE, 'set', help='set header value', arguments=[
+ (['--replace'], {'action': 'store_true', 'help':'remove old value'}),
+ (['key'], {'help':'header to refine'}),
+ (['value'], {'nargs':'+', 'help':'value'})])
+class EnvelopeSetCommand(Command):
+ """sets header fields of mail open in envelope buffer"""
+
+ def __init__(self, key, value, replace=True, **kwargs):
+ self.key = key
+ value = ' '.join(value)
+ self.value = encode_header(key, value)
+ self.replace = replace
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ envelope = ui.current_buffer
+ mail = envelope.get_email()
+ if self.replace:
+ del(mail[self.key])
+ mail[self.key] = self.value
+ envelope.rebuild()
diff --git a/alot/commands/globals.py b/alot/commands/globals.py
new file mode 100644
index 00000000..0da6ba18
--- /dev/null
+++ b/alot/commands/globals.py
@@ -0,0 +1,462 @@
+import os
+import code
+import threading
+import subprocess
+import shlex
+import email
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+import urwid
+from twisted.internet import defer
+
+from alot.commands import Command, registerCommand
+from alot import buffers
+from alot import settings
+from alot import widgets
+from alot import helper
+from alot.db import DatabaseLockedError
+from alot.completion import ContactsCompleter
+from alot.completion import AccountCompleter
+from alot.message import encode_header
+from alot import commands
+import argparse
+
+MODE = 'global'
+
+
+@registerCommand(MODE, 'exit', help='shut alot down cleanly')
+class ExitCommand(Command):
+ @defer.inlineCallbacks
+ def apply(self, ui):
+ if settings.config.getboolean('general', 'bug_on_exit'):
+ if (yield ui.choice('realy quit?', select='yes', cancel='no',
+ msg_position='left')) == 'no':
+ return
+ ui.exit()
+
+
+@registerCommand(MODE, 'search', usage='search query', arguments=[
+ (['query'], {'nargs':argparse.REMAINDER, 'help':'search string'})],
+ help='open a new search buffer')
+class SearchCommand(Command):
+ def __init__(self, query, **kwargs):
+ self.query = ' '.join(query)
+ Command.__init__(self, **kwargs)
+
+ @defer.inlineCallbacks
+ def apply(self, ui):
+ if self.query:
+ if self.query == '*' and ui.current_buffer:
+ s = 'really search for all threads? This takes a while..'
+ if (yield ui.choice(s, select='yes', cancel='no')) == 'no':
+ return
+ open_searches = ui.get_buffers_of_type(buffers.SearchBuffer)
+ to_be_focused = None
+ for sb in open_searches:
+ if sb.querystring == self.query:
+ to_be_focused = sb
+ if to_be_focused:
+ ui.buffer_focus(to_be_focused)
+ else:
+ ui.buffer_open(buffers.SearchBuffer(ui, self.query))
+ else:
+ ui.notify('empty query string')
+
+
+@registerCommand(MODE, 'prompt', help='starts commandprompt', arguments=[
+ (['startwith'], {'nargs':'?', 'default':'', 'help':'initial content'})])
+class PromptCommand(Command):
+ def __init__(self, startwith='', **kwargs):
+ self.startwith = startwith
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ ui.commandprompt(self.startwith)
+
+
+@registerCommand(MODE, 'refresh', help='refreshes the current buffer')
+class RefreshCommand(Command):
+ def apply(self, ui):
+ ui.current_buffer.rebuild()
+ ui.update()
+
+
+@registerCommand(MODE, 'shellescape', arguments=[
+ (['--spawn'], {'action': 'store_true', 'help':'run in terminal window'}),
+ (['--thread'], {'action': 'store_true', 'help':'run in separate thread'}),
+ (['--refocus'], {'action': 'store_true', 'help':'refocus current buffer \
+ after command has finished'}),
+ (['cmd'], {'help':'command line to execute'})],
+ help='calls external command')
+class ExternalCommand(Command):
+ def __init__(self, cmd, path=None, spawn=False, refocus=True,
+ thread=False, on_success=None, **kwargs):
+ """
+ :param cmd: the command to call
+ :type cmd: str
+ :param path: a path to a file (or None)
+ :type path: str
+ :param spawn: run command in a new terminal
+ :type spawn: boolean
+ :param thread: run asynchronously, don't block alot
+ :type thread: boolean
+ :param refocus: refocus calling buffer after cmd termination
+ :type refocus: boolean
+ :param on_success: code to execute after command successfully exited
+ :type on_success: callable
+ """
+ self.commandstring = cmd
+ self.path = path
+ self.spawn = spawn
+ self.refocus = refocus
+ self.in_thread = thread
+ self.on_success = on_success
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ callerbuffer = ui.current_buffer
+
+ def afterwards(data):
+ if data == 'success':
+ if callable(self.on_success):
+ self.on_success()
+ else:
+ ui.notify(data, priority='error')
+ if self.refocus and callerbuffer in ui.buffers:
+ ui.logger.info('refocussing')
+ ui.buffer_focus(callerbuffer)
+
+ write_fd = ui.mainloop.watch_pipe(afterwards)
+
+ def thread_code(*args):
+ if self.path:
+ if '{}' in self.commandstring:
+ cmd = self.commandstring.replace('{}',
+ helper.shell_quote(self.path))
+ else:
+ cmd = '%s %s' % (self.commandstring,
+ helper.shell_quote(self.path))
+ else:
+ cmd = self.commandstring
+
+ if self.spawn:
+ cmd = '%s %s' % (settings.config.get('general',
+ 'terminal_cmd'),
+ cmd)
+ cmd = cmd.encode('utf-8', errors='ignore')
+ ui.logger.info('calling external command: %s' % cmd)
+ try:
+ if 0 == subprocess.call(shlex.split(cmd)):
+ os.write(write_fd, 'success')
+ except OSError, e:
+ os.write(write_fd, str(e))
+
+ if self.in_thread:
+ thread = threading.Thread(target=thread_code)
+ thread.start()
+ else:
+ ui.mainloop.screen.stop()
+ thread_code()
+ ui.mainloop.screen.start()
+
+
+#@registerCommand(MODE, 'edit', arguments=[
+# (['--nospawn'], {'action': 'store_true', 'help':'spawn '}), #todo
+# (['path'], {'help':'file to edit'})]
+#]
+#)
+class EditCommand(ExternalCommand):
+ """opens editor"""
+ def __init__(self, path, spawn=None, **kwargs):
+ self.path = path
+ if spawn != None:
+ self.spawn = spawn
+ else:
+ self.spawn = settings.config.getboolean('general', 'spawn_editor')
+ editor_cmd = settings.config.get('general', 'editor_cmd')
+
+ ExternalCommand.__init__(self, editor_cmd, path=self.path,
+ spawn=self.spawn, thread=self.spawn,
+ **kwargs)
+
+
+@registerCommand(MODE, 'pyshell',
+ help="opens an interactive python shell for introspection")
+class PythonShellCommand(Command):
+ def apply(self, ui):
+ ui.mainloop.screen.stop()
+ code.interact(local=locals())
+ ui.mainloop.screen.start()
+
+
+@registerCommand(MODE, 'bclose',
+ help="close current buffer or exit if it is the last")
+@registerCommand('bufferlist', 'closefocussed', forced={'focussed': True},
+ help='close focussed buffer')
+class BufferCloseCommand(Command):
+ def __init__(self, buffer=None, focussed=False, **kwargs):
+ """
+ :param buffer: the selected buffer
+ :type buffer: `alot.buffers.Buffer`
+ """
+ self.buffer = buffer
+ self.focussed = focussed
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ if self.focussed:
+ #if in bufferlist, this is ugly.
+ self.buffer = ui.current_buffer.get_selected_buffer()
+ elif not self.buffer:
+ self.buffer = ui.current_buffer
+ ui.buffer_close(self.buffer)
+ ui.buffer_focus(ui.current_buffer)
+
+
+@registerCommand(MODE, 'bprevious', forced={'offset': -1},
+ help='focus previous buffer')
+@registerCommand(MODE, 'bnext', forced={'offset': +1},
+ help='focus next buffer')
+@registerCommand('bufferlist', 'openfocussed', # todo separate
+ help='focus selected buffer')
+class BufferFocusCommand(Command):
+ def __init__(self, buffer=None, offset=0, **kwargs):
+ """
+ :param buffer: the buffer to focus
+ :type buffer: `alot.buffers.Buffer`
+ """
+ self.buffer = buffer
+ self.offset = offset
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ if self.offset:
+ idx = ui.buffers.index(ui.current_buffer)
+ num = len(ui.buffers)
+ self.buffer = ui.buffers[(idx + self.offset) % num]
+ else:
+ if not self.buffer:
+ self.buffer = ui.current_buffer.get_selected_buffer()
+ ui.buffer_focus(self.buffer)
+
+
+@registerCommand(MODE, 'bufferlist', help='opens buffer list')
+class OpenBufferlistCommand(Command):
+ def __init__(self, filtfun=None, **kwargs):
+ self.filtfun = filtfun
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ blists = ui.get_buffers_of_type(buffers.BufferlistBuffer)
+ if blists:
+ ui.buffer_focus(blists[0])
+ else:
+ ui.buffer_open(buffers.BufferlistBuffer(ui, self.filtfun))
+
+
+@registerCommand(MODE, 'taglist', help='opens taglist buffer')
+class TagListCommand(Command):
+ def __init__(self, filtfun=None, **kwargs):
+ self.filtfun = filtfun
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ tags = ui.dbman.get_all_tags()
+ buf = buffers.TagListBuffer(ui, tags, self.filtfun)
+ ui.buffers.append(buf)
+ buf.rebuild()
+ ui.buffer_focus(buf)
+
+
+@registerCommand(MODE, 'flush',
+ help='Flushes write operations or retries until committed')
+class FlushCommand(Command):
+ def apply(self, ui):
+ try:
+ ui.dbman.flush()
+ except DatabaseLockedError:
+ timeout = settings.config.getint('general', 'flush_retry_timeout')
+
+ def f(*args):
+ self.apply(ui)
+ ui.mainloop.set_alarm_in(timeout, f)
+ ui.notify('index locked, will try again in %d secs' % timeout)
+ ui.update()
+ return
+
+
+#todo choices
+@registerCommand(MODE, 'help', arguments=[
+ (['commandname'], {'help':'command or \'bindings\''})],
+ help='display help for a command. Use \'bindings\' to\
+ display all keybings interpreted in current mode.',
+)
+class HelpCommand(Command):
+ def __init__(self, commandname='', **kwargs):
+ Command.__init__(self, **kwargs)
+ self.commandname = commandname
+
+ def apply(self, ui):
+ ui.logger.debug('HELP')
+ if self.commandname == 'bindings':
+ # get mappings
+ modemaps = dict(settings.config.items('%s-maps' % ui.mode))
+ globalmaps = dict(settings.config.items('global-maps'))
+
+ # build table
+ maxkeylength = len(max((modemaps).keys() + globalmaps.keys(),
+ key=len))
+ keycolumnwidth = maxkeylength + 2
+
+ linewidgets = []
+ # mode specific maps
+ linewidgets.append(urwid.Text(('helptexth1',
+ '\n%s-mode specific maps' % ui.mode)))
+ for (k, v) in modemaps.items():
+ line = urwid.Columns([('fixed', keycolumnwidth, urwid.Text(k)),
+ urwid.Text(v)])
+ linewidgets.append(line)
+
+ # global maps
+ linewidgets.append(urwid.Text(('helptexth1',
+ '\nglobal maps')))
+ for (k, v) in globalmaps.items():
+ if k not in modemaps:
+ line = urwid.Columns(
+ [('fixed', keycolumnwidth, urwid.Text(k)),
+ urwid.Text(v)])
+ linewidgets.append(line)
+
+ body = urwid.ListBox(linewidgets)
+ ckey = 'cancel'
+ titletext = 'Bindings Help (%s cancels)' % ckey
+
+ box = widgets.DialogBox(body, titletext,
+ bodyattr='helptext',
+ titleattr='helptitle')
+
+ # put promptwidget as overlay on main widget
+ overlay = urwid.Overlay(box, ui.mainframe, 'center',
+ ('relative', 70), 'middle',
+ ('relative', 70))
+ ui.show_as_root_until_keypress(overlay, 'cancel')
+ else:
+ ui.logger.debug('HELP %s' % self.commandname)
+ parser = commands.lookup_parser(self.commandname, ui.mode)
+ if parser:
+ ui.notify(parser.format_help())
+ else:
+ ui.notify('command not known: %s' % self.commandname)
+
+
+@registerCommand(MODE, 'compose', help='compose a new email',
+ arguments=[
+ (['--sender'], {'nargs': '?', 'help':'sender'}),
+ (['--subject'], {'nargs':'?', 'help':'subject line'}),
+ (['--to'], {'nargs':'+', 'help':'recipient'}),
+ (['--cc'], {'nargs':'+', 'help':'copy to'}),
+ (['--bcc'], {'nargs':'+', 'help':'blind copy to'}),
+])
+class ComposeCommand(Command):
+ def __init__(self, mail=None, headers={},
+ sender=u'', subject=u'', to=[], cc=[], bcc=[],
+ **kwargs):
+ Command.__init__(self, **kwargs)
+ if not mail:
+ self.mail = MIMEMultipart()
+ self.mail.attach(MIMEText('', 'plain', 'UTF-8'))
+ else:
+ self.mail = mail
+ for key, value in headers.items():
+ self.mail[key] = encode_header(key, value)
+
+ if sender:
+ self.mail['From'] = encode_header('From', sender)
+ if subject:
+ self.mail['Subject'] = encode_header('Subject', subject)
+ if to:
+ self.mail['To'] = encode_header('To', ','.join(to))
+ if cc:
+ self.mail['Cc'] = encode_header('Cc', ','.join(cc))
+ if bcc:
+ self.mail['Bcc'] = encode_header('Bcc', ','.join(bcc))
+
+ @defer.inlineCallbacks
+ def apply(self, ui):
+ # TODO: fill with default header (per account)
+ # get From header
+ if not 'From' in self.mail:
+ accounts = ui.accountman.get_accounts()
+ if len(accounts) == 0:
+ ui.notify('no accounts set')
+ return
+ elif len(accounts) == 1:
+ a = accounts[0]
+ else:
+ cmpl = AccountCompleter(ui.accountman)
+ fromaddress = yield ui.prompt(prefix='From>', completer=cmpl,
+ tab=1)
+ validaddresses = [a.address for a in accounts] + [None]
+ while fromaddress not in validaddresses: # TODO: not cool
+ ui.notify('no account for this address. (<esc> cancels)')
+ fromaddress = yield ui.prompt(prefix='From>',
+ completer=cmpl)
+ if not fromaddress:
+ ui.notify('canceled')
+ return
+ a = ui.accountman.get_account_by_address(fromaddress)
+ self.mail['From'] = "%s <%s>" % (a.realname, a.address)
+
+ #get To header
+ if 'To' not in self.mail:
+ name, addr = email.Utils.parseaddr(unicode(self.mail.get('From')))
+ a = ui.accountman.get_account_by_address(addr)
+
+ allbooks = not settings.config.getboolean('general',
+ 'complete_matching_abook_only')
+ ui.logger.debug(allbooks)
+ abooks = ui.accountman.get_addressbooks(order=[a],
+ append_remaining=allbooks)
+ ui.logger.debug(abooks)
+ to = yield ui.prompt(prefix='To>',
+ completer=ContactsCompleter(abooks))
+ if to == None:
+ ui.notify('canceled')
+ return
+ self.mail['To'] = encode_header('to', to)
+ if settings.config.getboolean('general', 'ask_subject') and \
+ not 'Subject' in self.mail:
+ subject = yield ui.prompt(prefix='Subject>')
+ if subject == None:
+ ui.notify('canceled')
+ return
+ self.mail['Subject'] = encode_header('subject', subject)
+
+ ui.apply_command(commands.envelope.EnvelopeEditCommand(mail=self.mail))
+
+
+@registerCommand(MODE, 'move', help='move focus', arguments=[
+ (['key'], {'nargs':'+', 'help':'direction'})])
+@registerCommand(MODE, 'cancel', help='send cancel event',
+ forced={'key': 'cancel'})
+@registerCommand(MODE, 'select', help='send select event',
+ forced={'key': 'select'})
+class SendKeypressCommand(Command):
+ def __init__(self, key, **kwargs):
+ Command.__init__(self, **kwargs)
+ if isinstance(key, list):
+ key = ' '.join(key)
+ self.key = key
+
+ def apply(self, ui):
+ ui.keypress(self.key)
+
+
+class EnvelopeOpenCommand(Command):
+ """open a new envelope buffer"""
+ def __init__(self, mail=None, **kwargs):
+ self.mail = mail
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ ui.buffer_open(buffers.EnvelopeBuffer(ui, mail=self.mail))
diff --git a/alot/commands/search.py b/alot/commands/search.py
new file mode 100644
index 00000000..291e4c03
--- /dev/null
+++ b/alot/commands/search.py
@@ -0,0 +1,150 @@
+from alot.commands import Command, registerCommand
+from twisted.internet import defer
+import argparse
+
+
+from alot.db import DatabaseROError
+from alot import commands
+from alot import buffers
+
+MODE = 'search'
+
+
+@registerCommand(MODE, 'openthread', # todo: make this a select
+ help='open a new thread buffer')
+class OpenThreadCommand(Command):
+ def __init__(self, thread=None, **kwargs):
+ self.thread = thread
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ if not self.thread:
+ self.thread = ui.current_buffer.get_selected_thread()
+ if self.thread:
+ query = ui.current_buffer.querystring
+ ui.logger.info('open thread view for %s' % self.thread)
+
+ sb = buffers.ThreadBuffer(ui, self.thread)
+ ui.buffer_open(sb)
+ sb.unfold_matching(query)
+
+
+@registerCommand(MODE, 'toggletag', arguments=[
+ (['tag'], {'nargs':'+', 'default':'', 'help':'tag to flip'})],
+ help='toggles tags in selected thread')
+class ToggleThreadTagCommand(Command):
+ def __init__(self, tag, thread=None, **kwargs):
+ assert tag
+ self.thread = thread
+ self.tags = set(tag)
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ if not self.thread:
+ self.thread = ui.current_buffer.get_selected_thread()
+ if not self.thread:
+ return
+ try:
+ self.thread.set_tags(set(self.thread.get_tags()) ^ self.tags)
+ except DatabaseROError:
+ ui.notify('index in read-only mode', priority='error')
+ return
+
+ # flush index
+ ui.apply_command(commands.globals.FlushCommand())
+
+ # update current buffer
+ # TODO: what if changes not yet flushed?
+ cb = ui.current_buffer
+ if isinstance(cb, buffers.SearchBuffer):
+ # refresh selected threadline
+ threadwidget = cb.get_selected_threadline()
+ threadwidget.rebuild() # rebuild and redraw the line
+ #remove line from searchlist if thread doesn't match the query
+ qs = "(%s) AND thread:%s" % (cb.querystring,
+ self.thread.get_thread_id())
+ if ui.dbman.count_messages(qs) == 0:
+ ui.logger.debug('remove: %s' % self.thread)
+ cb.threadlist.remove(threadwidget)
+ cb.result_count -= self.thread.get_total_messages()
+ ui.update()
+ elif isinstance(cb, buffers.ThreadBuffer):
+ pass
+
+
+@registerCommand(MODE, 'refine', usage='refine query', arguments=[
+ (['query'], {'nargs':argparse.REMAINDER, 'help':'search string'})],
+ help='refine the query of the currently open searchbuffer')
+class RefineCommand(Command):
+ def __init__(self, query=None, **kwargs):
+ self.querystring = ' '.join(query)
+ Command.__init__(self, **kwargs)
+
+ @defer.inlineCallbacks
+ def apply(self, ui):
+ if self.querystring:
+ if self.querystring == '*':
+ s = 'really search for all threads? This takes a while..'
+ if (yield ui.choice(s, select='yes', cancel='no')) == 'no':
+ return
+ sbuffer = ui.current_buffer
+ oldquery = sbuffer.querystring
+ if self.querystring not in [None, oldquery]:
+ sbuffer.querystring = self.querystring
+ sbuffer = ui.current_buffer
+ sbuffer.rebuild()
+ ui.update()
+ else:
+ ui.notify('empty query string')
+
+
+@registerCommand(MODE, 'refineprompt',
+ help='prompt to change current search buffers query')
+class RefinePromptCommand(Command):
+ def apply(self, ui):
+ sbuffer = ui.current_buffer
+ oldquery = sbuffer.querystring
+ ui.commandprompt('refine ' + oldquery)
+
+
+@registerCommand(MODE, 'retagprompt',
+ help='prompt to retag selected threads\' tags')
+class RetagPromptCommand(Command):
+ def apply(self, ui):
+ thread = ui.current_buffer.get_selected_thread()
+ if not thread:
+ return
+ initial_tagstring = ','.join(thread.get_tags())
+ ui.commandprompt('retag ' + initial_tagstring)
+
+
+@registerCommand(MODE, 'retag', arguments=[
+ (['tags'], {'help':'comma separated list of tags'})],
+ help='overwrite selected thread\'s tags')
+class RetagCommand(Command):
+ def __init__(self, tags=u'', thread=None, **kwargs):
+ self.tagsstring = tags
+ self.thread = thread
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ if not self.thread:
+ self.thread = ui.current_buffer.get_selected_thread()
+ if not self.thread:
+ return
+ tags = filter(lambda x: x, self.tagsstring.split(','))
+ ui.logger.info("got %s:%s" % (self.tagsstring, tags))
+ try:
+ self.thread.set_tags(tags)
+ except DatabaseROError:
+ ui.notify('index in read-only mode', priority='error')
+ return
+
+ # flush index
+ ui.apply_command(commands.globals.FlushCommand())
+
+ # refresh selected threadline
+ sbuffer = ui.current_buffer
+ threadwidget = sbuffer.get_selected_threadline()
+ # rebuild and redraw the line
+ threadwidget.rebuild()
diff --git a/alot/commands/taglist.py b/alot/commands/taglist.py
new file mode 100644
index 00000000..fd8a52d8
--- /dev/null
+++ b/alot/commands/taglist.py
@@ -0,0 +1,12 @@
+from alot.commands import Command, registerCommand
+from alot.commands.globals import SearchCommand
+
+MODE = 'taglist'
+
+
+@registerCommand(MODE, 'select', help='open search for selected tag')
+class TaglistSelectCommand(Command):
+ def apply(self, ui):
+ tagstring = ui.current_buffer.get_selected_tag()
+ cmd = SearchCommand(query='tag:%s' % tagstring)
+ ui.apply_command(cmd)
diff --git a/alot/commands/thread.py b/alot/commands/thread.py
new file mode 100644
index 00000000..add2b055
--- /dev/null
+++ b/alot/commands/thread.py
@@ -0,0 +1,412 @@
+import os
+import logging
+import tempfile
+from email import Charset
+from email.header import Header
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from twisted.internet import defer
+
+from alot.commands import Command, registerCommand
+from alot.commands.globals import ExternalCommand
+from alot.commands.globals import FlushCommand
+from alot.commands.globals import ComposeCommand
+from alot import settings
+from alot import widgets
+from alot import completion
+from alot import helper
+from alot.message import encode_header
+
+MODE = 'thread'
+
+
+@registerCommand(MODE, 'reply', arguments=[
+ (['--all'], {'action':'store_true', 'help':'reply to all'})],
+ help='reply to currently selected message')
+class ReplyCommand(Command):
+ def __init__(self, message=None, all=False, **kwargs):
+ """
+ :param message: the original message to reply to
+ :type message: `alot.message.Message`
+ :param groupreply: copy other recipients from Bcc/Cc/To to the reply
+ :type groupreply: boolean
+ """
+ self.message = message
+ self.groupreply = all
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ if not self.message:
+ self.message = ui.current_buffer.get_selected_message()
+ mail = self.message.get_email()
+ # set body text
+ name, address = self.message.get_author()
+ timestamp = self.message.get_date()
+ qf = settings.hooks.get('reply_prefix')
+ if qf:
+ quotestring = qf(name, address, timestamp,
+ ui=ui, dbm=ui.dbman, aman=ui.accountman,
+ log=ui.logger, config=settings.config)
+ else:
+ quotestring = 'Quoting %s (%s)\n' % (name, timestamp)
+ mailcontent = quotestring
+ for line in self.message.accumulate_body().splitlines():
+ mailcontent += '>' + line + '\n'
+
+ Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
+ bodypart = MIMEText(mailcontent.encode('utf-8'), 'plain', 'UTF-8')
+ reply = MIMEMultipart()
+ reply.attach(bodypart)
+
+ # copy subject
+ subject = mail.get('Subject', '')
+ if not subject.startswith('Re:'):
+ subject = 'Re: ' + subject
+ reply['Subject'] = Header(subject.encode('utf-8'), 'UTF-8').encode()
+
+ # set From
+ my_addresses = ui.accountman.get_addresses()
+ matched_address = ''
+ in_to = [a for a in my_addresses if a in mail.get('To', '')]
+ if in_to:
+ matched_address = in_to[0]
+ else:
+ cc = mail.get('Cc', '') + mail.get('Bcc', '')
+ in_cc = [a for a in my_addresses if a in cc]
+ if in_cc:
+ matched_address = in_cc[0]
+ if matched_address:
+ account = ui.accountman.get_account_by_address(matched_address)
+ fromstring = '%s <%s>' % (account.realname, account.address)
+ reply['From'] = encode_header('From', fromstring)
+
+ # set To
+ del(reply['To'])
+ if self.groupreply:
+ cleared = self.clear_my_address(my_addresses, mail.get('To', ''))
+ if cleared:
+ logging.info(mail['From'] + ', ' + cleared)
+ to = mail['From'] + ', ' + cleared
+ reply['To'] = encode_header('To', to)
+ logging.info(reply['To'])
+ else:
+ reply['To'] = encode_header('To', mail['From'])
+ # copy cc and bcc for group-replies
+ if 'Cc' in mail:
+ cc = self.clear_my_address(my_addresses, mail['Cc'])
+ reply['Cc'] = encode_header('Cc', cc)
+ if 'Bcc' in mail:
+ bcc = self.clear_my_address(my_addresses, mail['Bcc'])
+ reply['Bcc'] = encode_header('Bcc', bcc)
+ else:
+ reply['To'] = encode_header('To', mail['From'])
+
+ # set In-Reply-To header
+ del(reply['In-Reply-To'])
+ reply['In-Reply-To'] = '<%s>' % self.message.get_message_id()
+
+ # set References header
+ old_references = mail.get('References', '')
+ if old_references:
+ old_references = old_references.split()
+ references = old_references[-8:]
+ if len(old_references) > 8:
+ references = old_references[:1] + references
+ references.append('<%s>' % self.message.get_message_id())
+ reply['References'] = ' '.join(references)
+ else:
+ reply['References'] = '<%s>' % self.message.get_message_id()
+
+ ui.apply_command(ComposeCommand(mail=reply))
+
+ def clear_my_address(self, my_addresses, value):
+ new_value = []
+ for entry in value.split(','):
+ if not [a for a in my_addresses if a in entry]:
+ new_value.append(entry.strip())
+ return ', '.join(new_value)
+
+
+@registerCommand(MODE, 'forward', arguments=[
+ (['--attach'], {'action':'store_true', 'help':'attach original mail'})],
+ help='forward currently selected message')
+class ForwardCommand(Command):
+ def __init__(self, message=None, attach=True, **kwargs):
+ """
+ :param message: the original message to forward. If None, the currently
+ selected one is used
+ :type message: `alot.message.Message`
+ :param attach: attach original mail instead of inline quoting its body
+ :type attach: boolean
+ """
+ self.message = message
+ self.inline = not attach
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ if not self.message:
+ self.message = ui.current_buffer.get_selected_message()
+ mail = self.message.get_email()
+
+ reply = MIMEMultipart()
+ Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
+ if self.inline: # inline mode
+ # set body text
+ name, address = self.message.get_author()
+ timestamp = self.message.get_date()
+ qf = settings.hooks.get('forward_prefix')
+ if qf:
+ quote = qf(name, address, timestamp,
+ ui=ui, dbm=ui.dbman, aman=ui.accountman,
+ log=ui.logger, config=settings.config)
+ else:
+ quote = 'Forwarded message from %s (%s):\n' % (name, timestamp)
+ mailcontent = quote
+ for line in self.message.accumulate_body().splitlines():
+ mailcontent += '>' + line + '\n'
+
+ bodypart = MIMEText(mailcontent.encode('utf-8'), 'plain', 'UTF-8')
+ reply.attach(bodypart)
+
+ else: # attach original mode
+ # create empty text msg
+ bodypart = MIMEText('', 'plain', 'UTF-8')
+ reply.attach(bodypart)
+ # attach original msg
+ reply.attach(mail)
+
+ # copy subject
+ subject = mail.get('Subject', '')
+ subject = 'Fwd: ' + subject
+ reply['Subject'] = Header(subject.encode('utf-8'), 'UTF-8').encode()
+
+ # set From
+ my_addresses = ui.accountman.get_addresses()
+ matched_address = ''
+ in_to = [a for a in my_addresses if a in mail.get('To', '')]
+ if in_to:
+ matched_address = in_to[0]
+ else:
+ cc = mail.get('Cc', '') + mail.get('Bcc', '')
+ in_cc = [a for a in my_addresses if a in cc]
+ if in_cc:
+ matched_address = in_cc[0]
+ if matched_address:
+ account = ui.accountman.get_account_by_address(matched_address)
+ fromstring = '%s <%s>' % (account.realname, account.address)
+ reply['From'] = encode_header('From', fromstring)
+ ui.apply_command(ComposeCommand(mail=reply))
+
+
+@registerCommand(MODE, 'fold', forced={'visible': False}, arguments=[
+ (['--all'], {'action': 'store_true', 'help':'fold all messages'})],
+ help='fold message(s)')
+@registerCommand(MODE, 'unfold', forced={'visible': True}, arguments=[
+ (['--all'], {'action': 'store_true', 'help':'unfold all messages'})],
+ help='unfold message(s)')
+class FoldMessagesCommand(Command):
+ def __init__(self, all=False, visible=None, **kwargs):
+ self.all = all
+ self.visible = visible
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ lines = []
+ if not self.all:
+ lines.append(ui.current_buffer.get_selection())
+ else:
+ lines = ui.current_buffer.get_message_widgets()
+
+ for widget in lines:
+ # in case the thread is yet unread, remove this tag
+ msg = widget.get_message()
+ if self.visible or (self.visible == None and widget.folded):
+ if 'unread' in msg.get_tags():
+ msg.remove_tags(['unread'])
+ ui.apply_command(FlushCommand())
+ widget.rebuild()
+ widget.fold(visible=True)
+ else:
+ widget.fold(visible=False)
+
+
+@registerCommand(MODE, 'toggleheaders',
+ help='toggle display of all headers')
+class ToggleHeaderCommand(Command):
+ def apply(self, ui):
+ msgw = ui.current_buffer.get_selection()
+ msgw.toggle_full_header()
+
+
+@registerCommand(MODE, 'pipeto', arguments=[
+ (['cmd'], {'help':'shellcommand to pipe to'}),
+ (['--all'], {'action': 'store_true', 'help':'pass all messages'}),
+ (['--separately'], {'action': 'store_true',
+ 'help':'call command once for each message'})],
+ help='pipe message(s) to stdin of a shellcommand')
+class PipeCommand(Command):
+ def __init__(self, cmd, all=False, separately=False,
+ noop_msg='no command specified', confirm_msg='',
+ done_msg='done', **kwargs):
+ Command.__init__(self, **kwargs)
+ self.cmd = cmd
+ self.whole_thread = all
+ self.separately = separately
+ self.noop_msg = noop_msg
+ self.confirm_msg = confirm_msg
+ self.done_msg = done_msg
+
+ @defer.inlineCallbacks
+ def apply(self, ui):
+ # abort if command unset
+ if not self.cmd:
+ ui.notify(self.noop_msg, priority='error')
+ return
+
+ # get messages to pipe
+ if self.whole_thread:
+ thread = ui.current_buffer.get_selected_thread()
+ if not thread:
+ return
+ to_print = thread.get_messages().keys()
+ else:
+ to_print = [ui.current_buffer.get_selected_message()]
+
+ # ask for confirmation if needed
+ if self.confirm_msg:
+ if (yield ui.choice(self.confirm_msg, select='yes',
+ cancel='no')) == 'no':
+ return
+
+ # prepare message sources
+ mailstrings = [m.get_email().as_string() for m in to_print]
+ if not self.separately:
+ mailstrings = ['\n\n'.join(mailstrings)]
+
+ # do teh monkey
+ for mail in mailstrings:
+ out, err = helper.pipe_to_command(self.cmd, mail)
+ if err:
+ ui.notify(err, priority='error')
+ return
+
+ # display 'done' message
+ if self.done_msg:
+ ui.notify(self.done_msg)
+
+
+@registerCommand(MODE, 'print', arguments=[
+ (['--all'], {'action': 'store_true', 'help':'print all messages'}),
+ (['--separately'], {'action': 'store_true',
+ 'help':'call print command once for each message'})],
+ help='print message(s)')
+class PrintCommand(PipeCommand):
+ def __init__(self, all=False, separately=False, **kwargs):
+ # get print command
+ cmd = settings.config.get('general', 'print_cmd', fallback='')
+
+ # set up notification strings
+ if all:
+ confirm_msg = 'print all messages in thread?'
+ ok_msg = 'printed thread using %s' % cmd
+ else:
+ confirm_msg = 'print selected message?'
+ ok_msg = 'printed message using %s' % cmd
+
+ # no print cmd set
+ noop_msg = 'no print command specified. Set "print_cmd" in the '\
+ 'global section.'
+ PipeCommand.__init__(self, cmd, all=all,
+ separately=separately,
+ noop_msg=noop_msg, confirm_msg=confirm_msg,
+ done_msg=ok_msg, **kwargs)
+
+
+@registerCommand(MODE, 'save', arguments=[
+ (['--all'], {'action': 'store_true', 'help':'save all attachments'}),
+ (['path'], {'nargs':'?', 'help':'path to save to'})],
+ help='save attachment(s)')
+class SaveAttachmentCommand(Command):
+ def __init__(self, all=False, path=None, **kwargs):
+ Command.__init__(self, **kwargs)
+ self.all = all
+ self.path = path
+
+ @defer.inlineCallbacks
+ def apply(self, ui):
+ pcomplete = completion.PathCompleter()
+ if self.all:
+ msg = ui.current_buffer.get_selected_message()
+ if not self.path:
+ self.path = yield ui.prompt(prefix='save attachments to:',
+ text=os.path.join('~', ''),
+ completer=pcomplete)
+ if self.path:
+ if os.path.isdir(os.path.expanduser(self.path)):
+ for a in msg.get_attachments():
+ dest = a.save(self.path)
+ name = a.get_filename()
+ if name:
+ ui.notify('saved %s as: %s' % (name, dest))
+ else:
+ ui.notify('saved attachment as: %s' % dest)
+ else:
+ ui.notify('not a directory: %s' % self.path,
+ priority='error')
+ else:
+ ui.notify('canceled')
+ else: # save focussed attachment
+ focus = ui.get_deep_focus()
+ if isinstance(focus, widgets.AttachmentWidget):
+ attachment = focus.get_attachment()
+ filename = attachment.get_filename()
+ if not self.path:
+ msg = 'save attachment (%s) to:' % filename
+ initialtext = os.path.join('~', filename)
+ self.path = yield ui.prompt(prefix=msg,
+ completer=pcomplete,
+ text=initialtext)
+ if self.path:
+ try:
+ dest = attachment.save(self.path)
+ ui.notify('saved attachment as: %s' % dest)
+ except (IOError, OSError), e:
+ ui.notify(str(e), priority='error')
+ else:
+ ui.notify('canceled')
+
+
+class OpenAttachmentCommand(Command):
+ """displays an attachment according to mailcap"""
+ def __init__(self, attachment, **kwargs):
+ Command.__init__(self, **kwargs)
+ self.attachment = attachment
+
+ def apply(self, ui):
+ logging.info('open attachment')
+ mimetype = self.attachment.get_content_type()
+ handler = settings.get_mime_handler(mimetype)
+ if handler:
+ path = self.attachment.save(tempfile.gettempdir())
+ handler = handler.replace('%s', '{}')
+
+ def afterwards():
+ os.remove(path)
+ ui.apply_command(ExternalCommand(handler, path=path,
+ on_success=afterwards,
+ thread=True))
+ else:
+ ui.notify('unknown mime type')
+
+
+@registerCommand(MODE, 'select')
+class ThreadSelectCommand(Command):
+ def apply(self, ui):
+ focus = ui.get_deep_focus()
+ if isinstance(focus, widgets.MessageSummaryWidget):
+ ui.apply_command(FoldMessagesCommand())
+ elif isinstance(focus, widgets.AttachmentWidget):
+ logging.info('open attachment')
+ ui.apply_command(OpenAttachmentCommand(focus.get_attachment()))
+ else:
+ logging.info('unknown widget %s' % focus)
diff --git a/alot/completion.py b/alot/completion.py
index 43796e5c..c29ed978 100644
--- a/alot/completion.py
+++ b/alot/completion.py
@@ -22,7 +22,7 @@ import os
import glob
import logging
-import command
+import alot.commands as commands
class Completer(object):
@@ -158,8 +158,8 @@ class CommandCompleter(Completer):
#TODO refine <tab> should get current querystring
commandprefix = original[:pos]
logging.debug('original="%s" prefix="%s"' % (original, commandprefix))
- cmdlist = command.COMMANDS['global'].copy()
- cmdlist.update(command.COMMANDS[self.mode])
+ cmdlist = commands.COMMANDS['global'].copy()
+ cmdlist.update(commands.COMMANDS[self.mode])
matching = [t for t in cmdlist if t.startswith(commandprefix)]
return [(t, len(t)) for t in matching]
diff --git a/alot/defaults/alot.rc b/alot/defaults/alot.rc
index 63bf6ad9..2641fb87 100644
--- a/alot/defaults/alot.rc
+++ b/alot/defaults/alot.rc
@@ -70,17 +70,17 @@ esc = cancel
enter = select
@ = refresh
-? = help
+? = help bindings
I = search tag:inbox AND NOT tag:killed
L = taglist
shift tab = bprevious
U = search tag:unread
tab = bnext
-\ = 'prompt search '
+\ = prompt 'search '
d = bclose
$ = flush
m = compose
-o = 'prompt search '
+o = prompt 'search '
q = exit
';' = bufferlist
colon = prompt
@@ -92,6 +92,7 @@ select = openfocussed
[search-maps]
a = toggletag inbox
& = toggletag killed
+! = toggletag flagged
l = retagprompt
O = refineprompt
select = openthread
@@ -112,10 +113,10 @@ select = reedit
C = fold --all
E = unfold --all
H = toggleheaders
-P = print --thread
+P = print --all
S = save --all
a = toggletag inbox
-g = groupreply
+g = reply --all
f = forward
p = print
s = save
diff --git a/alot/init.py b/alot/init.py
index 5b479d35..0184a6aa 100755
--- a/alot/init.py
+++ b/alot/init.py
@@ -25,7 +25,9 @@ import settings
from account import AccountManager
from db import DBManager
from ui import UI
-from command import interpret_commandline
+import alot.commands as commands
+from commands import *
+from alot.commands import CommandParseError
def parse_args():
@@ -91,6 +93,7 @@ def main():
logging.basicConfig(level=numeric_loglevel, filename=logfilename)
logger = logging.getLogger()
+ #logger.debug(commands.COMMANDS)
#accountman
aman = AccountManager(settings.config)
@@ -98,13 +101,15 @@ def main():
dbman = DBManager(path=args.db_path, ro=args.read_only)
# get initial searchstring
- if args.command != '':
- cmd = interpret_commandline(args.command, 'global')
- if cmd is None:
- sys.exit('Invalid command: ' + args.command)
- else:
- default_commandline = settings.config.get('general', 'initial_command')
- cmd = interpret_commandline(default_commandline, 'global')
+ try:
+ if args.command != '':
+ cmd = commands.commandfactory(args.command, 'global')
+ else:
+ default_commandline = settings.config.get('general',
+ 'initial_command')
+ cmd = commands.commandfactory(default_commandline, 'global')
+ except CommandParseError, e:
+ sys.exit(e)
# set up and start interface
UI(dbman,
diff --git a/alot/ui.py b/alot/ui.py
index df1af492..01828c96 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -20,10 +20,10 @@ import urwid
from twisted.internet import reactor, defer
from settings import config
-from buffer import BufferlistBuffer
-import command
-from command import commandfactory
-from command import interpret_commandline
+from buffers import BufferlistBuffer
+import commands
+from commands import commandfactory
+from alot.commands import CommandParseError
import widgets
from completion import CommandLineCompleter
@@ -44,7 +44,7 @@ class InputWrap(urwid.WidgetWrap):
def allowed_command(self, cmd):
if not self.select_cancel_only:
return True
- elif isinstance(cmd, command.SendKeypressCommand):
+ elif isinstance(cmd, commands.globals.SendKeypressCommand):
if cmd.key in ['select', 'cancel']:
return True
else:
@@ -57,10 +57,13 @@ class InputWrap(urwid.WidgetWrap):
mode = 'global'
cmdline = config.get_mapping(mode, key)
if cmdline:
- cmd = interpret_commandline(cmdline, mode)
- if self.allowed_command(cmd):
- self.ui.apply_command(cmd)
- return None
+ try:
+ cmd = commandfactory(cmdline, mode)
+ if self.allowed_command(cmd):
+ self.ui.apply_command(cmd)
+ return None
+ except CommandParseError, e:
+ self.ui.notify(e.message, priority='error')
self.ui.logger.debug('relaying key: %s' % key)
return self._w.keypress(size, key)
@@ -191,11 +194,11 @@ class UI(object):
if cmdline:
mode = self.current_buffer.typename
self.commandprompthistory.append(cmdline)
- cmd = interpret_commandline(cmdline, mode)
- if cmd:
+ try:
+ cmd = commandfactory(cmdline, mode)
self.apply_command(cmd)
- else:
- self.notify('invalid command')
+ except CommandParseError, e:
+ self.notify(e.message, priority='error')
def buffer_open(self, b):
"""
diff --git a/setup.py b/setup.py
index 301d6ee3..a42c08a6 100755
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ setup(name='alot',
author=alot.__author__,
author_email=alot.__author_email__,
url=alot.__url__,
- packages=['alot'],
+ packages=['alot', 'alot.commands'],
package_data={'alot': ['defaults/alot.rc', 'defaults/notmuch.rc']},
scripts=['bin/alot'],
license=alot.__copyright__,