@@ -7,6 +7,103 @@ package nativecontextmenu
77#import <Cocoa/Cocoa.h>
88#import <Foundation/Foundation.h>
99
10+ @interface FileMenuHandler : NSObject
11+ @property (strong) NSURL *fileURL;
12+ @property (strong) NSString *filePath;
13+ - (void)openFile:(id)sender;
14+ - (void)showInFinder:(id)sender;
15+ - (void)moveToTrash:(id)sender;
16+ - (void)getInfo:(id)sender;
17+ - (void)renameFile:(id)sender;
18+ - (void)compressFile:(id)sender;
19+ - (void)duplicateFile:(id)sender;
20+ - (void)makeAlias:(id)sender;
21+ - (void)quickLook:(id)sender;
22+ - (void)copyFile:(id)sender;
23+ - (void)copyPath:(id)sender;
24+ @end
25+
26+ @implementation FileMenuHandler
27+ - (void)openFile:(id)sender {
28+ [[NSWorkspace sharedWorkspace] openURL:self.fileURL];
29+ }
30+
31+ - (void)showInFinder:(id)sender {
32+ [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[self.fileURL]];
33+ }
34+
35+ - (void)moveToTrash:(id)sender {
36+ [[NSFileManager defaultManager] trashItemAtURL:self.fileURL resultingItemURL:nil error:nil];
37+ }
38+
39+ - (void)getInfo:(id)sender {
40+ // Use AppleScript to show Get Info window
41+ NSString *script = [NSString stringWithFormat:@"tell application \"Finder\" to open information window of (POSIX file \"%@\" as alias)", self.filePath];
42+ NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script];
43+ [appleScript executeAndReturnError:nil];
44+ }
45+
46+ - (void)renameFile:(id)sender {
47+ [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[self.fileURL]];
48+ // Trigger rename via AppleScript
49+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
50+ NSString *script = @"tell application \"System Events\" to tell process \"Finder\" to keystroke return";
51+ NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script];
52+ [appleScript executeAndReturnError:nil];
53+ });
54+ }
55+
56+ - (void)compressFile:(id)sender {
57+ NSTask *task = [[NSTask alloc] init];
58+ [task setLaunchPath:@"/usr/bin/ditto"];
59+ [task setArguments:@[@"-c", @"-k", @"--sequesterRsrc", @"--keepParent", self.filePath, [NSString stringWithFormat:@"%@.zip", self.filePath]]];
60+ [task launch];
61+ }
62+
63+ - (void)duplicateFile:(id)sender {
64+ NSFileManager *fm = [NSFileManager defaultManager];
65+ NSString *directory = [self.filePath stringByDeletingLastPathComponent];
66+ NSString *filename = [[self.filePath lastPathComponent] stringByDeletingPathExtension];
67+ NSString *extension = [self.filePath pathExtension];
68+ NSString *newPath = [NSString stringWithFormat:@"%@/%@ copy.%@", directory, filename, extension];
69+
70+ int counter = 1;
71+ while ([fm fileExistsAtPath:newPath]) {
72+ newPath = [NSString stringWithFormat:@"%@/%@ copy %d.%@", directory, filename, counter++, extension];
73+ }
74+
75+ [fm copyItemAtPath:self.filePath toPath:newPath error:nil];
76+ }
77+
78+ - (void)makeAlias:(id)sender {
79+ NSString *directory = [self.filePath stringByDeletingLastPathComponent];
80+ NSString *aliasPath = [NSString stringWithFormat:@"%@/%@ alias", directory, [self.filePath lastPathComponent]];
81+ [[NSFileManager defaultManager] createSymbolicLinkAtPath:aliasPath withDestinationPath:self.filePath error:nil];
82+ }
83+
84+ - (void)quickLook:(id)sender {
85+ [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[self.fileURL]];
86+ // Trigger Quick Look via keystroke
87+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
88+ NSString *script = @"tell application \"System Events\" to keystroke \" \"";
89+ NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script];
90+ [appleScript executeAndReturnError:nil];
91+ });
92+ }
93+
94+ - (void)copyFile:(id)sender {
95+ NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
96+ [pasteboard clearContents];
97+ [pasteboard writeObjects:@[self.fileURL]];
98+ }
99+
100+ - (void)copyPath:(id)sender {
101+ NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
102+ [pasteboard clearContents];
103+ [pasteboard setString:self.filePath forType:NSPasteboardTypeString];
104+ }
105+ @end
106+
10107// ShowContextMenu displays the macOS context menu for a file or folder
11108// Returns 0 on success, non-zero on error
12109int ShowContextMenu(const char* path) {
@@ -22,107 +119,164 @@ int ShowContextMenu(const char* path) {
22119 return 2;
23120 }
24121
25- // Get the mouse location
26- NSPoint mouseLocation = [NSEvent mouseLocation];
122+ __block int result = 0;
27123
28- // Find the screen containing the mouse
29- NSScreen *targetScreen = nil;
30- for (NSScreen *screen in [NSScreen screens]) {
31- if (NSPointInRect(mouseLocation, [screen frame])) {
32- targetScreen = screen;
33- break;
34- }
35- }
124+ // All UI operations must be performed on the main thread
125+ dispatch_sync(dispatch_get_main_queue(), ^{
126+ @autoreleasepool {
127+ // Get the mouse location
128+ NSPoint mouseLocation = [NSEvent mouseLocation];
36129
37- if (!targetScreen) {
38- targetScreen = [NSScreen mainScreen];
39- }
130+ // Find the screen containing the mouse
131+ NSScreen *targetScreen = nil;
132+ for (NSScreen *screen in [NSScreen screens]) {
133+ if (NSPointInRect(mouseLocation, [screen frame])) {
134+ targetScreen = screen;
135+ break;
136+ }
137+ }
138+
139+ if (!targetScreen) {
140+ targetScreen = [NSScreen mainScreen];
141+ }
142+
143+ // Create a minimal window at the mouse position
144+ NSRect windowFrame = NSMakeRect(mouseLocation.x, mouseLocation.y, 1, 1);
145+ NSWindow *window = [[NSWindow alloc] initWithContentRect:windowFrame
146+ styleMask:NSWindowStyleMaskBorderless
147+ backing:NSBackingStoreBuffered
148+ defer:NO];
149+ [window setLevel:NSPopUpMenuWindowLevel];
150+ [window setOpaque:NO];
151+ [window setBackgroundColor:[NSColor clearColor]];
152+ [window makeKeyAndOrderFront:nil];
153+
154+ // Get file URL and attributes
155+ NSURL *fileURL = [NSURL fileURLWithPath:filePath];
156+ NSDictionary *attributes = [fileManager attributesOfItemAtPath:filePath error:nil];
157+ BOOL isDirectory = [[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeDirectory];
158+ BOOL isAppBundle = isDirectory && [filePath hasSuffix:@".app"];
159+
160+ // Create handler
161+ FileMenuHandler *handler = [[FileMenuHandler alloc] init];
162+ handler.fileURL = fileURL;
163+ handler.filePath = filePath;
164+
165+ // Create the context menu
166+ NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
167+ [menu setAutoenablesItems:NO];
168+
169+ // Open
170+ NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:@"Open"
171+ action:@selector(openFile:)
172+ keyEquivalent:@""];
173+ [openItem setTarget:handler];
174+ [openItem setEnabled:YES];
175+ [menu addItem:openItem];
176+
177+ // Show Package Contents (for .app bundles)
178+ if (isAppBundle) {
179+ NSMenuItem *showPackageItem = [[NSMenuItem alloc] initWithTitle:@"Show Package Contents"
180+ action:@selector(showInFinder:)
181+ keyEquivalent:@""];
182+ [showPackageItem setTarget:handler];
183+ [showPackageItem setEnabled:YES];
184+ [menu addItem:showPackageItem];
185+ }
186+
187+ [menu addItem:[NSMenuItem separatorItem]];
188+
189+ // Move to Trash
190+ NSMenuItem *trashItem = [[NSMenuItem alloc] initWithTitle:@"Move to Trash"
191+ action:@selector(moveToTrash:)
192+ keyEquivalent:@""];
193+ [trashItem setTarget:handler];
194+ [trashItem setEnabled:YES];
195+ [menu addItem:trashItem];
40196
41- // Convert mouse location to window coordinates
42- // macOS uses bottom-left origin, so we need to flip Y coordinate
43- CGFloat screenHeight = [targetScreen frame].size.height;
44- NSPoint windowPoint = NSMakePoint(mouseLocation.x, screenHeight - mouseLocation.y);
45-
46- // Create a minimal window at the mouse position
47- NSRect windowFrame = NSMakeRect(mouseLocation.x, mouseLocation.y, 1, 1);
48- NSWindow *window = [[NSWindow alloc] initWithContentRect:windowFrame
49- styleMask:NSWindowStyleMaskBorderless
50- backing:NSBackingStoreBuffered
51- defer:NO];
52- [window setLevel:NSPopUpMenuWindowLevel];
53- [window setOpaque:NO];
54- [window setBackgroundColor:[NSColor clearColor]];
55- [window makeKeyAndOrderFront:nil];
56-
57- // Create the context menu
58- NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
59- [menu setAutoenablesItems:YES];
60-
61- // Get file URL
62- NSURL *fileURL = [NSURL fileURLWithPath:filePath];
63-
64- // Add "Open" menu item
65- NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:@"Open"
66- action:@selector(openFile:)
67- keyEquivalent:@""];
68- [openItem setRepresentedObject:fileURL];
69- [openItem setTarget:[[NSWorkspace sharedWorkspace] class]];
70- [menu addItem:openItem];
71-
72- // Add "Show in Finder" menu item
73- NSMenuItem *showInFinderItem = [[NSMenuItem alloc] initWithTitle:@"Show in Finder"
74- action:@selector(revealInFinder:)
197+ [menu addItem:[NSMenuItem separatorItem]];
198+
199+ // Get Info
200+ NSMenuItem *infoItem = [[NSMenuItem alloc] initWithTitle:@"Get Info"
201+ action:@selector(getInfo:)
75202 keyEquivalent:@""];
76- [showInFinderItem setRepresentedObject:fileURL];
77- [showInFinderItem setTarget:[[NSWorkspace sharedWorkspace] class]];
78- [menu addItem:showInFinderItem];
79-
80- [menu addItem:[NSMenuItem separatorItem]];
81-
82- // Add "Get Info" menu item
83- NSMenuItem *getInfoItem = [[NSMenuItem alloc] initWithTitle:@"Get Info"
84- action:@selector(showFileInfo:)
85- keyEquivalent:@""];
86- [getInfoItem setRepresentedObject:fileURL];
87- [menu addItem:getInfoItem];
88-
89- // Add "Quick Look" menu item
90- NSMenuItem *quickLookItem = [[NSMenuItem alloc] initWithTitle:@"Quick Look"
91- action:@selector(quickLook:)
92- keyEquivalent:@""];
93- [quickLookItem setRepresentedObject:fileURL];
94- [menu addItem:quickLookItem];
95-
96- [menu addItem:[NSMenuItem separatorItem]];
97-
98- // Add "Copy" menu item
99- NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy"
100- action:@selector(copyFile:)
101- keyEquivalent:@""];
102- [copyItem setRepresentedObject:fileURL];
103- [menu addItem:copyItem];
104-
105- // Add "Move to Trash" menu item
106- NSMenuItem *trashItem = [[NSMenuItem alloc] initWithTitle:@"Move to Trash"
107- action:@selector(moveToTrash:)
108- keyEquivalent:@""];
109- [trashItem setRepresentedObject:fileURL];
110- [menu addItem:trashItem];
111-
112- // Display the menu
113- // Convert screen coordinates to window coordinates
114- NSPoint menuLocation = NSMakePoint(0, 0);
115-
116- // Show the menu
117- [menu popUpMenuPositioningItem:nil atLocation:menuLocation inView:[window contentView]];
118-
119- // Keep the window alive while menu is shown
120- // The menu will close automatically when user clicks outside or selects an item
121- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
122- [window close];
203+ [infoItem setTarget:handler];
204+ [infoItem setEnabled:YES];
205+ [menu addItem:infoItem];
206+
207+ // Rename
208+ NSMenuItem *renameItem = [[NSMenuItem alloc] initWithTitle:@"Rename"
209+ action:@selector(renameFile:)
210+ keyEquivalent:@""];
211+ [renameItem setTarget:handler];
212+ [renameItem setEnabled:YES];
213+ [menu addItem:renameItem];
214+
215+ // Compress
216+ NSString *compressTitle = [NSString stringWithFormat:@"Compress \"%@\"", [filePath lastPathComponent]];
217+ NSMenuItem *compressItem = [[NSMenuItem alloc] initWithTitle:compressTitle
218+ action:@selector(compressFile:)
219+ keyEquivalent:@""];
220+ [compressItem setTarget:handler];
221+ [compressItem setEnabled:YES];
222+ [menu addItem:compressItem];
223+
224+ // Duplicate
225+ NSMenuItem *duplicateItem = [[NSMenuItem alloc] initWithTitle:@"Duplicate"
226+ action:@selector(duplicateFile:)
227+ keyEquivalent:@""];
228+ [duplicateItem setTarget:handler];
229+ [duplicateItem setEnabled:YES];
230+ [menu addItem:duplicateItem];
231+
232+ // Make Alias
233+ NSMenuItem *aliasItem = [[NSMenuItem alloc] initWithTitle:@"Make Alias"
234+ action:@selector(makeAlias:)
235+ keyEquivalent:@""];
236+ [aliasItem setTarget:handler];
237+ [aliasItem setEnabled:YES];
238+ [menu addItem:aliasItem];
239+
240+ // Quick Look
241+ NSMenuItem *quickLookItem = [[NSMenuItem alloc] initWithTitle:@"Quick Look"
242+ action:@selector(quickLook:)
243+ keyEquivalent:@""];
244+ [quickLookItem setTarget:handler];
245+ [quickLookItem setEnabled:YES];
246+ [menu addItem:quickLookItem];
247+
248+ [menu addItem:[NSMenuItem separatorItem]];
249+
250+ // Copy
251+ NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy"
252+ action:@selector(copyFile:)
253+ keyEquivalent:@""];
254+ [copyItem setTarget:handler];
255+ [copyItem setEnabled:YES];
256+ [menu addItem:copyItem];
257+
258+ // Copy Path
259+ NSMenuItem *copyPathItem = [[NSMenuItem alloc] initWithTitle:@"Copy Path"
260+ action:@selector(copyPath:)
261+ keyEquivalent:@""];
262+ [copyPathItem setTarget:handler];
263+ [copyPathItem setEnabled:YES];
264+ [menu addItem:copyPathItem];
265+
266+ // Display the menu at mouse location
267+ NSPoint menuLocation = NSMakePoint(0, 0);
268+ [menu popUpMenuPositioningItem:nil atLocation:menuLocation inView:[window contentView]];
269+
270+ // Keep the window and handler alive while menu is shown
271+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
272+ [window close];
273+ // Keep handler alive
274+ (void)handler;
275+ });
276+ }
123277 });
124278
125- return 0 ;
279+ return result ;
126280 }
127281}
128282
0 commit comments