diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2011-08-27 22:04:59 +0100 |
---|---|---|
committer | Patrick Totzke <patricktotzke@gmail.com> | 2011-08-27 22:04:59 +0100 |
commit | a32214f88f41c4919272780412976c8994838d09 (patch) | |
tree | 7105d9af07ed985bf46ae6597f2983f2b3b0e46b /alot | |
parent | 744e86bbaa5f521908100f4fd3c8b8fc43a894f4 (diff) | |
parent | 4ea9ef9dab9202149e851ef1bfff2b3cb29a34d2 (diff) |
Merge branch 'develop' into addressbook
Diffstat (limited to 'alot')
-rw-r--r-- | alot/__init__.py | 4 | ||||
-rw-r--r-- | alot/buffer.py | 3 | ||||
-rw-r--r-- | alot/command.py | 125 | ||||
-rw-r--r-- | alot/message.py | 2 | ||||
-rw-r--r-- | alot/settings.py | 5 | ||||
-rw-r--r-- | alot/ui.py | 30 | ||||
-rw-r--r-- | alot/widgets.py | 19 |
7 files changed, 136 insertions, 52 deletions
diff --git a/alot/__init__.py b/alot/__init__.py index ba4f02f1..4a84dc9b 100644 --- a/alot/__init__.py +++ b/alot/__init__.py @@ -17,10 +17,10 @@ along with notmuch. If not, see <http://www.gnu.org/licenses/>. Copyright (C) 2011 Patrick Totzke <patricktotzke@gmail.com> """ __productname__ = 'alot' -__version__ = '0.1' +__version__ = '0.11' __copyright__ = "Copyright (C) 2011 Patrick Totzke" __author__ = "Patrick Totzke" __author_email__ = "patricktotzke@gmail.com" -__description__ = "Terminal User Interface for notmuch mail (notmuchmail.org)" +__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/buffer.py index 553648b4..eb476530 100644 --- a/alot/buffer.py +++ b/alot/buffer.py @@ -124,7 +124,8 @@ class EnvelopeBuffer(Buffer): for part in self.mail.walk(): if not part.is_multipart(): if part.get_content_maintype() != 'text': - lines.append(widgets.AttachmentWidget(part, selectable=False)) + lines.append(widgets.AttachmentWidget(part, + selectable=False)) self.attachment_wgt = urwid.Pile(lines) displayed_widgets.append(self.attachment_wgt) diff --git a/alot/command.py b/alot/command.py index 8deea288..1c58e370 100644 --- a/alot/command.py +++ b/alot/command.py @@ -17,11 +17,13 @@ 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 import mimetypes @@ -490,10 +492,7 @@ class ReplyCommand(Command): reply.attach(bodypart) # copy subject - if 'Subject' not in mail or mail['Subject'] == None: - subject = '' - else: - subject = mail['Subject'] + subject = mail.get('Subject', '') if not subject.startswith('Re:'): subject = 'Re: ' + subject reply['Subject'] = Header(subject.encode('utf-8'), 'UTF-8').encode() @@ -501,7 +500,7 @@ class ReplyCommand(Command): # set From my_addresses = ui.accountman.get_addresses() matched_address = '' - in_to = [a for a in my_addresses if a in mail['To']] + in_to = [a for a in my_addresses if a in mail.get('To', '')] if in_to: matched_address = in_to[0] else: @@ -515,10 +514,9 @@ class ReplyCommand(Command): reply['From'] = encode_header('From', fromstring) # set To - #reply['To'] = Header(mail['From'].encode('utf-8'), 'UTF-8').encode() del(reply['To']) if self.groupreply: - cleared = self.clear_my_address(my_addresses, mail['To']) + cleared = self.clear_my_address(my_addresses, mail.get('To', '')) if cleared: logging.info(mail['From'] + ', ' + cleared) to = mail['From'] + ', ' + cleared @@ -541,7 +539,7 @@ class ReplyCommand(Command): reply['In-Reply-To'] = '<%s>' % self.message.get_message_id() # set References header - old_references = mail['References'] + old_references = mail.get('References', '') if old_references: old_references = old_references.split() references = old_references[-8:] @@ -601,14 +599,14 @@ class ForwardCommand(Command): reply.attach(mail) # copy subject - subject = mail['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['To']] + in_to = [a for a in my_addresses if a in mail.get('To', '')] if in_to: matched_address = in_to[0] else: @@ -655,6 +653,57 @@ class ToggleHeaderCommand(Command): msgw.toggle_full_header() +class PrintCommand(Command): + def __init__(self, all=False, separately=False, confirm=True, **kwargs): + Command.__init__(self, **kwargs) + self.all = all + self.separately = separately + self.confirm = confirm + + def apply(self, ui): + # get messages to print + if self.all: + thread = ui.current_buffer.get_selected_thread() + to_print = thread.get_messages().keys() + confirm_msg = 'print all messages in thread?' + ok_msg = 'printed thread: %s' % str(thread) + else: + to_print = [ui.current_buffer.get_selected_message()] + confirm_msg = 'print this message?' + ok_msg = 'printed message: %s' % str(to_print[0]) + + # ask for confirmation if needed + if self.confirm: + if not ui.choice(confirm_msg) == 'yes': + return + + # prepare message sources + mailstrings = [m.get_email().as_string() for m in to_print] + if not self.separately: + mailstrings = ['\n\n'.join(mailstrings)] + + # get print command + cmd = settings.config.get('general', 'print_cmd') + args = shlex.split(cmd.encode('ascii')) + + # print + try: + for mail in mailstrings: + proc = subprocess.Popen(args, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = proc.communicate(mail) + if proc.poll(): # returncode is not 0 + raise OSError(err) + except OSError, e: # handle errors + ui.notify(str(e), priority='error') + return + + # display 'done' message + ui.notify(ok_msg) + + + class SaveAttachmentCommand(Command): def __init__(self, all=False, path=None, **kwargs): Command.__init__(self, **kwargs) @@ -756,17 +805,29 @@ class EnvelopeEditCommand(Command): 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) - - #split editor out headertext, bodytext = editor_input.split('\n\n', 1) + # go through multiline, utf-8 encoded headers + key = value = None for line in headertext.splitlines(): - key, value = line.strip().split(':', 1) - value = value.strip() - del self.mail[key] # ensure there is only one + 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 to value + 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(): @@ -790,7 +851,7 @@ class EnvelopeEditCommand(Command): for key in edit_headers: value = u'' if key in self.mail: - value = decode_header(self.mail[key]) + value = decode_header(self.mail.get(key, '')) headertext += '%s: %s\n' % (key, value) if self.mail.is_multipart(): @@ -871,26 +932,25 @@ class EnvelopeSendCommand(Command): class EnvelopeAttachCommand(Command): def __init__(self, path=None, mail=None, **kwargs): Command.__init__(self, **kwargs) - self.files = [] - if path: - self.files = glob.glob(os.path.expanduser(path)) self.mail = mail + self.path = path def apply(self, ui): - if not self.files: - path = ui.prompt(prefix='attach files matching:', text='~/', - completer=completion.PathCompleter()) - if path: - self.files = glob.glob(os.path.expanduser(path)) - if not self.files: - ui.notify('no matches, abort') - return - logging.info(self.files) - msg = self.mail if not msg: msg = ui.current_buffer.get_email() - for path in self.files: + + 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 @@ -933,6 +993,7 @@ COMMANDS = { 'groupreply': (ReplyCommand, {'groupreply': True}), 'forward': (ForwardCommand, {}), 'fold': (FoldMessagesCommand, {'visible': False}), + 'print': (PrintCommand, {}), 'unfold': (FoldMessagesCommand, {'visible': True}), 'select': (ThreadSelectCommand, {}), 'save': (SaveAttachmentCommand, {}), @@ -1048,6 +1109,10 @@ def interpret_commandline(cmdline, mode): 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, all=('--all' in args), + separately=('--separately' in args)) elif not params and cmd in ['exit', 'flush', 'pyshell', 'taglist', 'bclose', 'compose', 'openfocussed', diff --git a/alot/message.py b/alot/message.py index f4cc2f39..f3128eba 100644 --- a/alot/message.py +++ b/alot/message.py @@ -184,7 +184,7 @@ def extract_body(mail): raw_payload = part.get_payload(decode=True) if part.get_content_maintype() == 'text': if enc: - raw_payload = unicode(raw_payload, enc) + raw_payload = unicode(raw_payload, enc, errors='replace') else: raw_payload = unicode(raw_payload, errors='replace') if ctype == 'text/plain': diff --git a/alot/settings.py b/alot/settings.py index 55b543a3..ef3babff 100644 --- a/alot/settings.py +++ b/alot/settings.py @@ -41,6 +41,7 @@ DEFAULTS = { 'hooksfile': '~/.alot.py', 'bug_on_exit': 'False', 'timestamp_format': '', + 'print_cmd': 'muttprint', }, '16c-theme': { 'bufferlist_focus_bg': 'dark gray', @@ -242,17 +243,19 @@ DEFAULTS = { 'C': 'fold --all', 'E': 'unfold --all', 'H': 'toggleheaders', + 'P': 'print --all', 'a': 'toggletag inbox', 'enter': 'select', 'f': 'forward', 'g': 'groupreply', + 'p': 'print', 'r': 'reply', }, 'taglist-maps': { 'enter': 'select', }, 'envelope-maps': { - 'a': 'attach', + 'a': 'prompt attach ~/', 'y': 'send', 'enter': 'reedit', 't': 'prompt to ', @@ -77,7 +77,7 @@ class UI: def keypress(self, key): self.logger.debug('unhandeled input: %s' % key) - def prompt(self, prefix='>', text=u'', completer=None, tab=0 ,history=[]): + def prompt(self, prefix='>', text=u'', completer=None, tab=0, history=[]): """prompt for text input :param prefix: text to print before the input field @@ -131,7 +131,7 @@ class UI: if history: if historypos == None: history.append(editpart.get_edit_text()) - historypos = len(history)-1 + historypos = len(history) - 1 if key == 'cursor up': historypos = (historypos - 1) % len(history) else: @@ -145,6 +145,11 @@ class UI: self.mainloop.draw_screen() def commandprompt(self, startstring): + """prompt for a commandline and interpret/apply it upon enter + + :param startstring: initial text in edit part + :type startstring: str + """ self.logger.info('open command shell') mode = self.current_buffer.typename cmdline = self.prompt(prefix=':', @@ -155,12 +160,21 @@ class UI: history=self.commandprompthistory, ) if cmdline: - self.commandprompthistory.append(cmdline) - cmd = interpret_commandline(cmdline, mode) - if cmd: - self.apply_command(cmd) - else: - self.notify('invalid command') + self.interpret_commandline(cmdline) + + def interpret_commandline(self, cmdline): + """interpret and apply a commandstring + + :param cmdline: command string to apply + :type cmdline: str + """ + mode = self.current_buffer.typename + self.commandprompthistory.append(cmdline) + cmd = interpret_commandline(cmdline, mode) + if cmd: + self.apply_command(cmd) + else: + self.notify('invalid command') def buffer_open(self, b): """ diff --git a/alot/widgets.py b/alot/widgets.py index 690c6260..2b811146 100644 --- a/alot/widgets.py +++ b/alot/widgets.py @@ -59,7 +59,7 @@ class ThreadlineWidget(urwid.AttrMap): for tag in tags: tw = TagWidget(tag) self.tag_widgets.append(tw) - cols.append(('fixed', tw.len(), tw)) + cols.append(('fixed', tw.width(), tw)) authors = self.thread.get_authors() or '(None)' maxlength = config.getint('general', 'authors_maxlength') @@ -138,16 +138,17 @@ class BufferlineWidget(urwid.Text): class TagWidget(urwid.AttrMap): def __init__(self, tag): self.tag = tag - self.translated = config.get('tag translate', tag, fallback=tag) - # encode to utf-8 before passing to urwid (issue #4) + self.translated = config.get('tag-translate', tag, fallback=tag) self.translated = self.translated.encode('utf-8') - txt = urwid.Text(self.translated, wrap='clip') + self.txt = urwid.Text(self.translated, wrap='clip') normal = config.get_tagattr(tag) focus = config.get_tagattr(tag, focus=True) - urwid.AttrMap.__init__(self, txt, normal, focus) + urwid.AttrMap.__init__(self, self.txt, normal, focus) - def len(self): - return len(self.translated) + def width(self): + # evil voodoo hotfix for double width chars that may + # lead e.g. to strings with length 1 that need width 2 + return self.txt.pack()[0] def selectable(self): return True @@ -442,8 +443,8 @@ class MessageHeaderWidget(urwid.AttrMap): else: value = value + v #sanitize it a bit: - value = value.replace('\t', '') - value = value.replace('\r', '') + value = value.replace('\t', ' ') + value = ' '.join([line.strip() for line in value.splitlines()]) keyw = ('fixed', max_key_len + 1, urwid.Text(('message_header_key', key))) valuew = urwid.Text(('message_header_value', value)) |