Wayland: Allow overriding the kitty OS Window icon on compositors that implement the xdg-toplevel-icon protocol

This commit is contained in:
Kovid Goyal 2025-03-10 12:32:03 +05:30
parent ce6425e1d4
commit 3b8e0e209b
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
10 changed files with 134 additions and 39 deletions

View file

@ -102,6 +102,8 @@ Detailed list of changes
- Fix flickering of hyperlink underline when client program continuously
redraws on mouse movement (:iss:`8414`)
- Wayland: Allow overriding the kitty OS Window icon on compositors that implement the xdg-toplevel-icon protocol
0.40.0 [2025-03-08]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -375,9 +375,11 @@ many alternate icons available, click on an icon to visit its homepage:
:target: https://github.com/sfsam/some_icons
:width: 256
On macOS and X11 you can put :file:`kitty.app.icns` (macOS only) or :file:`kitty.app.png` in the
You can put :file:`kitty.app.icns` (macOS only) or :file:`kitty.app.png` in the
:ref:`kitty configuration directory <confloc>`, and this icon will be applied
automatically at startup. On X11, this will set the icon for kitty windows.
automatically at startup. On X11 and Wayland, this will set the icon for kitty windows.
Note that not all Wayland compositors support the `protocol needed <https://wayland.app/protocols/xdg-toplevel-icon-v1>`__
for changing window icons.
Unfortunately, on macOS, Apple's Dock does not change its cached icon so the
custom icon will revert when kitty is quit. Run the following to force the Dock

4
glfw/glfw3.h vendored
View file

