diff --git a/send2trash/plat_osx.py b/send2trash/plat_osx.py index 4ac830e..37909f6 100644 --- a/send2trash/plat_osx.py +++ b/send2trash/plat_osx.py @@ -6,18 +6,21 @@ from __future__ import unicode_literals -from ctypes import cdll, byref, Structure, c_char, c_char_p +from ctypes import cdll, byref, Structure, c_char, c_char_p, c_void_p, c_int64, sizeof from ctypes.util import find_library from .compat import binary_type Foundation = cdll.LoadLibrary(find_library('Foundation')) CoreServices = cdll.LoadLibrary(find_library('CoreServices')) +objc = cdll.LoadLibrary(find_library('objc')) GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString GetMacOSStatusCommentString.restype = c_char_p FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync +AEGetParamDesc = CoreServices.AEGetParamDesc +AESendMessage = CoreServices.AESendMessage kFSPathMakeRefDefaultOptions = 0 kFSPathMakeRefDoNotFollowLeafSymlink = 0x01 @@ -36,9 +39,12 @@ def check_op_result(op_result): msg = GetMacOSStatusCommentString(op_result).decode('utf-8') raise OSError(msg) -def send2trash(path): +def send2trash(path, with_put_back=False): if not isinstance(path, binary_type): path = path.encode('utf-8') + _with_put_back(path) if with_put_back else _normally(path) + +def _normally(path): fp = FSRef() opts = kFSPathMakeRefDoNotFollowLeafSymlink op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None) @@ -46,3 +52,106 @@ def send2trash(path): opts = kFSFileOperationDefaultOptions op_result = FSMoveObjectToTrashSync(byref(fp), None, opts) check_op_result(op_result) + +# Everything below here is required for getting the "put back" feature +# in the macOS trash - we attempt to ask Finder the trash the file for us. + +objc.objc_getClass.restype = c_void_p +objc.sel_registerName.restype = c_void_p +objc.objc_msgSend.restype = c_void_p +objc.objc_msgSend.argtypes = [c_void_p, c_void_p] + +_msg = objc.objc_msgSend +_cls = objc.objc_getClass +_sel = objc.sel_registerName + +kAEWaitReply = 0x3 +kAnyTransactionID = 0 +kAEDefaultTimeout = -1 +kAutoGenerateReturnID = -1 +NSUTF8StringEncoding = 0x4 + +# TODO: this is probably incorrect/or changes depending on arch, etc +typeKernelProcessID = 0x6b706964 + +# TODO: not sure exactly what these should be +keyDirectObject = '----' +typeWildCard = '****' + +class AEDataStorage(Structure): + _fields_ = [] + +# AppleEvent seems to be an alias of AEDesc +class AEDesc(Structure): + # DescType is an unsigned int ? + # AEDataStorage is something else ? + _fields_ = [('descriptorType', c_int64), ('dataHandle', AEDataStorage)] + +# This was inspired by https://github.com/ali-rantakari/trash. +# See https://github.com/ali-rantakari/trash/blob/master/trash.m#L263-L341 +# and also https://stackoverflow.com/a/1490644/5552584. +def _with_put_back(path): + # Create an autorelease pool. + NSAutoreleasePool = _cls('NSAutoreleasePool') + pool = _msg(NSAutoreleasePool, _sel('alloc')) + pool = _msg(pool, _sel('init')) + + try: + # Generate list descriptor containing the file URL + NSAppleEventDescriptor = _cls('NSAppleEventDescriptor') + url_list_descr = _msg(NSAppleEventDescriptor, _sel('listDescriptor')) + url = _msg(_cls('NSURL'), _sel('fileURLWithPath'), path) + url_str = _msg(url, _sel('absoluteString')) + data = _msg(url_str, _sel('dataUsingEncoding:'), NSUTF8StringEncoding) + descr = _msg(NSAppleEventDescriptor, + _sel('descriptorWithDescriptorType:'), 'furl', + _sel('data:'), data) + _msg(url_list_descr, + _sel('insertDescriptor:'), descr, + _sel('atIndex:'), 1) + + # Generate the 'top level' "delete" descriptor + finder_pid = _get_finder_pid() + target_descr = _msg(NSAppleEventDescriptor, + _sel('descriptorWithDescriptorType:'), typeKernelProcessID, + _sel('bytes:'), byref(finder_pid), + _sel('length:'), sizeof(finder_pid)) + descriptor = _msg(NSAppleEventDescriptor, + _sel('appleEventWithEventClass:'), 'core', + _sel('eventID:'), 'delo', + _sel('targetDescriptor:'), target_descr, + _sel('returnID:'), kAutoGenerateReturnID, + _sel('transactionID:'), kAnyTransactionID) + + # add the list of file URLs as argument + _msg(descriptor, + _sel('setDescriptor:'), url_list_descr, + _sel('forKeyword:'), '----') + + # send the Apple Event synchronously + reply_event = AEDesc() + op_result = AESendMessage(_msg(descriptor, _sel('asDesc')), + byref(reply_event), kAEWaitReply, kAEDefaultTimeout) + check_op_result(op_result) + + # check reply in order to determine return value + reply_ae_descr = AEDesc() + op_result = AEGetParamDesc(byref(reply_event), keyDirectObject, + typeWildCard, byref(reply_ae_descr)) + check_op_result(op_result) + + reply_descr = _msg(_msg(_msg(NSAppleEventDescriptor, _sel('alloc')), + _sel('initWithAEDescNoCopy:'), byref(reply_ae_descr)), _sel('autorelease')) + + if _msg(reply_descr, _sel('numberOfItems')) == 0: + raise Exception('file could not be trashed') + finally: + _msg(pool, _sel('release')) + +def _get_finder_pid(): + import subprocess + child = subprocess.Popen(['pgrep', '-f', 'Finder'], + stdout=subprocess.PIPE, shell=False) + response = child.communicate()[0] + # TODO: assumed that pid_t is a c_int64 (should depend on arch) + return c_int64(int(response.split()[0]))