Skip to content

Commit

Permalink
commands/thread/open: add "open" command to open attachments
Browse files Browse the repository at this point in the history
This allows the user to open an attachment file with a program of their choice.

GitHub: closes pazz#1494
  • Loading branch information
pacien committed May 9, 2020
1 parent 911bd81 commit eb90fba
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 53 deletions.
164 changes: 111 additions & 53 deletions alot/commands/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,72 +934,131 @@ async def apply(self, ui):
raise CommandCanceled()


@registerCommand(MODE, 'open', arguments=[
(['cmd'], {
'help': '''Shell command to use to open the attachment. \
The path to the attachment file will be passed as an argument. \
If absent, mailcap is used to select the command \
based on the attachment's MIME type.''',
'nargs': '*',
}),
(['--thread'], {
'action': 'store_true',
'help': 'run in separate thread',
}),
(['--spawn'], {
'action': 'store_true',
'help': 'run in a new terminal window',
}),
])
class OpenAttachmentCommand(Command):
"""
opens an attachment with a given shell command
or according to .mailcap
"""

"""displays an attachment according to mailcap"""
def __init__(self, attachment, **kwargs):
def __init__(self, cmd=(), thread=False, spawn=False, **kwargs):
"""
:param attachment: attachment to open
:type attachment: :class:`~alot.db.attachment.Attachment`
:param cmd: shell command to use to open the attachment
:type cmd: list of str
:param thread: whether to run in a separate thread
:type thread: bool
:param spawn: whether to run in a new terminal window
:type spawn: bool
"""
Command.__init__(self, **kwargs)
self.attachment = attachment
self.cmd = cmd
self.thread = thread
self.spawn = spawn

async def apply(self, ui):
logging.info('open attachment')
mimetype = self.attachment.get_content_type()
try:
logging.info('open attachment')
attachment = OpenAttachmentCommand._get_attachment(ui)
external_handler = \
self._get_user_handler(attachment) if self.cmd \
else self._get_mailcap_handler(attachment)

await ui.apply_command(external_handler)

except RuntimeError as error:
ui.notify(str(error), priority='error')

def _get_user_handler(self, attachment):
temp_file_name, destructor = self._make_temp_file(attachment)
return ExternalCommand(self.cmd + [temp_file_name],
on_exit=destructor,
thread=self.thread,
spawn=self.spawn)

def _get_mailcap_handler(self, attachment):
mimetype = attachment.get_content_type()

# returns pair of preliminary command string and entry dict containing
# more info. We only use the dict and construct the command ourselves
_, entry = settings.mailcap_find_match(mimetype)
if entry:
afterwards = None # callback, will rm tempfile if used
handler_stdin = None
tempfile_name = None
handler_raw_commandstring = entry['view']
# read parameter
part = self.attachment.get_mime_representation()
parms = tuple('='.join(p) for p in part.get_params())

# in case the mailcap defined command contains no '%s',
# we pipe the files content to the handling command via stdin
if '%s' in handler_raw_commandstring:
nametemplate = entry.get('nametemplate', '%s')
prefix, suffix = parse_mailcap_nametemplate(nametemplate)

fn_hook = settings.get_hook('sanitize_attachment_filename')
if fn_hook:
# get filename
filename = self.attachment.get_filename()
prefix, suffix = fn_hook(filename, prefix, suffix)

with tempfile.NamedTemporaryFile(delete=False, prefix=prefix,
suffix=suffix) as tmpfile:
tempfile_name = tmpfile.name
self.attachment.write(tmpfile)

def afterwards():
os.unlink(tempfile_name)
else:
handler_stdin = BytesIO()
self.attachment.write(handler_stdin)
if not entry:
raise RuntimeError(
f'no mailcap handler found for MIME type {mimetype}')

afterwards = None # callback, will rm tempfile if used
handler_stdin = None
tempfile_name = None
handler_raw_commandstring = entry['view']
# read parameter
part = attachment.get_mime_representation()
parms = tuple('='.join(p) for p in part.get_params())

# in case the mailcap defined command contains no '%s',
# we pipe the files content to the handling command via stdin
if '%s' in handler_raw_commandstring:
nametemplate = entry.get('nametemplate', '%s')
prefix, suffix = parse_mailcap_nametemplate(nametemplate)
tempfile_name, afterwards = \
self._make_temp_file(attachment, prefix, suffix)
else:
handler_stdin = BytesIO()
attachment.write(handler_stdin)

# create handler command list
handler_cmd = mailcap.subst(handler_raw_commandstring, mimetype,
filename=tempfile_name, plist=parms)
# create handler command list
handler_cmd = mailcap.subst(handler_raw_commandstring, mimetype,
filename=tempfile_name, plist=parms)

handler_cmdlist = split_commandstring(handler_cmd)
handler_cmdlist = split_commandstring(handler_cmd)

# 'needsterminal' makes handler overtake the terminal
# XXX: could this be repalced with "'needsterminal' not in entry"?
overtakes = entry.get('needsterminal') is None
# 'needsterminal' makes handler overtake the terminal
# XXX: could this be replaced with "'needsterminal' not in entry"?
overtakes = entry.get('needsterminal') is None

await ui.apply_command(ExternalCommand(handler_cmdlist,
stdin=handler_stdin,
on_success=afterwards,
thread=overtakes))
return ExternalCommand(handler_cmdlist,
stdin=handler_stdin,
on_exit=afterwards,
thread=overtakes,
spawn=self.spawn)

@staticmethod
def _get_attachment(ui):
focus = ui.get_deep_focus()
if isinstance(focus, AttachmentWidget):
return focus.get_attachment()
elif getattr(focus, 'mimepart', False) and \
isinstance(focus.mimepart, Attachment):
return focus.mimepart
else:
ui.notify('unknown mime type')
raise RuntimeError('not focused on an attachment')

@staticmethod
def _make_temp_file(attachment, prefix='', suffix=''):
filename = attachment.get_filename()
sanitize_hook = settings.get_hook('sanitize_attachment_filename')
prefix, suffix = sanitize_hook(filename, prefix, suffix) \
if sanitize_hook else '', ''

with tempfile.NamedTemporaryFile(delete=False, prefix=prefix,
suffix=suffix) as tmpfile:
logging.info(f'created temp file {tmpfile.name}')
attachment.write(tmpfile)
return tmpfile.name, lambda: os.unlink(tmpfile.name)


@registerCommand(
Expand Down Expand Up @@ -1061,11 +1120,10 @@ class ThreadSelectCommand(Command):
async def apply(self, ui):
focus = ui.get_deep_focus()
if isinstance(focus, AttachmentWidget):
logging.info('open attachment')
await ui.apply_command(OpenAttachmentCommand(focus.get_attachment()))
await ui.apply_command(OpenAttachmentCommand())
elif getattr(focus, 'mimepart', False):
if isinstance(focus.mimepart, Attachment):
await ui.apply_command(OpenAttachmentCommand(focus.mimepart))
await ui.apply_command(OpenAttachmentCommand())
else:
await ui.apply_command(ChangeDisplaymodeCommand(
mimepart=True, mimetree='toggle'))
Expand Down
16 changes: 16 additions & 0 deletions docs/source/usage/modes/thread.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ The following commands are available in thread mode:
up, down, [half]page up, [half]page down, first, last, parent, first reply, last reply, next sibling, previous sibling, next, previous, next unfolded, previous unfolded, next NOTMUCH_QUERY, previous NOTMUCH_QUERY


.. _cmd.thread.open:

.. describe:: open

open attachment with a given shell command

argument
Shell command to use to open the attachment.
The path to the attachment file will be passed as an argument.
If absent, mailcap is used to select the command based on the attachment's MIME type.

optional arguments
:---thread: run in a separate thread
:---spawn: run in a new terminal window


.. _cmd.thread.pipeto:

.. describe:: pipeto
Expand Down

0 comments on commit eb90fba

Please sign in to comment.