|
@@ -1,7 +1,48 @@
|
|
|
+#import <AppKit/AppKit.h>
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
-#import "AppDelegate.h"
|
|
|
+#import <CoreServices/CoreServices.h>
|
|
|
+#import <Security/Security.h>
|
|
|
+#import <ServiceManagement/ServiceManagement.h>
|
|
|
#import "app_darwin.h"
|
|
|
|
|
|
+@interface AppDelegate ()
|
|
|
+
|
|
|
+@property (strong, nonatomic) NSStatusItem *statusItem;
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
+@implementation AppDelegate
|
|
|
+
|
|
|
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
|
|
+ // show status menu
|
|
|
+ NSMenu *menu = [[NSMenu alloc] init];
|
|
|
+ [menu addItemWithTitle:@"Quit Ollama" action:@selector(quit) keyEquivalent:@"q"];
|
|
|
+ self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
|
|
|
+ [self.statusItem addObserver:self forKeyPath:@"button.effectiveAppearance" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil];
|
|
|
+
|
|
|
+ self.statusItem.menu = menu;
|
|
|
+ [self showIcon];
|
|
|
+}
|
|
|
+
|
|
|
+-(void) showIcon {
|
|
|
+ NSAppearance* appearance = self.statusItem.button.effectiveAppearance;
|
|
|
+ NSString* appearanceName = (NSString*)(appearance.name);
|
|
|
+ NSString* iconName = [[appearanceName lowercaseString] containsString:@"dark"] ? @"iconDark" : @"icon";
|
|
|
+ NSImage* statusImage = [NSImage imageNamed:iconName];
|
|
|
+ [statusImage setTemplate:YES];
|
|
|
+ self.statusItem.button.image = statusImage;
|
|
|
+}
|
|
|
+
|
|
|
+-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
|
|
|
+ [self showIcon];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)quit {
|
|
|
+ [NSApp stop:nil];
|
|
|
+}
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
void run() {
|
|
|
@autoreleasepool {
|
|
|
[NSApplication sharedApplication];
|
|
@@ -33,9 +74,139 @@ void killOtherInstances() {
|
|
|
kill(app.processIdentifier, SIGTERM);
|
|
|
}
|
|
|
|
|
|
+ NSDate *startTime = [NSDate date];
|
|
|
for (NSRunningApplication *app in apps) {
|
|
|
while (!app.terminated) {
|
|
|
+ if (-[startTime timeIntervalSinceNow] >= 5) {
|
|
|
+ kill(app.processIdentifier, SIGKILL);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+bool askToMoveToApplications() {
|
|
|
+ NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
|
|
+ if ([bundlePath hasPrefix:@"/Applications"]) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSAlert *alert = [[NSAlert alloc] init];
|
|
|
+ [alert setMessageText:@"Move to Applications?"];
|
|
|
+ [alert setInformativeText:@"Ollama works best when run from the Applications directory."];
|
|
|
+ [alert addButtonWithTitle:@"Move to Applications"];
|
|
|
+ [alert addButtonWithTitle:@"Don't move"];
|
|
|
+
|
|
|
+ [NSApp activateIgnoringOtherApps:YES];
|
|
|
+
|
|
|
+ if ([alert runModal] != NSAlertFirstButtonReturn) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // move to applications
|
|
|
+ NSString *applicationsPath = @"/Applications";
|
|
|
+ NSString *newPath = [applicationsPath stringByAppendingPathComponent:@"Ollama.app"];
|
|
|
+ NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
+
|
|
|
+ // Check if the newPath already exists
|
|
|
+ if ([fileManager fileExistsAtPath:newPath]) {
|
|
|
+ NSError *removeError = nil;
|
|
|
+ [fileManager removeItemAtPath:newPath error:&removeError];
|
|
|
+ if (removeError) {
|
|
|
+ NSLog(@"Error removing file at %@: %@", newPath, removeError);
|
|
|
+ return false; // or handle the error
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ NSError *moveError = nil;
|
|
|
+ [fileManager moveItemAtPath:bundlePath toPath:newPath error:&moveError];
|
|
|
+ if (moveError) {
|
|
|
+ NSLog(@"Error moving file from %@ to %@: %@", bundlePath, newPath, moveError);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSLog(@"Opening %@", newPath);
|
|
|
+ NSError *error = nil;
|
|
|
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
|
|
|
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
|
+ [workspace launchApplicationAtURL:[NSURL fileURLWithPath:newPath]
|
|
|
+ options:NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchDefault
|
|
|
+ configuration:@{}
|
|
|
+ error:&error];
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+int installSymlink() {
|
|
|
+ NSString *linkPath = @"/usr/local/bin/ollama";
|
|
|
+ NSError *error = nil;
|
|
|
+
|
|
|
+ NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
+ NSString *symlinkPath = [fileManager destinationOfSymbolicLinkAtPath:linkPath error:&error];
|
|
|
+ NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
|
|
+ NSString *execPath = [[NSBundle mainBundle] executablePath];
|
|
|
+ NSString *resPath = [[NSBundle mainBundle] pathForResource:@"ollama" ofType:nil];
|
|
|
+
|
|
|
+ // if the symlink already exists and points to the right place, don't prompt
|
|
|
+ if ([symlinkPath isEqualToString:resPath]) {
|
|
|
+ NSLog(@"symbolic link already exists and points to the right place");
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSString *authorizationPrompt = @"Ollama is trying to install its command line interface (CLI) tool.";
|
|
|
+
|
|
|
+ AuthorizationRef auth = NULL;
|
|
|
+ OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
|
|
|
+ if (createStatus != errAuthorizationSuccess) {
|
|
|
+ NSLog(@"Error creating authorization");
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
|
|
|
+ NSString *rightNameString = [NSString stringWithFormat:@"%@.%@", bundleIdentifier, @"auth3"];
|
|
|
+ const char *rightName = rightNameString.UTF8String;
|
|
|
+
|
|
|
+ OSStatus getRightResult = AuthorizationRightGet(rightName, NULL);
|
|
|
+ if (getRightResult == errAuthorizationDenied) {
|
|
|
+ if (AuthorizationRightSet(auth, rightName, (__bridge CFTypeRef _Nonnull)(@(kAuthorizationRuleAuthenticateAsAdmin)), (__bridge CFStringRef _Nullable)(authorizationPrompt), NULL, NULL) != errAuthorizationSuccess) {
|
|
|
+ NSLog(@"Failed to set right");
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ AuthorizationItem right = { .name = rightName, .valueLength = 0, .value = NULL, .flags = 0 };
|
|
|
+ AuthorizationRights rights = { .count = 1, .items = &right };
|
|
|
+ AuthorizationFlags flags = (AuthorizationFlags)(kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed);
|
|
|
+ AuthorizationItem iconAuthorizationItem = {.name = kAuthorizationEnvironmentIcon, .valueLength = 0, .value = NULL, .flags = 0};
|
|
|
+ AuthorizationEnvironment authorizationEnvironment = {.count = 0, .items = NULL};
|
|
|
+
|
|
|
+ BOOL failedToUseSystemDomain = NO;
|
|
|
+ OSStatus copyStatus = AuthorizationCopyRights(auth, &rights, &authorizationEnvironment, flags, NULL);
|
|
|
+ if (copyStatus != errAuthorizationSuccess) {
|
|
|
+ failedToUseSystemDomain = YES;
|
|
|
+
|
|
|
+ if (copyStatus == errAuthorizationCanceled) {
|
|
|
+ NSLog(@"User cancelled authorization");
|
|
|
+ return -1;
|
|
|
+ } else {
|
|
|
+ NSLog(@"Failed copying system domain rights: %d", copyStatus);
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const char *toolPath = "/bin/ln";
|
|
|
+ const char *args[] = {"-s", "-F", [resPath UTF8String], "/usr/local/bin/ollama", NULL};
|
|
|
+ FILE *pipe = NULL;
|
|
|
+
|
|
|
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
|
+ OSStatus status = AuthorizationExecuteWithPrivileges(auth, toolPath, kAuthorizationFlagDefaults, (char *const *)args, &pipe);
|
|
|
+ if (status != errAuthorizationSuccess) {
|
|
|
+ NSLog(@"Failed to create symlink");
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ AuthorizationFree(auth, kAuthorizationFlagDestroyRights);
|
|
|
+ return 0;
|
|
|
+}
|