@ -3000,10 +3000,6 @@ GLFWAPI void glfwSetWindowTitle(GLFWwindow* window, const char* title);
* [Bundle Programming Guide](https://developer.apple.com/library/mac/documentation/CoreFoundation/Conceptual/CFBundles/)
* in the Mac Developer Library.
*
* @remark @wayland There is no existing protocol to change an icon, the
* window will thus inherit the one defined in the application's desktop file.
* This function will emit @ref GLFW_FEATURE_UNAVAILABLE.
*
* @thread_safety This function must only be called from the main thread.
*
* @sa @ref window_icon

View file

@ -84,6 +84,7 @@
"staging/fractional-scale/fractional-scale-v1.xml",
"staging/single-pixel-buffer/single-pixel-buffer-v1.xml",
"unstable/idle-inhibit/idle-inhibit-unstable-v1.xml",
"staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml",
"kwin-blur-v1.xml",
"wlr-layer-shell-unstable-v1.xml"

18
glfw/wl_init.c vendored
View file

@ -489,8 +489,7 @@ static void registryHandleGlobal(void* data UNUSED,
}
else if (is(wl_shm))
{
_glfw.wl.shm =
wl_registry_bind(registry, name, &wl_shm_interface, 1);
_glfw.wl.shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
}
else if (is(wl_output))
{
@ -601,6 +600,9 @@ static void registryHandleGlobal(void* data UNUSED,
else if (is(zwp_idle_inhibit_manager_v1)) {
_glfw.wl.idle_inhibit_manager = wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1);
}
else if (is(xdg_toplevel_icon_manager_v1)) {
_glfw.wl.xdg_toplevel_icon_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_icon_manager_v1_interface, 1);
}
#undef is
}
@ -702,17 +704,17 @@ _glfwWaylandCompositorName(void) {
static const char*
get_compositor_missing_capabilities(void) {
#define C(title, x) if (!_glfw.wl.x) p += snprintf(buf, sizeof(buf) - (p - buf), "%s ", #title);
static char buf[256];
#define C(title, x) if (!_glfw.wl.x) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", #title);
static char buf[512];
char *p = buf;
*p = 0;
C(viewporter, wp_viewporter); C(fractional_scale, wp_fractional_scale_manager_v1);
C(blur, org_kde_kwin_blur_manager); C(server_side_decorations, decorationManager);
C(cursor_shape, wp_cursor_shape_manager_v1); C(layer_shell, zwlr_layer_shell_v1);
C(single_pixel_buffer, wp_single_pixel_buffer_manager_v1); C(preferred_scale, has_preferred_buffer_scale);
C(idle_inhibit, idle_inhibit_manager);
if (_glfw.wl.xdg_wm_base_version < 6) p += snprintf(buf, sizeof(buf) - (p - buf), "%s ", "window-state-suspended");
if (_glfw.wl.xdg_wm_base_version < 5) p += snprintf(buf, sizeof(buf) - (p - buf), "%s ", "window-capabilities");
C(idle_inhibit, idle_inhibit_manager); C(icon, xdg_toplevel_icon_manager_v1);
if (_glfw.wl.xdg_wm_base_version < 6) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-state-suspended");
if (_glfw.wl.xdg_wm_base_version < 5) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-capabilities");
#undef C
while (p > buf && (p - 1)[0] == ' ') { p--; *p = 0; }
return buf;
@ -885,6 +887,8 @@ void _glfwPlatformTerminate(void)
zwp_primary_selection_device_manager_v1_destroy(_glfw.wl.primarySelectionDeviceManager);
if (_glfw.wl.xdg_activation_v1)
xdg_activation_v1_destroy(_glfw.wl.xdg_activation_v1);
if (_glfw.wl.xdg_toplevel_icon_manager_v1)
xdg_toplevel_icon_manager_v1_destroy(_glfw.wl.xdg_toplevel_icon_manager_v1);
if (_glfw.wl.wp_single_pixel_buffer_manager_v1)
wp_single_pixel_buffer_manager_v1_destroy(_glfw.wl.wp_single_pixel_buffer_manager_v1);
if (_glfw.wl.wp_cursor_shape_manager_v1)

2
glfw/wl_platform.h vendored
View file

@ -66,6 +66,7 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR
#include "wayland-wlr-layer-shell-unstable-v1-client-protocol.h"
#include "wayland-single-pixel-buffer-v1-client-protocol.h"
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
#include "wayland-xdg-toplevel-icon-v1-client-protocol.h"
#define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL)
#define _glfw_dlclose(handle) dlclose(handle)
@ -332,6 +333,7 @@ typedef struct _GLFWlibraryWayland
struct zwp_primary_selection_device_v1* primarySelectionDevice;
struct zwp_primary_selection_source_v1* dataSourceForPrimarySelection;
struct xdg_activation_v1* xdg_activation_v1;
struct xdg_toplevel_icon_manager_v1* xdg_toplevel_icon_manager_v1;
struct wp_cursor_shape_manager_v1* wp_cursor_shape_manager_v1;
struct wp_cursor_shape_device_v1* wp_cursor_shape_device_v1;
struct wp_fractional_scale_manager_v1 *wp_fractional_scale_manager_v1;

82
glfw/wl_window.c vendored
View file

@ -101,6 +101,19 @@ get_activation_token(
#undef fail
}
static void
convert_glfw_image_to_wayland_image(const GLFWimage* image, unsigned char *target) {
// convert RGBA non-premultiplied to ARGB pre-multiplied
unsigned char* source = (unsigned char*) image->pixels;
for (int i = 0; i < image->width * image->height; i++, source += 4) {
unsigned int alpha = source[3];
*target++ = (unsigned char) ((source[2] * alpha) / 255);
*target++ = (unsigned char) ((source[1] * alpha) / 255);
*target++ = (unsigned char) ((source[0] * alpha) / 255);
*target++ = (unsigned char) alpha;
}
}
static struct wl_buffer* createShmBuffer(const GLFWimage* image, bool is_opaque, bool init_data)
{
struct wl_shm_pool* pool;
@ -108,7 +121,7 @@ static struct wl_buffer* createShmBuffer(const GLFWimage* image, bool is_opaque,
int stride = image->width * 4;
int length = image->width * image->height * 4;
void* data;
int fd, i;
int fd;
fd = createAnonymousFile(length);
if (fd < 0)
@ -131,19 +144,7 @@ static struct wl_buffer* createShmBuffer(const GLFWimage* image, bool is_opaque,
pool = wl_shm_create_pool(_glfw.wl.shm, fd, length);
close(fd);
if (init_data) {
unsigned char* source = (unsigned char*) image->pixels;
unsigned char* target = data;
for (i = 0; i < image->width * image->height; i++, source += 4)
{
unsigned int alpha = source[3];
*target++ = (unsigned char) ((source[2] * alpha) / 255);
*target++ = (unsigned char) ((source[1] * alpha) / 255);
*target++ = (unsigned char) ((source[0] * alpha) / 255);
*target++ = (unsigned char) alpha;
}
}
if (init_data) convert_glfw_image_to_wayland_image(image, data);
buffer =
wl_shm_pool_create_buffer(pool, 0,
@ -1465,11 +1466,54 @@ void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title)
}
}
void _glfwPlatformSetWindowIcon(_GLFWwindow* window UNUSED,
int count UNUSED, const GLFWimage* images UNUSED)
{
_glfwInputError(GLFW_FEATURE_UNAVAILABLE,
"Wayland: The platform does not support setting the window icon");
void
_glfwPlatformSetWindowIcon(_GLFWwindow* window, int count, const GLFWimage* images) {
if (!_glfw.wl.xdg_toplevel_icon_manager_v1) {
static bool warned_once = false;
if (!warned_once) {
_glfwInputError(GLFW_FEATURE_UNAVAILABLE, "Wayland: The compositor does not support changing window icons");
warned_once = true;
}
}
if (!count) {
xdg_toplevel_icon_manager_v1_set_icon(_glfw.wl.xdg_toplevel_icon_manager_v1, window->wl.xdg.toplevel, NULL);
return;
}
struct wl_buffer* *buffers = malloc(sizeof(struct wl_buffer*) * count);
if (!buffers) return;
size_t total_data_size = 0;
for (int i = 0; i < count; i++) total_data_size += images[i].width * images[i].height * 4;
int fd = createAnonymousFile(total_data_size);
if (fd < 0) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Creating a buffer file for %ld B failed: %s", (long)total_data_size, strerror(errno));
free(buffers);
return;
}
unsigned char *data = mmap(NULL, total_data_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: mmap failed: %s", strerror(errno));
free(buffers);
close(fd);
return;
}
struct wl_shm_pool* pool = wl_shm_create_pool(_glfw.wl.shm, fd, total_data_size);
struct xdg_toplevel_icon_v1 *icon = xdg_toplevel_icon_manager_v1_create_icon(_glfw.wl.xdg_toplevel_icon_manager_v1);
size_t pos = 0;
for (int i = 0; i < count; i++) {
const size_t sz = images[i].width * images[i].height * 4;
convert_glfw_image_to_wayland_image(images + i, data + pos);
buffers[i] = wl_shm_pool_create_buffer(
pool, pos, images[i].width, images[i].height, images[i].width * 4, WL_SHM_FORMAT_ARGB8888);
xdg_toplevel_icon_v1_add_buffer(icon, buffers[i], 1);
pos += sz;
}
xdg_toplevel_icon_manager_v1_set_icon(_glfw.wl.xdg_toplevel_icon_manager_v1, window->wl.xdg.toplevel, icon);
xdg_toplevel_icon_v1_destroy(icon);
for (int i = 0; i < count; i++) wl_buffer_destroy(buffers[i]);
free(buffers);
wl_shm_pool_destroy(pool);
munmap(data, total_data_size);
close(fd);
}
void _glfwPlatformGetWindowPos(_GLFWwindow* window UNUSED, int* xpos UNUSED, int* ypos UNUSED)

View file

@ -654,6 +654,9 @@ def set_default_window_icon(path: str) -> None:
pass
def set_os_window_icon(os_window_id: int, path: str | None | bytes = None) -> None: ...
def set_custom_cursor(
cursor_shape: str,
images: Tuple[Tuple[bytes, int, int], ...],

View file

@ -767,6 +767,45 @@ set_default_window_icon(PyObject UNUSED *self, PyObject *args) {
Py_RETURN_NONE;
}
static PyObject*
set_os_window_icon(PyObject UNUSED *self, PyObject *args) {
size_t sz;
unsigned int width, height;
PyObject *what = NULL;
uint8_t *data;
unsigned long long id;
if(!PyArg_ParseTuple(args, "K|O", &id, &what)) return NULL;
OSWindow *os_window = os_window_for_id(id);
if (!os_window) { PyErr_Format(PyExc_KeyError, "No OS Window with id: %llu", id); return NULL; }
if (!what || what == Py_None) {
glfwSetWindowIcon(os_window->handle, 0, NULL);
Py_RETURN_NONE;
}
if (PyUnicode_Check(what)) {
const char *path = PyUnicode_AsUTF8(what);
if (png_path_to_bitmap(path, &data, &width, &height, &sz)) {
GLFWimage img = { .pixels = data, .width = width, .height = height };
glfwSetWindowIcon(os_window->handle, 1, &img);
free(data);
} else {
PyErr_Format(PyExc_ValueError, "%s is not a valid PNG image", path);
return NULL;
}
Py_RETURN_NONE;
}
RAII_PY_BUFFER(buf);
if(!PyArg_ParseTuple(args, "Ky*", &id, &buf)) return NULL;
if (png_from_data(buf.buf, buf.len, "<data>", &data, &width, &height, &sz)) {
GLFWimage img = { .pixels = data, .width = width, .height = height };
glfwSetWindowIcon(os_window->handle, 1, &img);
} else {
PyErr_Format(PyExc_ValueError, "The supplied data of %lu bytes is not a valid PNG image", (unsigned long)buf.len);
return NULL;
}
Py_RETURN_NONE;
}
void*
make_os_window_context_current(OSWindow *w) {
@ -2323,6 +2362,7 @@ static PyMethodDef module_methods[] = {
METHODB(pointer_name_to_css_name, METH_O),
{"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL},
METHODB(set_default_window_icon, METH_VARARGS),
METHODB(set_os_window_icon, METH_VARARGS),
METHODB(set_clipboard_data_types, METH_VARARGS),
METHODB(get_clipboard_mime, METH_VARARGS),
METHODB(toggle_secure_input, METH_NOARGS),

View file

@ -156,17 +156,19 @@ def get_icon128_path(base_path: str) -> str:
return f'{path}-128{ext}'
def set_x11_window_icon() -> None:
def set_window_icon() -> None:
custom_icon_path = get_custom_window_icon()[1]
is_x11 = not is_macos and not is_wayland()
try:
if custom_icon_path is not None:
custom_icon128_path = get_icon128_path(custom_icon_path)
if safe_mtime(custom_icon128_path) is None:
set_default_window_icon(custom_icon_path)
else:
if is_x11 and safe_mtime(custom_icon128_path) is not None:
set_default_window_icon(custom_icon128_path)
else:
set_default_window_icon(custom_icon_path)
else:
set_default_window_icon(get_icon128_path(logo_png_file))
if is_x11:
set_default_window_icon(get_icon128_path(logo_png_file))
except ValueError as err:
log_error(err)
@ -217,8 +219,7 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = (),
set_macos_app_custom_icon()
else:
global_shortcuts = {}
if not is_wayland(): # no window icons on wayland
set_x11_window_icon()
set_window_icon()
with cached_values_for(run_app.cached_values_name) as cached_values:
startup_sessions = tuple(create_sessions(opts, args, default_session=opts.startup_session))