@@ -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+ ])
937954class 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' ))
0 commit comments