mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-20 20:22:10 +00:00
Wayland: Allow overriding the kitty OS Window icon on compositors that implement the xdg-toplevel-icon protocol
This commit is contained in:
parent
ce6425e1d4
commit
3b8e0e209b
10 changed files with 134 additions and 39 deletions
|
|
@ -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]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -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
4
glfw/glfw3.h
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
18
glfw/wl_init.c
vendored
|
|
@ -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
2
glfw/wl_platform.h
vendored
|
|
@ -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
82
glfw/wl_window.c
vendored
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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], ...],
|
||||
|
|
|
|||
40
kitty/glfw.c
40
kitty/glfw.c
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue