Skip to content

Instantly share code, notes, and snippets.

@mmackh
Last active April 4, 2022 07:58
Show Gist options
  • Save mmackh/7eb06877683f659ab9c8f4fd27e42357 to your computer and use it in GitHub Desktop.
Save mmackh/7eb06877683f659ab9c8f4fd27e42357 to your computer and use it in GitHub Desktop.
PasteboardHelper - Load .eml files from Mail.app (macOS)
//
// 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
//
// 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