1) Dont use deprecated code 2) Always set the dock icon on startup as the dock icon doesnt change till the dock is restarted 3) Update the app icon automatically if the mtime on the custom icon in the config dir is newer than the mtime of the sentinel file apple puts inside the application bundle to indicate it has a custom icon
997 lines
35 KiB
Objective-C
997 lines
35 KiB
Objective-C
/*
|
|
* cocoa_window.m
|
|
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
|
|
*
|
|
* Distributed under terms of the GPL3 license.
|
|
*/
|
|
|
|
|
|
#include "state.h"
|
|
#include "cleanup.h"
|
|
#include "monotonic.h"
|
|
#include <Carbon/Carbon.h>
|
|
#include <Cocoa/Cocoa.h>
|
|
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
|
#include <UserNotifications/UserNotifications.h>
|
|
#endif
|
|
|
|
#include <AvailabilityMacros.h>
|
|
// Needed for _NSGetProgname
|
|
#include <crt_externs.h>
|
|
#include <objc/runtime.h>
|
|
|
|
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 101300)
|
|
#define NSControlStateValueOn NSOnState
|
|
#define NSControlStateValueOff NSOffState
|
|
#define NSControlStateValueMixed NSMixedState
|
|
#endif
|
|
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 101200)
|
|
#define NSWindowStyleMaskResizable NSResizableWindowMask
|
|
#define NSEventModifierFlagOption NSAlternateKeyMask
|
|
#define NSEventModifierFlagCommand NSCommandKeyMask
|
|
#define NSEventModifierFlagControl NSControlKeyMask
|
|
#endif
|
|
#if (MAC_OS_X_VERSION_MAX_ALLOWED < 110000)
|
|
#define UNNotificationPresentationOptionList (1 << 3)
|
|
#define UNNotificationPresentationOptionBanner (1 << 4)
|
|
#endif
|
|
|
|
typedef int CGSConnectionID;
|
|
typedef int CGSWindowID;
|
|
typedef int CGSWorkspaceID;
|
|
typedef enum _CGSSpaceSelector {
|
|
kCGSSpaceCurrent = 5,
|
|
kCGSSpaceAll = 7
|
|
} CGSSpaceSelector;
|
|
extern CGSConnectionID _CGSDefaultConnection(void);
|
|
CFArrayRef CGSCopySpacesForWindows(CGSConnectionID Connection, CGSSpaceSelector Type, CFArrayRef Windows);
|
|
|
|
static NSMenuItem* title_menu = NULL;
|
|
|
|
|
|
static NSString*
|
|
find_app_name(void) {
|
|
size_t i;
|
|
NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
|
|
|
// Keys to search for as potential application names
|
|
NSString* name_keys[] =
|
|
{
|
|
@"CFBundleDisplayName",
|
|
@"CFBundleName",
|
|
@"CFBundleExecutable",
|
|
};
|
|
|
|
for (i = 0; i < sizeof(name_keys) / sizeof(name_keys[0]); i++)
|
|
{
|
|
id name = infoDictionary[name_keys[i]];
|
|
if (name &&
|
|
[name isKindOfClass:[NSString class]] &&
|
|
![name isEqualToString:@""])
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
|
|
char** progname = _NSGetProgname();
|
|
if (progname && *progname)
|
|
return @(*progname);
|
|
|
|
// Really shouldn't get here
|
|
return @"kitty";
|
|
}
|
|
|
|
#define debug_key(...) if (OPT(debug_keyboard)) { fprintf(stderr, __VA_ARGS__); fflush(stderr); }
|
|
|
|
// SecureKeyboardEntryController {{{
|
|
@interface SecureKeyboardEntryController : NSObject
|
|
|
|
@property (nonatomic, readonly) BOOL isDesired;
|
|
@property (nonatomic, readonly, getter=isEnabled) BOOL enabled;
|
|
|
|
+ (instancetype)sharedInstance;
|
|
|
|
- (void)toggle;
|
|
- (void)update;
|
|
|
|
@end
|
|
|
|
@implementation SecureKeyboardEntryController {
|
|
int _count;
|
|
BOOL _desired;
|
|
}
|
|
|
|
+ (instancetype)sharedInstance {
|
|
static id instance;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
instance = [[self alloc] init];
|
|
});
|
|
return instance;
|
|
}
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self) {
|
|
_desired = false;
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(applicationDidResignActive:)
|
|
name:NSApplicationDidResignActiveNotification
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(applicationDidBecomeActive:)
|
|
name:NSApplicationDidBecomeActiveNotification
|
|
object:nil];
|
|
if ([NSApp isActive]) {
|
|
[self update];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - API
|
|
|
|
- (void)toggle {
|
|
// Set _desired to the opposite of the current state.
|
|
_desired = !_desired;
|
|
debug_key("SecureKeyboardEntry: toggle called. Setting desired to %d ", _desired);
|
|
|
|
// Try to set the system's state of secure input to the desired state.
|
|
[self update];
|
|
}
|
|
|
|
- (BOOL)isEnabled {
|
|
return !!IsSecureEventInputEnabled();
|
|
}
|
|
|
|
- (BOOL)isDesired {
|
|
return _desired;
|
|
}
|
|
|
|
#pragma mark - Notifications
|
|
|
|
- (void)applicationDidResignActive:(NSNotification *)notification {
|
|
(void)notification;
|
|
if (_count > 0) {
|
|
debug_key("SecureKeyboardEntry: Application resigning active.");
|
|
[self update];
|
|
}
|
|
}
|
|
|
|
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
|
(void)notification;
|
|
if (self.isDesired) {
|
|
debug_key("SecureKeyboardEntry: Application became active.");
|
|
[self update];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Private
|
|
|
|
- (BOOL)allowed {
|
|
return [NSApp isActive];
|
|
}
|
|
|
|
- (void)update {
|
|
debug_key("Update secure keyboard entry. desired=%d active=%d\n",
|
|
(int)self.isDesired, (int)[NSApp isActive]);
|
|
const BOOL secure = self.isDesired && [self allowed];
|
|
|
|
if (secure && _count > 0) {
|
|
debug_key("Want to turn on secure input but it's already on\n");
|
|
return;
|
|
}
|
|
|
|
if (!secure && _count == 0) {
|
|
debug_key("Want to turn off secure input but it's already off\n");
|
|
return;
|
|
}
|
|
|
|
debug_key("Before: IsSecureEventInputEnabled returns %d ", (int)self.isEnabled);
|
|
if (secure) {
|
|
OSErr err = EnableSecureEventInput();
|
|
debug_key("EnableSecureEventInput err=%d ", (int)err);
|
|
if (err) {
|
|
debug_key("EnableSecureEventInput failed with error %d ", (int)err);
|
|
} else {
|
|
_count += 1;
|
|
}
|
|
} else {
|
|
OSErr err = DisableSecureEventInput();
|
|
debug_key("DisableSecureEventInput err=%d ", (int)err);
|
|
if (err) {
|
|
debug_key("DisableSecureEventInput failed with error %d ", (int)err);
|
|
} else {
|
|
_count -= 1;
|
|
}
|
|
}
|
|
debug_key("After: IsSecureEventInputEnabled returns %d\n", (int)self.isEnabled);
|
|
}
|
|
|
|
@end
|
|
// }}}
|
|
|
|
@interface GlobalMenuTarget : NSObject
|
|
+ (GlobalMenuTarget *) shared_instance;
|
|
@end
|
|
|
|
#define PENDING(selector, which) - (void)selector:(id)sender { (void)sender; set_cocoa_pending_action(which, NULL); }
|
|
|
|
@implementation GlobalMenuTarget
|
|
|
|
PENDING(edit_config_file, PREFERENCES_WINDOW)
|
|
PENDING(new_os_window, NEW_OS_WINDOW)
|
|
PENDING(detach_tab, DETACH_TAB)
|
|
PENDING(close_os_window, CLOSE_OS_WINDOW)
|
|
PENDING(close_tab, CLOSE_TAB)
|
|
PENDING(new_tab, NEW_TAB)
|
|
PENDING(next_tab, NEXT_TAB)
|
|
PENDING(previous_tab, PREVIOUS_TAB)
|
|
PENDING(new_window, NEW_WINDOW)
|
|
PENDING(close_window, CLOSE_WINDOW)
|
|
PENDING(reset_terminal, RESET_TERMINAL)
|
|
PENDING(clear_terminal_and_scrollback, CLEAR_TERMINAL_AND_SCROLLBACK)
|
|
PENDING(reload_config, RELOAD_CONFIG)
|
|
PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY)
|
|
PENDING(toggle_fullscreen, TOGGLE_FULLSCREEN)
|
|
PENDING(open_kitty_website, OPEN_KITTY_WEBSITE)
|
|
|
|
- (BOOL)validateMenuItem:(NSMenuItem *)item {
|
|
if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) {
|
|
item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff;
|
|
} else if (item.action == @selector(toggle_fullscreen:)) {
|
|
item.title = ([NSApp currentSystemPresentationOptions] & NSApplicationPresentationFullScreen) ? @"Exit Full Screen" : @"Enter Full Screen";
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
#undef PENDING
|
|
|
|
+ (GlobalMenuTarget *) shared_instance
|
|
{
|
|
static GlobalMenuTarget *sharedGlobalMenuTarget = nil;
|
|
@synchronized(self)
|
|
{
|
|
if (!sharedGlobalMenuTarget) {
|
|
sharedGlobalMenuTarget = [[GlobalMenuTarget alloc] init];
|
|
SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance];
|
|
if (!k.isDesired && [[NSUserDefaults standardUserDefaults] boolForKey:@"SecureKeyboardEntry"]) [k toggle];
|
|
}
|
|
return sharedGlobalMenuTarget;
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
typedef struct {
|
|
char key[32];
|
|
NSEventModifierFlags mods;
|
|
} GlobalShortcut;
|
|
typedef struct {
|
|
GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config;
|
|
GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal, clear_terminal_and_scrollback;
|
|
GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen, open_kitty_website;
|
|
} GlobalShortcuts;
|
|
static GlobalShortcuts global_shortcuts;
|
|
|
|
static PyObject*
|
|
cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
|
|
int mods;
|
|
unsigned int key;
|
|
const char *name;
|
|
if (!PyArg_ParseTuple(args, "siI", &name, &mods, &key)) return NULL;
|
|
GlobalShortcut *gs = NULL;
|
|
#define Q(x) if (strcmp(name, #x) == 0) gs = &global_shortcuts.x
|
|
Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file);
|
|
else Q(new_tab); else Q(next_tab); else Q(previous_tab);
|
|
else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(clear_terminal_and_scrollback); else Q(reload_config);
|
|
else Q(toggle_macos_secure_keyboard_entry); else Q(toggle_fullscreen); else Q(open_kitty_website);
|
|
#undef Q
|
|
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
|
|
int cocoa_mods;
|
|
get_cocoa_key_equivalent(key, mods, gs->key, 32, &cocoa_mods);
|
|
gs->mods = cocoa_mods;
|
|
if (gs->key[0]) Py_RETURN_TRUE;
|
|
Py_RETURN_FALSE;
|
|
}
|
|
|
|
// Implementation of applicationDockMenu: for the app delegate
|
|
static NSMenu *dockMenu = nil;
|
|
static NSMenu *
|
|
get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) {
|
|
if (!dockMenu) {
|
|
GlobalMenuTarget *global_menu_target = [GlobalMenuTarget shared_instance];
|
|
dockMenu = [[NSMenu alloc] init];
|
|
[[dockMenu addItemWithTitle:@"New OS Window"
|
|
action:@selector(new_os_window:)
|
|
keyEquivalent:@""]
|
|
setTarget:global_menu_target];
|
|
}
|
|
return dockMenu;
|
|
}
|
|
|
|
static PyObject *notification_activated_callback = NULL;
|
|
|
|
static PyObject*
|
|
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
|
|
Py_CLEAR(notification_activated_callback);
|
|
if (callback != Py_None) {
|
|
notification_activated_callback = callback;
|
|
Py_INCREF(callback);
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
#ifdef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
|
|
|
@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
|
|
@end
|
|
|
|
@implementation NotificationDelegate
|
|
- (void)userNotificationCenter:(NSUserNotificationCenter *)center
|
|
didDeliverNotification:(NSUserNotification *)notification {
|
|
(void)(center); (void)(notification);
|
|
}
|
|
|
|
- (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center
|
|
shouldPresentNotification:(NSUserNotification *)notification {
|
|
(void)(center); (void)(notification);
|
|
return YES;
|
|
}
|
|
|
|
- (void) userNotificationCenter:(NSUserNotificationCenter *)center
|
|
didActivateNotification:(NSUserNotification *)notification {
|
|
(void)(center); (void)(notification);
|
|
if (notification_activated_callback) {
|
|
PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z",
|
|
notification.userInfo[@"user_id"] ? [notification.userInfo[@"user_id"] UTF8String] : NULL);
|
|
if (ret == NULL) PyErr_Print();
|
|
else Py_DECREF(ret);
|
|
}
|
|
}
|
|
@end
|
|
|
|
static PyObject*
|
|
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
|
char *identifier = NULL, *title = NULL, *informativeText = NULL, *subtitle = NULL;
|
|
if (!PyArg_ParseTuple(args, "zsz|z", &identifier, &title, &informativeText, &subtitle)) return NULL;
|
|
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
|
|
if (!center) {PyErr_SetString(PyExc_RuntimeError, "Failed to get the user notification center"); return NULL; }
|
|
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
|
|
NSUserNotification *n = [NSUserNotification new];
|
|
if (title) n.title = @(title);
|
|
if (subtitle) n.subtitle = @(subtitle);
|
|
if (informativeText) n.informativeText = @(informativeText);
|
|
if (identifier) {
|
|
n.userInfo = @{@"user_id": @(identifier)};
|
|
}
|
|
[center deliverNotification:n];
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
#else
|
|
|
|
@interface NotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
|
|
@end
|
|
|
|
@implementation NotificationDelegate
|
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|
willPresentNotification:(UNNotification *)notification
|
|
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
|
|
(void)(center); (void)notification;
|
|
UNNotificationPresentationOptions options = UNNotificationPresentationOptionSound;
|
|
if (@available(macOS 11.0, *)) options |= UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner;
|
|
else options |= (1 << 2); // UNNotificationPresentationOptionAlert avoid deprecated warning
|
|
completionHandler(options);
|
|
}
|
|
|
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
|
withCompletionHandler:(void (^)(void))completionHandler {
|
|
(void)(center);
|
|
if (notification_activated_callback) {
|
|
NSString *identifier = [[[response notification] request] identifier];
|
|
PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z",
|
|
identifier ? [identifier UTF8String] : NULL);
|
|
if (ret == NULL) PyErr_Print();
|
|
else Py_DECREF(ret);
|
|
}
|
|
completionHandler();
|
|
}
|
|
@end
|
|
|
|
|
|
static void
|
|
schedule_notification(const char *identifier, const char *title, const char *body, const char *subtitle) {
|
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
|
if (!center) return;
|
|
// Configure the notification's payload.
|
|
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
|
|
if (title) content.title = @(title);
|
|
if (body) content.body = @(body);
|
|
if (subtitle) content.subtitle = @(subtitle);
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
// Deliver the notification
|
|
static unsigned long counter = 1;
|
|
UNNotificationRequest* request = [
|
|
UNNotificationRequest requestWithIdentifier:(identifier ? @(identifier) : [NSString stringWithFormat:@"Id_%lu", counter++])
|
|
content:content trigger:nil];
|
|
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
|
|
if (error != nil) {
|
|
log_error("Failed to show notification: %s", [[error localizedDescription] UTF8String]);
|
|
}
|
|
}];
|
|
[content release];
|
|
}
|
|
|
|
|
|
typedef struct {
|
|
char *identifier, *title, *body, *subtitle;
|
|
} QueuedNotification;
|
|
|
|
typedef struct {
|
|
QueuedNotification *notifications;
|
|
size_t count, capacity;
|
|
} NotificationQueue;
|
|
static NotificationQueue notification_queue = {0};
|
|
|
|
static void
|
|
queue_notification(const char *identifier, const char *title, const char* body, const char* subtitle) {
|
|
ensure_space_for((¬ification_queue), notifications, QueuedNotification, notification_queue.count + 16, capacity, 16, true);
|
|
QueuedNotification *n = notification_queue.notifications + notification_queue.count++;
|
|
n->identifier = identifier ? strdup(identifier) : NULL;
|
|
n->title = title ? strdup(title) : NULL;
|
|
n->body = body ? strdup(body) : NULL;
|
|
n->subtitle = subtitle ? strdup(subtitle) : NULL;
|
|
}
|
|
|
|
static void
|
|
drain_pending_notifications(BOOL granted) {
|
|
if (granted) {
|
|
for (size_t i = 0; i < notification_queue.count; i++) {
|
|
QueuedNotification *n = notification_queue.notifications + i;
|
|
schedule_notification(n->identifier, n->title, n->body, n->subtitle);
|
|
}
|
|
}
|
|
while(notification_queue.count) {
|
|
QueuedNotification *n = notification_queue.notifications + --notification_queue.count;
|
|
free(n->identifier); free(n->title); free(n->body); free(n->subtitle);
|
|
n->identifier = NULL; n->title = NULL; n->body = NULL; n->subtitle = NULL;
|
|
}
|
|
}
|
|
|
|
static PyObject*
|
|
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
|
char *identifier = NULL, *title = NULL, *body = NULL, *subtitle = NULL;
|
|
if (!PyArg_ParseTuple(args, "zsz|z", &identifier, &title, &body, &subtitle)) return NULL;
|
|
|
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
|
if (!center) Py_RETURN_NONE;
|
|
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
|
|
queue_notification(identifier, title, body, subtitle);
|
|
|
|
// The badge permission needs to be requested as well, even though it is not used,
|
|
// otherwise macOS refuses to show the preference checkbox for enable/disable notification sound.
|
|
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge)
|
|
completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
|
if (error != nil) {
|
|
log_error("Failed to request permission for showing notification: %s", [[error localizedDescription] UTF8String]);
|
|
}
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
drain_pending_notifications(granted);
|
|
});
|
|
}
|
|
];
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
#endif
|
|
|
|
@interface ServiceProvider : NSObject
|
|
@end
|
|
|
|
@implementation ServiceProvider
|
|
|
|
- (BOOL)openTab:(NSPasteboard*)pasteboard
|
|
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
|
return [self openDirsFromPasteboard:pasteboard type:NEW_TAB_WITH_WD];
|
|
}
|
|
|
|
- (BOOL)openOSWindow:(NSPasteboard*)pasteboard
|
|
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
|
return [self openDirsFromPasteboard:pasteboard type:NEW_OS_WINDOW_WITH_WD];
|
|
}
|
|
|
|
- (BOOL)openDirsFromPasteboard:(NSPasteboard *)pasteboard type:(int)type {
|
|
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
|
|
NSArray *filePathArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
|
|
NSMutableArray<NSString*> *dirPathArray = [NSMutableArray arrayWithCapacity:[filePathArray count]];
|
|
for (NSURL *url in filePathArray) {
|
|
NSString *path = [url path];
|
|
BOOL isDirectory = NO;
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) {
|
|
if (!isDirectory) path = [path stringByDeletingLastPathComponent];
|
|
if (![dirPathArray containsObject:path]) [dirPathArray addObject:path];
|
|
}
|
|
}
|
|
if ([dirPathArray count] > 0) {
|
|
// Colons are not valid in paths under macOS.
|
|
set_cocoa_pending_action(type, [[dirPathArray componentsJoinedByString:@":"] UTF8String]);
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)openFileURLs:(NSPasteboard*)pasteboard
|
|
userData:(NSString *) UNUSED userData error:(NSError **) UNUSED error {
|
|
NSDictionary *options = @{ NSPasteboardURLReadingFileURLsOnlyKey: @YES };
|
|
NSArray *urlArray = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:options];
|
|
for (NSURL *url in urlArray) {
|
|
NSString *path = [url path];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
|
set_cocoa_pending_action(LAUNCH_URLS, [[[NSURL fileURLWithPath:path] absoluteString] UTF8String]);
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
// global menu {{{
|
|
void
|
|
cocoa_create_global_menu(void) {
|
|
NSString* app_name = find_app_name();
|
|
NSMenu* bar = [[NSMenu alloc] init];
|
|
GlobalMenuTarget *global_menu_target = [GlobalMenuTarget shared_instance];
|
|
[NSApp setMainMenu:bar];
|
|
|
|
#define MENU_ITEM(menu, title, name) { \
|
|
NSMenuItem *__mi = [menu addItemWithTitle:title action:@selector(name:) keyEquivalent:@(global_shortcuts.name.key)]; \
|
|
[__mi setKeyEquivalentModifierMask:global_shortcuts.name.mods]; \
|
|
[__mi setTarget:global_menu_target]; \
|
|
}
|
|
|
|
NSMenuItem* appMenuItem =
|
|
[bar addItemWithTitle:@""
|
|
action:NULL
|
|
keyEquivalent:@""];
|
|
NSMenu* appMenu = [[NSMenu alloc] init];
|
|
[appMenuItem setSubmenu:appMenu];
|
|
|
|
[appMenu addItemWithTitle:[NSString stringWithFormat:@"About %@", app_name]
|
|
action:@selector(orderFrontStandardAboutPanel:)
|
|
keyEquivalent:@""];
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
MENU_ITEM(appMenu, @"Preferences…", edit_config_file);
|
|
MENU_ITEM(appMenu, @"Reload Preferences", reload_config);
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
NSMenu* servicesMenu = [[NSMenu alloc] init];
|
|
[NSApp setServicesMenu:servicesMenu];
|
|
[[appMenu addItemWithTitle:@"Services"
|
|
action:NULL
|
|
keyEquivalent:@""] setSubmenu:servicesMenu];
|
|
[servicesMenu release];
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", app_name]
|
|
action:@selector(hide:)
|
|
keyEquivalent:@"h"];
|
|
[[appMenu addItemWithTitle:@"Hide Others"
|
|
action:@selector(hideOtherApplications:)
|
|
keyEquivalent:@"h"]
|
|
setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand];
|
|
[appMenu addItemWithTitle:@"Show All"
|
|
action:@selector(unhideAllApplications:)
|
|
keyEquivalent:@""];
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
MENU_ITEM(appMenu, @"Secure Keyboard Entry", toggle_macos_secure_keyboard_entry);
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", app_name]
|
|
action:@selector(terminate:)
|
|
keyEquivalent:@"q"];
|
|
[appMenu release];
|
|
|
|
NSMenuItem* shellMenuItem =
|
|
[bar addItemWithTitle:@"Shell"
|
|
action:NULL
|
|
keyEquivalent:@""];
|
|
NSMenu* shellMenu = [[NSMenu alloc] initWithTitle:@"Shell"];
|
|
[shellMenuItem setSubmenu:shellMenu];
|
|
MENU_ITEM(shellMenu, @"New OS Window", new_os_window);
|
|
MENU_ITEM(shellMenu, @"New Tab", new_tab);
|
|
MENU_ITEM(shellMenu, @"New Window", new_window);
|
|
[shellMenu addItem:[NSMenuItem separatorItem]];
|
|
MENU_ITEM(shellMenu, @"Close OS Window", close_os_window);
|
|
MENU_ITEM(shellMenu, @"Close Tab", close_tab);
|
|
MENU_ITEM(shellMenu, @"Close Window", close_window);
|
|
[shellMenu addItem:[NSMenuItem separatorItem]];
|
|
MENU_ITEM(shellMenu, @"Reset", reset_terminal);
|
|
MENU_ITEM(shellMenu, @"Clear to Cursor Line", clear_terminal_and_scrollback);
|
|
[shellMenu release];
|
|
|
|
NSMenuItem* windowMenuItem =
|
|
[bar addItemWithTitle:@"Window"
|
|
action:NULL
|
|
keyEquivalent:@""];
|
|
NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
|
|
[windowMenuItem setSubmenu:windowMenu];
|
|
|
|
[windowMenu addItemWithTitle:@"Minimize"
|
|
action:@selector(performMiniaturize:)
|
|
keyEquivalent:@"m"];
|
|
[windowMenu addItemWithTitle:@"Zoom"
|
|
action:@selector(performZoom:)
|
|
keyEquivalent:@""];
|
|
[windowMenu addItem:[NSMenuItem separatorItem]];
|
|
[windowMenu addItemWithTitle:@"Bring All to Front"
|
|
action:@selector(arrangeInFront:)
|
|
keyEquivalent:@""];
|
|
|
|
[windowMenu addItem:[NSMenuItem separatorItem]];
|
|
MENU_ITEM(windowMenu, @"Show Previous Tab", previous_tab);
|
|
MENU_ITEM(windowMenu, @"Show Next Tab", next_tab);
|
|
[[windowMenu addItemWithTitle:@"Move Tab to New Window"
|
|
action:@selector(detach_tab:)
|
|
keyEquivalent:@""] setTarget:global_menu_target];
|
|
|
|
[windowMenu addItem:[NSMenuItem separatorItem]];
|
|
MENU_ITEM(windowMenu, @"Enter Full Screen", toggle_fullscreen);
|
|
[NSApp setWindowsMenu:windowMenu];
|
|
[windowMenu release];
|
|
|
|
NSMenuItem* helpMenuItem =
|
|
[bar addItemWithTitle:@"Help"
|
|
action:NULL
|
|
keyEquivalent:@""];
|
|
NSMenu* helpMenu = [[NSMenu alloc] initWithTitle:@"Help"];
|
|
[helpMenuItem setSubmenu:helpMenu];
|
|
|
|
MENU_ITEM(helpMenu, @"Visit kitty Website", open_kitty_website);
|
|
[NSApp setHelpMenu:helpMenu];
|
|
[helpMenu release];
|
|
|
|
[bar release];
|
|
|
|
class_addMethod(
|
|
object_getClass([NSApp delegate]),
|
|
@selector(applicationDockMenu:),
|
|
(IMP)get_dock_menu,
|
|
"@@:@");
|
|
|
|
|
|
[NSApp setServicesProvider:[[[ServiceProvider alloc] init] autorelease]];
|
|
#undef MENU_ITEM
|
|
}
|
|
|
|
void
|
|
cocoa_update_menu_bar_title(PyObject *pytitle) {
|
|
NSString *title = nil;
|
|
if (OPT(macos_menubar_title_max_length) > 0 && PyUnicode_GetLength(pytitle) > OPT(macos_menubar_title_max_length)) {
|
|
static char fmt[64];
|
|
snprintf(fmt, sizeof(fmt), "%%%ld.%ldU%%s", OPT(macos_menubar_title_max_length), OPT(macos_menubar_title_max_length));
|
|
DECREF_AFTER_FUNCTION PyObject *st = PyUnicode_FromFormat(fmt, pytitle, "…");
|
|
if (st) title = @(PyUnicode_AsUTF8(st));
|
|
} else {
|
|
title = @(PyUnicode_AsUTF8(pytitle));
|
|
}
|
|
if (!title) return;
|
|
NSMenu *bar = [NSApp mainMenu];
|
|
if (title_menu != NULL) {
|
|
[bar removeItem:title_menu];
|
|
}
|
|
title_menu = [bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
|
|
NSMenu *m = [[NSMenu alloc] initWithTitle:[NSString stringWithFormat:@" :: %@", title]];
|
|
[title_menu setSubmenu:m];
|
|
[m release];
|
|
} // }}}
|
|
|
|
bool
|
|
cocoa_make_window_resizable(void *w, bool resizable) {
|
|
NSWindow *window = (NSWindow*)w;
|
|
|
|
@try {
|
|
if (resizable) {
|
|
[window setStyleMask:
|
|
[window styleMask] | NSWindowStyleMaskResizable];
|
|
} else {
|
|
[window setStyleMask:
|
|
[window styleMask] & ~NSWindowStyleMaskResizable];
|
|
}
|
|
} @catch (NSException *e) {
|
|
log_error("Failed to set style mask: %s: %s", [[e name] UTF8String], [[e reason] UTF8String]);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#define NSLeftAlternateKeyMask (0x000020 | NSEventModifierFlagOption)
|
|
#define NSRightAlternateKeyMask (0x000040 | NSEventModifierFlagOption)
|
|
|
|
bool
|
|
cocoa_alt_option_key_pressed(NSUInteger flags) {
|
|
NSUInteger q = (OPT(macos_option_as_alt) == 1) ? NSRightAlternateKeyMask : NSLeftAlternateKeyMask;
|
|
return (q & flags) == q;
|
|
}
|
|
|
|
void
|
|
cocoa_toggle_secure_keyboard_entry(void) {
|
|
SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance];
|
|
[k toggle];
|
|
[[NSUserDefaults standardUserDefaults] setBool:k.isDesired forKey:@"SecureKeyboardEntry"];
|
|
}
|
|
|
|
void
|
|
cocoa_focus_window(void *w) {
|
|
NSWindow *window = (NSWindow*)w;
|
|
[window makeKeyWindow];
|
|
}
|
|
|
|
long
|
|
cocoa_window_number(void *w) {
|
|
NSWindow *window = (NSWindow*)w;
|
|
return [window windowNumber];
|
|
}
|
|
|
|
size_t
|
|
cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz) {
|
|
NSWindow *window = (NSWindow*)w;
|
|
if (!window) return 0;
|
|
NSArray *window_array = @[ @([window windowNumber]) ];
|
|
CFArrayRef spaces = CGSCopySpacesForWindows(_CGSDefaultConnection(), kCGSSpaceAll, (__bridge CFArrayRef)window_array);
|
|
CFIndex ans = CFArrayGetCount(spaces);
|
|
if (ans > 0) {
|
|
for (CFIndex i = 0; i < MIN(ans, (CFIndex)array_sz); i++) {
|
|
NSNumber *s = (NSNumber*)CFArrayGetValueAtIndex(spaces, i);
|
|
workspace_ids[i] = [s intValue];
|
|
}
|
|
} else ans = 0;
|
|
CFRelease(spaces);
|
|
return ans;
|
|
}
|
|
|
|
static PyObject*
|
|
cocoa_get_lang(PyObject UNUSED *self) {
|
|
@autoreleasepool {
|
|
|
|
NSString* locale = nil;
|
|
NSString* lang_code = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
|
|
NSString* country_code = [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
|
|
if (lang_code && country_code) {
|
|
locale = [NSString stringWithFormat:@"%@_%@", lang_code, country_code];
|
|
} else {
|
|
locale = [[NSLocale currentLocale] localeIdentifier];
|
|
}
|
|
if (!locale) { Py_RETURN_NONE; }
|
|
const char* locale_utf8 = [locale UTF8String];
|
|
return Py_BuildValue("s", locale_utf8);
|
|
|
|
} // autoreleasepool
|
|
}
|
|
|
|
monotonic_t
|
|
cocoa_cursor_blink_interval(void) {
|
|
@autoreleasepool {
|
|
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
double on_period_ms = [defaults doubleForKey:@"NSTextInsertionPointBlinkPeriodOn"];
|
|
double off_period_ms = [defaults doubleForKey:@"NSTextInsertionPointBlinkPeriodOff"];
|
|
double period_ms = [defaults doubleForKey:@"NSTextInsertionPointBlinkPeriod"];
|
|
double max_value = 60 * 1000.0, ans = -1.0;
|
|
if (on_period_ms != 0. || off_period_ms != 0.) {
|
|
ans = on_period_ms + off_period_ms;
|
|
} else if (period_ms != 0.) {
|
|
ans = period_ms;
|
|
}
|
|
return ans > max_value ? 0ll : ms_double_to_monotonic_t(ans);
|
|
|
|
} // autoreleasepool
|
|
}
|
|
|
|
void
|
|
cocoa_set_activation_policy(bool hide_from_tasks) {
|
|
[NSApp setActivationPolicy:(hide_from_tasks ? NSApplicationActivationPolicyAccessory : NSApplicationActivationPolicyRegular)];
|
|
}
|
|
|
|
void
|
|
cocoa_set_titlebar_appearance(void *w, unsigned int theme)
|
|
{
|
|
if (!theme) return;
|
|
@autoreleasepool {
|
|
NSWindow *window = (NSWindow*)w;
|
|
[window setAppearance:[NSAppearance appearanceNamed:((theme == 2) ? NSAppearanceNameVibrantDark : NSAppearanceNameVibrantLight)]];
|
|
} // autoreleasepool
|
|
}
|
|
|
|
void
|
|
cocoa_set_titlebar_color(void *w, color_type titlebar_color)
|
|
{
|
|
@autoreleasepool {
|
|
|
|
NSWindow *window = (NSWindow*)w;
|
|
|
|
double red = ((titlebar_color >> 16) & 0xFF) / 255.0;
|
|
double green = ((titlebar_color >> 8) & 0xFF) / 255.0;
|
|
double blue = (titlebar_color & 0xFF) / 255.0;
|
|
|
|
NSColor *background =
|
|
[NSColor colorWithSRGBRed:red
|
|
green:green
|
|
blue:blue
|
|
alpha:1.0];
|
|
[window setTitlebarAppearsTransparent:YES];
|
|
[window setBackgroundColor:background];
|
|
|
|
double luma = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
|
|
|
if (luma < 0.5) {
|
|
[window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]];
|
|
} else {
|
|
[window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]];
|
|
}
|
|
|
|
} // autoreleasepool
|
|
}
|
|
|
|
static PyObject*
|
|
cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) {
|
|
@autoreleasepool {
|
|
|
|
const char *url_scheme = NULL, *bundle_id = NULL;
|
|
if (!PyArg_ParseTuple(args, "s|z", &url_scheme, &bundle_id)) return NULL;
|
|
if (!url_scheme || url_scheme[0] == '\0') {
|
|
PyErr_SetString(PyExc_TypeError, "Empty url scheme");
|
|
return NULL;
|
|
}
|
|
|
|
NSString *scheme = [NSString stringWithUTF8String:url_scheme];
|
|
NSString *identifier = @"";
|
|
if (!bundle_id) {
|
|
identifier = [[NSBundle mainBundle] bundleIdentifier];
|
|
if (!identifier || identifier.length == 0) identifier = @"net.kovidgoyal.kitty";
|
|
} else if (bundle_id[0] != '\0') {
|
|
identifier = [NSString stringWithUTF8String:bundle_id];
|
|
}
|
|
// This API has been marked as deprecated. It will need to be replaced when a new approach is available.
|
|
OSStatus err = LSSetDefaultHandlerForURLScheme((CFStringRef)scheme, (CFStringRef)identifier);
|
|
if (err == noErr) Py_RETURN_NONE;
|
|
PyErr_Format(PyExc_OSError, "Failed to set default handler with error code: %d", err);
|
|
return NULL;
|
|
} // autoreleasepool
|
|
}
|
|
|
|
static PyObject*
|
|
cocoa_set_app_icon(PyObject UNUSED *self, PyObject *args) {
|
|
@autoreleasepool {
|
|
|
|
const char *icon_path = NULL, *app_path = NULL;
|
|
if (!PyArg_ParseTuple(args, "s|z", &icon_path, &app_path)) return NULL;
|
|
if (!icon_path || icon_path[0] == '\0') {
|
|
PyErr_SetString(PyExc_TypeError, "Empty icon file path");
|
|
return NULL;
|
|
}
|
|
|
|
NSString *custom_icon_path = [NSString stringWithUTF8String:icon_path];
|
|
NSString *bundle_path = @"";
|
|
if (!app_path) {
|
|
bundle_path = [[NSBundle mainBundle] bundlePath];
|
|
if (!bundle_path || bundle_path.length == 0) bundle_path = @"/Applications/kitty.app";
|
|
} else if (app_path[0] != '\0') {
|
|
bundle_path = [NSString stringWithUTF8String:app_path];
|
|
}
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:custom_icon_path]) {
|
|
PyErr_Format(PyExc_FileNotFoundError, "Icon file not found: %s", [custom_icon_path UTF8String]);
|
|
return NULL;
|
|
}
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:bundle_path]) {
|
|
PyErr_Format(PyExc_FileNotFoundError, "Application bundle not found: %s", [bundle_path UTF8String]);
|
|
return NULL;
|
|
}
|
|
|
|
NSImage *icon_image = [[NSImage alloc] initWithContentsOfFile:custom_icon_path];
|
|
BOOL result = [[NSWorkspace sharedWorkspace] setIcon:icon_image forFile:bundle_path options:NSExcludeQuickDrawElementsIconCreationOption];
|
|
[icon_image release];
|
|
if (result) Py_RETURN_NONE;
|
|
PyErr_Format(PyExc_OSError, "Failed to set custom icon %s for %s", [custom_icon_path UTF8String], [bundle_path UTF8String]);
|
|
return NULL;
|
|
|
|
} // autoreleasepool
|
|
}
|
|
|
|
static PyObject*
|
|
cocoa_set_dock_icon(PyObject UNUSED *self, PyObject *args) {
|
|
@autoreleasepool {
|
|
|
|
const char *icon_path = NULL;
|
|
if (!PyArg_ParseTuple(args, "s", &icon_path)) return NULL;
|
|
if (!icon_path || icon_path[0] == '\0') {
|
|
PyErr_SetString(PyExc_TypeError, "Empty icon file path");
|
|
return NULL;
|
|
}
|
|
NSString *custom_icon_path = [NSString stringWithUTF8String:icon_path];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:custom_icon_path]) {
|
|
NSImage *icon_image = [[[NSImage alloc] initWithContentsOfFile:custom_icon_path] autorelease];
|
|
[NSApplication sharedApplication].applicationIconImage = icon_image;
|
|
Py_RETURN_NONE;
|
|
}
|
|
return NULL;
|
|
|
|
} // autoreleasepool
|
|
}
|
|
|
|
static NSSound *beep_sound = nil;
|
|
|
|
static void
|
|
cleanup() {
|
|
@autoreleasepool {
|
|
|
|
if (dockMenu) [dockMenu release];
|
|
dockMenu = nil;
|
|
if (beep_sound) [beep_sound release];
|
|
beep_sound = nil;
|
|
|
|
#ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API
|
|
drain_pending_notifications(NO);
|
|
free(notification_queue.notifications);
|
|
notification_queue.notifications = NULL;
|
|
notification_queue.capacity = 0;
|
|
#endif
|
|
|
|
} // autoreleasepool
|
|
}
|
|
|
|
void
|
|
cocoa_hide_window_title(void *w)
|
|
{
|
|
@autoreleasepool {
|
|
|
|
NSWindow *window = (NSWindow*)w;
|
|
[window setTitleVisibility:NSWindowTitleHidden];
|
|
|
|
} // autoreleasepool
|
|
}
|
|
|
|
void
|
|
cocoa_system_beep(const char *path) {
|
|
if (!path) { NSBeep(); return; }
|
|
static const char *beep_path = NULL;
|
|
if (beep_path != path) {
|
|
if (beep_sound) [beep_sound release];
|
|
beep_sound = [[NSSound alloc] initWithContentsOfFile:@(path) byReference:YES];
|
|
}
|
|
if (beep_sound) [beep_sound play];
|
|
else NSBeep();
|
|
}
|
|
|
|
static void
|
|
uncaughtExceptionHandler(NSException *exception) {
|
|
log_error("Unhandled exception in Cocoa: %s", [[exception description] UTF8String]);
|
|
log_error("Stack trace:\n%s", [[exception.callStackSymbols description] UTF8String]);
|
|
}
|
|
|
|
void
|
|
cocoa_set_uncaught_exception_handler(void) {
|
|
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
|
|
}
|
|
|
|
static PyMethodDef module_methods[] = {
|
|
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
|
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
|
|
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
|
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
|
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
|
|
{"cocoa_set_app_icon", (PyCFunction)cocoa_set_app_icon, METH_VARARGS, ""},
|
|
{"cocoa_set_dock_icon", (PyCFunction)cocoa_set_dock_icon, METH_VARARGS, ""},
|
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
};
|
|
|
|
bool
|
|
init_cocoa(PyObject *module) {
|
|
memset(&global_shortcuts, 0, sizeof(global_shortcuts));
|
|
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
|
register_at_exit_cleanup_func(COCOA_CLEANUP_FUNC, cleanup);
|
|
return true;
|
|
}
|