Skip to content

Commit 13186af

Browse files
committed
commands/thread/open-attachment: add command
This allows the user to open an attachment file with a program of their choice. GitHub: closes #1494
1 parent 911bd81 commit 13186af

File tree

2 files changed

+124
-53
lines changed

2 files changed

+124
-53
lines changed

alot/commands/thread.py

Lines changed: 110 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -934,72 +934,130 @@ async def apply(self, ui):
934934
raise CommandCanceled()
935935

936936

937+
@registerCommand(MODE, 'open-attachment', arguments=[
938+
(['cmd'], {
939+
'help': '''Shell command to use to open the attachment. \
940+
The path to the attachment file will be passed as an argument. \
941+
If absent, mailcap is used to select the command \
942+
based on the attachment's MIME type.''',
943+
'nargs': '*',
944+
}),
945+
(['--thread'], {
946+
'action': 'store_true',
947+
'help': 'run in separate thread',
948+
}),
949+
(['--spawn'], {
950+
'action': 'store_true',
951+
'help': 'run in a new terminal window',
952+
}),
953+
])
937954
class OpenAttachmentCommand(Command):
955+
"""opens an attachment with a given shell command
956+
or according to mailcap"""
938957

939-
"""displays an attachment according to mailcap"""
940-
def __init__(self, attachment, **kwargs):
958+
def __init__(self, cmd=(), thread=False, spawn=False, **kwargs):
941959
"""
942-
:param attachment: attachment to open
943-
:type attachment: :class:`~alot.db.attachment.Attachment`
960+
:param cmd: shell command to use to open the attachment
961+
:type cmd: list of str
962+
:param thread: whether to run in a separate thread
963+
:type thread: bool
964+
:param spawn: whether to run in a new terminal window
965+
:type spawn: bool
944966
"""
945967
Command.__init__(self, **kwargs)
946-
self.attachment = attachment
968+
self.cmd = cmd
969+
self.thread = thread
970+
self.spawn = spawn
947971

948972
async def apply(self, ui):
949-
logging.info('open attachment')
950-
mimetype = self.attachment.get_content_type()
973+
try:
974+
logging.info('open attachment')
975+
attachment = OpenAttachmentCommand._get_attachment(ui)
976+
external_handler = \
977+
self._get_user_handler(attachment) if self.cmd \
978+
else self._get_mailcap_handler(attachment)
979+
980+
await ui.apply_command(external_handler)
981+
982+
except RuntimeError as error:
983+
ui.notify(str(error), priority='error')
984+
985+
def _get_user_handler(self, attachment):
986+
temp_file_name, destructor = self._make_temp_file(attachment)
987+
return ExternalCommand(self.cmd + [temp_file_name],
988+
on_exit=destructor,
989+
thread=self.thread,
990+
spawn=self.spawn)
991+
992+
def _get_mailcap_handler(self, attachment):
993+
mimetype = attachment.get_content_type()
951994

952995
# returns pair of preliminary command string and entry dict containing
953996
# more info. We only use the dict and construct the command ourselves
954997
_, entry = settings.mailcap_find_match(mimetype)
955-
if entry:
956-
afterwards = None # callback, will rm tempfile if used
957-
handler_stdin = None
958-
tempfile_name = None
959-
handler_raw_commandstring = entry['view']
960-
# read parameter
961-
part = self.attachment.get_mime_representation()
962-
parms = tuple('='.join(p) for p in part.get_params())
963-
964-
# in case the mailcap defined command contains no '%s',
965-
# we pipe the files content to the handling command via stdin
966-
if '%s' in handler_raw_commandstring:
967-
nametemplate = entry.get('nametemplate', '%s')
968-
prefix, suffix = parse_mailcap_nametemplate(nametemplate)
969-
970-
fn_hook = settings.get_hook('sanitize_attachment_filename')
971-
if fn_hook:
972-
# get filename
973-
filename = self.attachment.get_filename()
974-
prefix, suffix = fn_hook(filename, prefix, suffix)
975-
976-
with tempfile.NamedTemporaryFile(delete=False, prefix=prefix,
977-
suffix=suffix) as tmpfile:
978-
tempfile_name = tmpfile.name
979-
self.attachment.write(tmpfile)
980-
981-
def afterwards():
982-
os.unlink(tempfile_name)
983-
else:
984-
handler_stdin = BytesIO()
985-
self.attachment.write(handler_stdin)
998+
if not entry:
999+
raise RuntimeError(
1000+
f'no mailcap handler found for MIME type {mimetype}')
1001+
1002+
afterwards = None # callback, will rm tempfile if used
1003+
handler_stdin = None
1004+
tempfile_name = None
1005+
handler_raw_commandstring = entry['view']
1006+
# read parameter
1007+
part = attachment.get_mime_representation()
1008+
parms = tuple('='.join(p) for p in part.get_params())
1009+
1010+
# in case the mailcap defined command contains no '%s',
1011+
# we pipe the files content to the handling command via stdin
1012+
if '%s' in handler_raw_commandstring:
1013+
nametemplate = entry.get('nametemplate', '%s')
1014+
prefix, suffix = parse_mailcap_nametemplate(nametemplate)
1015+
tempfile_name, afterwards = \
1016+
self._make_temp_file(attachment, prefix, suffix)
1017+
else:
1018+
handler_stdin = BytesIO()
1019+
attachment.write(handler_stdin)
9861020

