macOS: Allow to set custom app icon automatically

This commit is contained in:
pagedown 2022-09-25 15:25:43 +08:00
parent 98eacb2067
commit a1029418f8
No known key found for this signature in database
GPG Key ID: E921CF18AC8FF6EB
5 changed files with 147 additions and 4 deletions

View File

@ -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`) - 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] 0.26.3 [2022-09-22]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -287,7 +287,24 @@ homepage:
:target: https://github.com/samholmes/whiskers :target: https://github.com/samholmes/whiskers
:width: 256 :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 <confloc>`, 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` #. 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 #. Drag :file:`kitty.icns` onto the application icon in the kitty info pane

View File

@ -862,6 +862,98 @@ cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) {
} // autoreleasepool } // 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 NSSound *beep_sound = nil;
static void static void
@ -923,6 +1015,9 @@ static PyMethodDef module_methods[] = {
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, 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_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""}, {"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 */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };

View File

@ -730,7 +730,19 @@ def cocoa_get_lang() -> Optional[str]:
pass 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 pass

View File

@ -135,6 +135,20 @@ def get_macos_shortcut_for(
return ans 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: def set_x11_window_icon() -> None:
# max icon size on X11 64bits is 128x128 # max icon size on X11 64bits is 128x128
path, ext = os.path.splitext(logo_png_file) 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') val = get_macos_shortcut_for(func_map, f'open_url {website_url()}', lookup_name='open_kitty_website')
if val is not None: if val is not None:
global_shortcuts['open_kitty_website'] = val 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 if not is_wayland() and not is_macos: # no window icons on wayland
set_x11_window_icon() set_x11_window_icon()
with cached_values_for(run_app.cached_values_name) as cached_values: with cached_values_for(run_app.cached_values_name) as cached_values: