Last active
April 4, 2022 07:58
-
-
Save mmackh/7eb06877683f659ab9c8f4fd27e42357 to your computer and use it in GitHub Desktop.
PasteboardHelper - Load .eml files from Mail.app (macOS)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// PasteboardHelper.h | |
// Spaceboard | |
// | |
// Created by mmackh on 28.03.22. | |
// | |
#import <Foundation/Foundation.h> | |
@class PasteboardHelperPromiseResolveRequest; | |
@interface PasteboardHelper : NSObject | |
+ (void)submitRequest:(PasteboardHelperPromiseResolveRequest *_Nonnull)request; | |
+ (NSString *_Nonnull)identifierAppleMailPasteboardTypeAutomator; | |
@end | |
@interface PasteboardHelperPromiseResolveRequest : NSObject | |
+ (instancetype _Nonnull )requestWithTimeout:(NSTimeInterval)timeout identifier:(NSString * _Nullable)identifier estimatedFileCount:(NSInteger)estimatedFileCount completionHandler:(void(^_Nonnull)(NSArray<NSURL*>* _Nonnull fileURLs))completionHandler; | |
+ (NSInteger)currentlyEstimatedFileCount; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// PasteboardHelper.m | |
// Spaceboard | |
// | |
// Created by mmackh on 28.03.22. | |
// | |
#import "PasteboardHelper.h" | |
@import AppKit; | |
@interface PasteboardHelper () | |
+ (instancetype)sharedHelper; | |
@property (nonatomic) NSMutableArray *queue; | |
@property (nonatomic) dispatch_queue_t dispatchQueue; | |
@end | |
@interface PasteboardHelperPromiseResolveRequest () | |
@property (nonatomic) NSTimer *timeoutTimer; | |
@property (nonatomic) NSTimeInterval timeout; | |
@property (nonatomic) NSString *identifier; | |
@property (nonatomic) NSInteger fileCount; | |
@property (nonatomic, copy) void(^completionHandler)(NSArray *fileURLs); | |
@property (nonatomic) NSURL *directoryURL; | |
@property (nonatomic) int const fileDescriptor; | |
@property (nonatomic) dispatch_source_t source; | |
@property (nonatomic) PasteboardRef pasteboard; | |
- (NSURL *)directoryURL; | |
- (void)invalidateDirectoryWatcher; | |
- (void)startDirectoryWatcher; | |
@end | |
@implementation PasteboardHelper | |
+ (instancetype)sharedHelper { | |
static PasteboardHelper *sharedHelper = nil; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
sharedHelper = [[PasteboardHelper alloc] init]; | |
}); | |
return sharedHelper; | |
} | |
- (instancetype)init { | |
self = [super init]; | |
if (!self) return nil; | |
self.queue = [NSMutableArray new]; | |
self.dispatchQueue = dispatch_queue_create("com.pasteboardhelper.queue", DISPATCH_QUEUE_SERIAL); | |
return self; | |
} | |
+ (void)submitRequest:(PasteboardHelperPromiseResolveRequest *)request { | |
PasteboardHelper *sharedHelper = [PasteboardHelper sharedHelper]; | |
[sharedHelper.queue addObject:request]; | |
request.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:request.timeout target:self selector:@selector(timeoutRequest:) userInfo:request.identifier repeats:NO]; | |
NSString *requestIdentifier = request.identifier; | |
dispatch_async(sharedHelper.dispatchQueue, ^{ | |
NSURL *directoryURL = request.directoryURL; | |
NSPasteboard *dragPasteboard = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag]; | |
PasteboardRef pbref = NULL; | |
OSStatus state = PasteboardCreate((CFStringRef)dragPasteboard.name, &pbref); | |
if (!pbref || state != noErr) { | |
[PasteboardHelper processRequestWithIdentifier:requestIdentifier]; | |
return; | |
} | |
request.pasteboard = pbref; | |
[request startDirectoryWatcher]; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
PasteboardSynchronize(pbref); | |
PasteboardSetPasteLocation(pbref, (CFURLRef)directoryURL); | |
NSString *targetPath = [dragPasteboard propertyListForType:(NSString *)kPasteboardTypeFileURLPromise]; | |
// macOS 12 workaround, doesn't respect PasteboardSetPasteLocation for one Email being dragged and dropped | |
if (targetPath && ![targetPath isEqualToString:directoryURL.path]) { | |
request.directoryURL = [NSURL fileURLWithPath:targetPath]; | |
[request startDirectoryWatcher]; | |
} | |
}); | |
}); | |
} | |
+ (void)timeoutRequest:(NSTimer *)timer { | |
[self processRequestWithIdentifier:timer.userInfo]; | |
} | |
+ (void)processRequestWithIdentifier:(NSString *)identifier { | |
PasteboardHelper *sharedHelper = [PasteboardHelper sharedHelper]; | |
PasteboardHelperPromiseResolveRequest *currentRequest = nil; | |
for (PasteboardHelperPromiseResolveRequest *request in sharedHelper.queue) { | |
if ([identifier isEqualToString:request.identifier]) { | |
currentRequest = request; | |
break; | |
} | |
} | |
NSError *error = nil; | |
NSArray *fileURLs = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:currentRequest.directoryURL includingPropertiesForKeys:nil options:0 error:&error]; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
if (error) { | |
currentRequest.completionHandler(@[]); | |
} else { | |
currentRequest.completionHandler(fileURLs); | |
} | |
}); | |
[currentRequest invalidateDirectoryWatcher]; | |
if (currentRequest.pasteboard) { | |
CFRelease(currentRequest.pasteboard); | |
currentRequest.pasteboard = NULL; | |
} | |
[currentRequest.timeoutTimer invalidate]; | |
currentRequest.timeoutTimer = nil; | |
[sharedHelper.queue removeObject:currentRequest]; | |
} | |
+ (NSString *)identifierAppleMailPasteboardTypeAutomator { | |
return @"com.apple.mail.PasteboardTypeAutomator"; | |
} | |
@end | |
@implementation PasteboardHelperPromiseResolveRequest | |
+ (instancetype _Nonnull )requestWithTimeout:(NSTimeInterval)timeout identifier:(NSString * _Nullable)identifier estimatedFileCount:(NSInteger)estimatedFileCount completionHandler:(void(^_Nonnull)(NSArray<NSURL*>* _Nonnull fileURLs))completionHandler { | |
PasteboardHelperPromiseResolveRequest *request = [PasteboardHelperPromiseResolveRequest new]; | |
request.timeout = timeout; | |
request.identifier = identifier ?: @"pasteboard"; | |
request.fileCount = !estimatedFileCount ? 1 : estimatedFileCount; | |
request.completionHandler = completionHandler; | |
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:request.identifier]; | |
NSFileManager *fileManager = [NSFileManager defaultManager]; | |
if ([fileManager fileExistsAtPath:path]) { | |
[fileManager removeItemAtPath:path error:nil]; | |
} | |
[fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil]; | |
request.directoryURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:request.identifier]]; | |
return request; | |
} | |
+ (NSInteger)currentlyEstimatedFileCount { | |
NSPasteboard *dragPasteboard = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag]; | |
if ([dragPasteboard canReadItemWithDataConformingToTypes:@[[PasteboardHelper identifierAppleMailPasteboardTypeAutomator]]]) { | |
id plist = [dragPasteboard propertyListForType:[PasteboardHelper identifierAppleMailPasteboardTypeAutomator]]; | |
if ([plist isKindOfClass:[NSArray class]]) { | |
return [plist count]; | |
} | |
} | |
return dragPasteboard.pasteboardItems.count; | |
} | |
- (void)invalidateDirectoryWatcher { | |
if (self.source) { | |
dispatch_source_cancel(self.source); | |
self.source = NULL; | |
} | |
if (self.fileDescriptor > 0) { | |
close(self.fileDescriptor); | |
} | |
} | |
- (void)startDirectoryWatcher { | |
[self invalidateDirectoryWatcher]; | |
NSString *requestIdentifier = self.identifier; | |
__block NSInteger fileCount = self.fileCount; | |
// check if file location already contains the number of items for request, return success immediatly | |
if ([[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.directoryURL.path error:nil].count == fileCount) { | |
[PasteboardHelper processRequestWithIdentifier:requestIdentifier]; | |
return; | |
} | |
int const fileDescriptor = open(self.directoryURL.path.fileSystemRepresentation, O_EVTONLY); | |
if (fileDescriptor < 0) { | |
[PasteboardHelper processRequestWithIdentifier:requestIdentifier]; | |
return; | |
} | |
self.fileDescriptor = fileDescriptor; | |
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileDescriptor, DISPATCH_VNODE_WRITE | DISPATCH_VNODE_DELETE, PasteboardHelper.sharedHelper.dispatchQueue); | |
self.source = source; | |
dispatch_source_set_event_handler(source, ^() { | |
unsigned long const data = dispatch_source_get_data(source); | |
if (data & DISPATCH_VNODE_WRITE) { | |
fileCount -= 1; | |
if (fileCount > 0) return; | |
[PasteboardHelper processRequestWithIdentifier:requestIdentifier]; | |
} | |
}); | |
dispatch_resume(source); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment