diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index 4d9f448bc..41bc9c3ef 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -115,6 +115,8 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) { return dockMenu; } +static PyObject *notification_activated_callback = NULL; + @interface NotificationDelegate : NSObject @end @@ -133,13 +135,19 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) { - (void) userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification { (void)(center); (void)(notification); + if (notification_activated_callback) { + PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z", + notification.identifier ? [notification.identifier UTF8String] : NULL); + if (ret == NULL) PyErr_Print(); + else Py_DECREF(ret); + } } @end static PyObject* cocoa_send_notification(PyObject *self UNUSED, PyObject *args) { - char *title = NULL, *subtitle = NULL, *message = NULL, *path_to_image = NULL; - if (!PyArg_ParseTuple(args, "ssz|z", &title, &message, &path_to_image, &subtitle)) return NULL; + char *identifier = NULL, *title = NULL, *subtitle = NULL, *message = NULL, *path_to_image = NULL; + if (!PyArg_ParseTuple(args, "zssz|z", &identifier, &title, &message, &path_to_image, &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]; @@ -151,6 +159,7 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) { img = [[NSImage alloc] initWithContentsOfURL:url]; [url release]; [p release]; } + n.identifier = identifier ? [NSString stringWithUTF8String:identifier] : nil; n.title = title ? [NSString stringWithUTF8String:title] : nil; n.subtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : nil; n.informativeText = message ? [NSString stringWithUTF8String:message] : nil; @@ -159,6 +168,7 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) { [n setValue:@(false) forKey:@"_identityImageHasBorder"]; } [center deliverNotification:n]; + if (n.identifier) { [n.identifier release]; n.identifier = nil; } if (n.title) { [n.title release]; n.title = nil; } if (n.subtitle) { [n.subtitle release]; n.subtitle = nil; } if (n.informativeText) { [n.informativeText release]; n.informativeText = nil; } @@ -167,6 +177,41 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) { Py_RETURN_NONE; } +static void +SIGTERM_handler(int signum UNUSED) { + exit(EXIT_FAILURE); +} + +static void +call_timer_callback(PyObject *timer_callback) { + PyObject *ret = PyObject_CallObject(timer_callback, NULL); + if (ret == NULL) PyErr_Print(); + else Py_DECREF(ret); +} + +static PyObject* +cocoa_run_notification_loop(PyObject *self UNUSED, PyObject *args) { + PyObject *timer_callback; + double timeout; + if (!PyArg_ParseTuple(args, "OOd", ¬ification_activated_callback, &timer_callback, &timeout)) return NULL; + signal(SIGTERM, SIGTERM_handler); + signal(SIGINT, SIGTERM_handler); + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + NSApplication * application = [NSApplication sharedApplication]; + // prevent icon in dock + [application setActivationPolicy:NSApplicationActivationPolicyAccessory]; + // timer will fire after timeout, so fire it once at the start + call_timer_callback(timer_callback); + [NSTimer scheduledTimerWithTimeInterval:timeout + repeats:YES + block:^(NSTimer *timer UNUSED) { + call_timer_callback(timer_callback); + }]; + [application run]; + [pool drain]; + return 0; +} + void cocoa_create_global_menu(void) { NSString* app_name = find_app_name(); @@ -376,6 +421,7 @@ static PyMethodDef module_methods[] = { {"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""}, {"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""}, {"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""}, + {"cocoa_run_notification_loop", (PyCFunction)cocoa_run_notification_loop, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/kitty/notify.py b/kitty/notify.py index 8f889fc5f..89dacccab 100644 --- a/kitty/notify.py +++ b/kitty/notify.py @@ -3,20 +3,76 @@ # License: GPLv3 Copyright: 2019, Kovid Goyal +import os import subprocess +import time +from urllib.request import urlopen -from .constants import is_macos, logo_png_file +from .constants import cache_dir, is_macos, logo_png_file, version +from .utils import open_url + +CHANGELOG_URL = 'https://sw.kovidgoyal.net/kitty/changelog.html' +RELEASED_VERSION_URL = 'https://sw.kovidgoyal.net/kitty/current-version.txt' +CHECK_INTERVAL = 24 * 60 * 60 + + +def version_notification_log(): + return os.path.join(cache_dir(), 'new-version-notifications.txt') + + +def notify_new_version(version): + notify('kitty update available!', 'kitty version {} released'.format('.'.join(map(str, version)))) + + +def get_released_version(): + try: + raw = urlopen(RELEASED_VERSION_URL).read().decode('utf-8').strip() + except Exception: + raw = '0.0.0' + return tuple(map(int, raw.split('.'))) + + +notified_versions = set() + + +def save_notification(version): + notified_versions.add(version) + version = '.'.join(map(str, version)) + with open(version_notification_log(), 'a') as f: + print(version, file=f) + + +def already_notified(version): + if not hasattr(already_notified, 'read_cache'): + already_notified.read_cache = True + with open(version_notification_log()) as f: + for line in f: + notified_versions.add(tuple(map(int, line.strip().split('.')))) + return tuple(version) in notified_versions if is_macos: - from .fast_data_types import cocoa_send_notification + from .fast_data_types import cocoa_send_notification, cocoa_run_notification_loop - def notify(title, body, timeout=5000, application='kitty', icon=True): + def notify(title, body, timeout=5000, application='kitty', icon=True, identifier=None): if icon is True: icon = None - cocoa_send_notification(title, body, icon) + cocoa_send_notification(identifier, title, body, icon) + + def notification_activated(notification_identifier): + open_url(CHANGELOG_URL) + + def do_check(): + new_version = get_released_version() + if new_version > version and not already_notified(new_version): + save_notification(new_version) + notify_new_version(new_version) + + def update_check(): + cocoa_run_notification_loop(notification_activated, do_check, CHECK_INTERVAL) + else: - def notify(title, body, timeout=5000, application='kitty', icon=True): + def notify(title, body, timeout=5000, application='kitty', icon=True, identifier=None): cmd = ['notify-send', '-t', str(timeout), '-a', application] if icon is True: icon = logo_png_file @@ -24,3 +80,11 @@ else: cmd.extend(['-i', icon]) subprocess.Popen( cmd + [title, body], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL) + + def update_check(): + while True: + new_version = get_released_version() + if new_version > version and not already_notified(new_version): + save_notification(new_version) + notify_new_version(new_version) + time.sleep(CHECK_INTERVAL)