987-
# create handler command list
988-
handler_cmd = mailcap.subst(handler_raw_commandstring, mimetype,
989-
filename=tempfile_name, plist=parms)
1021+
# create handler command list
1022+
handler_cmd = mailcap.subst(handler_raw_commandstring, mimetype,
1023+
filename=tempfile_name, plist=parms)
9901024

991-
handler_cmdlist = split_commandstring(handler_cmd)
1025+
handler_cmdlist = split_commandstring(handler_cmd)
9921026

993-
# 'needsterminal' makes handler overtake the terminal
994-
# XXX: could this be repalced with "'needsterminal' not in entry"?
995-
overtakes = entry.get('needsterminal') is None
1027+
# 'needsterminal' makes handler overtake the terminal
1028+
# XXX: could this be replaced with "'needsterminal' not in entry"?
1029+
overtakes = entry.get('needsterminal') is None
9961030

997-
await ui.apply_command(ExternalCommand(handler_cmdlist,
998-
stdin=handler_stdin,
999-
on_success=afterwards,
1000-
thread=overtakes))
1031+
return ExternalCommand(handler_cmdlist,
1032+
stdin=handler_stdin,
1033+
on_exit=afterwards,
1034+
thread=overtakes,
1035+
spawn=self.spawn)
1036+
1037+
@staticmethod
1038+
def _get_attachment(ui):
1039+
focus = ui.get_deep_focus()
1040+
if isinstance(focus, AttachmentWidget):
1041+
return focus.get_attachment()
1042+
elif (getattr(focus, 'mimepart', False) and
1043+
isinstance(focus.mimepart, Attachment)):
1044+
return focus.mimepart
10011045
else:
1002-
ui.notify('unknown mime type')
1046+
raise RuntimeError('not focused on an attachment')
1047+
1048+
@staticmethod
1049+
def _make_temp_file(attachment, prefix='', suffix=''):
1050+
filename = attachment.get_filename()
1051+
sanitize_hook = settings.get_hook('sanitize_attachment_filename')
1052+
prefix, suffix = \
1053+
sanitize_hook(filename, prefix, suffix) \
1054+
if sanitize_hook else '', ''
1055+
1056+
with tempfile.NamedTemporaryFile(delete=False, prefix=prefix,
1057+
suffix=suffix) as tmpfile:
1058+
logging.info(f'created temp file {tmpfile.name}')
1059+
attachment.write(tmpfile)
1060+
return tmpfile.name, lambda: os.unlink(tmpfile.name)
10031061

10041062

10051063
@registerCommand(
@@ -1061,11 +1119,10 @@ class ThreadSelectCommand(Command):
10611119
async def apply(self, ui):
10621120
focus = ui.get_deep_focus()
10631121
if isinstance(focus, AttachmentWidget):
1064-
logging.info('open attachment')
1065-
await ui.apply_command(OpenAttachmentCommand(focus.get_attachment()))
1122+
await ui.apply_command(OpenAttachmentCommand())
10661123
elif getattr(focus, 'mimepart', False):
10671124
if isinstance(focus.mimepart, Attachment):
1068-
await ui.apply_command(OpenAttachmentCommand(focus.mimepart))
1125+
await ui.apply_command(OpenAttachmentCommand())
10691126
else:
10701127
await ui.apply_command(ChangeDisplaymodeCommand(
10711128
mimepart=True, mimetree='toggle'))

docs/source/usage/modes/thread.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ The following commands are available in thread mode:
6161
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
6262

6363

64+
.. _cmd.thread.open-attachment:
65+
66+
.. describe:: open-attachment
67+
68+
opens an attachment with a given shell command
69+
or according to mailcap
70+
71+
argument
72+
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.
73+
74+
optional arguments
75+
:---thread: run in separate thread
76+
:---spawn: run in a new terminal window
77+
6478
.. _cmd.thread.pipeto:
6579

6680
.. describe:: pipeto

0 commit comments

Comments
 (0)