diff --git a/docs/changelog.rst b/docs/changelog.rst index 26dd10a65..014aebfee 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -40,6 +40,8 @@ Detailed list of changes - X11: Fix a regression in the previous release that caused pasting from GTK based applications to have extra newlines (:iss:`5528`) +- macOS: Allow to set custom app icon automatically (:pull:`5464`) + 0.26.3 [2022-09-22] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/faq.rst b/docs/faq.rst index 3b884dab2..9068cb515 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -287,7 +287,24 @@ homepage: :target: https://github.com/samholmes/whiskers :width: 256 -On macOS you can change the icon by following the steps: +On macOS you can put :file:`kitty.app.icns` or :file:`kitty.app.png` in the +:ref:`kitty configuration directory `, and this icon will be applied +automatically at startup if the app bundle has no custom icon. This is +convenient because app updates under macOS will replace the entire app bundle +and the custom icon will be removed as well. To automatically update a new icon +at startup, you need to remove the custom icon on :file:`kitty.app` first. + +You can set custom icon via CLI, which can be used in shell scripts: + +.. code-block:: sh + + # Set kitty.icns as the icon for currently running kitty + kitty +runpy 'from kitty.fast_data_types import cocoa_set_app_icon; import sys; cocoa_set_app_icon(*sys.argv[1:]); print("OK")' kitty.icns + + # Set the icon for app bundle specified by the path + kitty +runpy 'from kitty.fast_data_types import cocoa_set_app_icon; import sys; cocoa_set_app_icon(*sys.argv[1:]); print("OK")' /path/to/icon.png /Applications/kitty.app + +You can also change the icon manually by following the steps: #. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+I` #. Drag :file:`kitty.icns` onto the application icon in the kitty info pane diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index 477a3154d..11ec6bcc8 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -862,6 +862,98 @@ cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) { } // autoreleasepool } +static PyObject* +cocoa_app_has_custom_icon(PyObject UNUSED *self, PyObject *args) { + @autoreleasepool { + + const char *app_path = NULL; + if (!PyArg_ParseTuple(args, "|z", &app_path)) return NULL; + NSString *bundle_path; + if (app_path && app_path[0] != '\0') bundle_path = [NSString stringWithUTF8String:app_path]; + else bundle_path = [[NSBundle mainBundle] bundlePath]; + if (!bundle_path || bundle_path.length == 0) bundle_path = @"/Applications/kitty.app"; + + // These APIs have been marked as deprecated. + // However support for NSURLCustomIconKey has never been implemented by Apple (so far, macOS 12.5.x and below). + // so the following NSImage icon_image will be nil even if a custom icon is set: + // [[NSURL fileURLWithPath:bundle_path] getResourceValue:&icon_image forKey:NSURLCustomIconKey error:nil] +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FSRef ref; + FSCatalogInfo catalog_info; + OSStatus err = FSPathMakeRef((const UInt8 *)[bundle_path fileSystemRepresentation], &ref, NULL); + if (err == noErr) { + err = FSGetCatalogInfo(&ref, kFSCatInfoFinderInfo, &catalog_info, NULL, NULL, NULL); + if (err == noErr) { + FileInfo *file_info = (FileInfo *)(&catalog_info.finderInfo); + if ((file_info->finderFlags & kHasCustomIcon) != 0) Py_RETURN_TRUE; + } + } +#pragma clang diagnostic pop + Py_RETURN_FALSE; + + } // 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 @@ -923,6 +1015,9 @@ static PyMethodDef module_methods[] = { {"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_app_has_custom_icon", (PyCFunction)cocoa_app_has_custom_icon, 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 */ }; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 63dabfa96..6b00983c4 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -730,7 +730,19 @@ def cocoa_get_lang() -> Optional[str]: pass -def cocoa_set_url_handler(url_scheme: str, bundle_id: Optional[str]) -> None: +def cocoa_set_url_handler(url_scheme: str, bundle_id: Optional[str] = None) -> None: + pass + + +def cocoa_app_has_custom_icon(app_path: Optional[str] = None) -> bool: + pass + + +def cocoa_set_app_icon(icon_path: str, app_path: Optional[str] = None) -> None: + pass + + +def cocoa_set_dock_icon(icon_path: str) -> None: pass diff --git a/kitty/main.py b/kitty/main.py index f078caa6c..b29968760 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -135,6 +135,20 @@ def get_macos_shortcut_for( return ans +def set_macos_app_custom_icon() -> None: + for name in ('kitty.app.icns', 'kitty.app.png'): + icon_path = os.path.join(config_dir, name) + if os.path.exists(icon_path): + from .fast_data_types import cocoa_app_has_custom_icon, cocoa_set_app_icon, cocoa_set_dock_icon + if not cocoa_app_has_custom_icon(): + cocoa_set_app_icon(icon_path) + # kitty dock icon doesn't refresh automatically, so set it explicitly + # This has the drawback that the dock icon reverts to the original icon after exiting the application, + # even if the custom icon has been successfully updated, until the next launch. + cocoa_set_dock_icon(icon_path) + break + + def set_x11_window_icon() -> None: # max icon size on X11 64bits is 128x128 path, ext = os.path.splitext(logo_png_file) @@ -167,8 +181,11 @@ def _run_app(opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines val = get_macos_shortcut_for(func_map, f'open_url {website_url()}', lookup_name='open_kitty_website') if val is not None: global_shortcuts['open_kitty_website'] = val - if is_macos and opts.macos_custom_beam_cursor: - set_custom_ibeam_cursor() + + if opts.macos_custom_beam_cursor: + set_custom_ibeam_cursor() + set_macos_app_custom_icon() + if not is_wayland() and not is_macos: # no window icons on wayland set_x11_window_icon() with cached_values_for(run_app.cached_values_name) as cached_values: