From 5d2e258c35e79453025f483d43a6cbed1296062a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 16 Apr 2025 15:48:51 +0530 Subject: [PATCH] Wayland: When the compositor supports the xdg-system-bell protocol use it to play the default bell sound --- docs/changelog.rst | 4 ++++ glfw/glfw.py | 1 + glfw/source-info.json | 1 + glfw/wl_init.c | 7 ++++++- glfw/wl_platform.h | 2 ++ glfw/wl_window.c | 8 ++++++++ kitty/boss.py | 4 ++-- kitty/fast_data_types.pyi | 2 +- kitty/glfw-wrapper.c | 3 +++ kitty/glfw-wrapper.h | 4 ++++ kitty/glfw.c | 18 ++++++++++++------ kitty/options/definition.py | 3 ++- kitty/tabs.py | 4 ++-- 13 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 84b0c712f..accdaf9d5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -103,6 +103,10 @@ Detailed list of changes - Fix a regression that caused automatic color themes to not be re-applied after config file reload (:iss:`8530`) +- Wayland: When the compositor supports the `xdg-system-bell + `__ protocol use it to play + the default bell sound + 0.41.1 [2025-04-03] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/glfw/glfw.py b/glfw/glfw.py index 88749bb6e..d0585b5d0 100755 --- a/glfw/glfw.py +++ b/glfw/glfw.py @@ -324,6 +324,7 @@ def generate_wrappers(glfw_header: str) -> None: bool glfwWaylandSetTitlebarColor(GLFWwindow *handle, uint32_t color, bool use_system_color) void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle) bool glfwWaylandIsWindowFullyCreated(GLFWwindow *handle) + bool glfwWaylandBeep(GLFWwindow *handle) void glfwWaylandSetupLayerShellForNextWindow(const GLFWLayerShellConfig *c) pid_t glfwWaylandCompositorPID(void) unsigned long long glfwDBusUserNotify(const GLFWDBUSNotificationData *n, GLFWDBusnotificationcreatedfun callback, void *data) diff --git a/glfw/source-info.json b/glfw/source-info.json index 6ccc48970..876a04e9f 100644 --- a/glfw/source-info.json +++ b/glfw/source-info.json @@ -85,6 +85,7 @@ "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", + "staging/xdg-system-bell/xdg-system-bell-v1.xml", "kwin-blur-v1.xml", "wlr-layer-shell-unstable-v1.xml" diff --git a/glfw/wl_init.c b/glfw/wl_init.c index bc0f5f410..5bbca1d7f 100644 --- a/glfw/wl_init.c +++ b/glfw/wl_init.c @@ -603,6 +603,9 @@ static void registryHandleGlobal(void* data UNUSED, 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); } + else if (is(xdg_system_bell_v1)) { + _glfw.wl.xdg_system_bell_v1 = wl_registry_bind(registry, name, &xdg_system_bell_v1_interface, 1); + } #undef is } @@ -712,7 +715,7 @@ get_compositor_missing_capabilities(void) { 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); C(icon, xdg_toplevel_icon_manager_v1); + C(idle_inhibit, idle_inhibit_manager); C(icon, xdg_toplevel_icon_manager_v1); C(bell, xdg_system_bell_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 @@ -889,6 +892,8 @@ void _glfwPlatformTerminate(void) 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.xdg_system_bell_v1) + xdg_system_bell_v1_destroy(_glfw.wl.xdg_system_bell_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) diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index a54560dcb..ffdcf3c51 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -67,6 +67,7 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR #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" +#include "wayland-xdg-system-bell-v1-client-protocol.h" #define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL) #define _glfw_dlclose(handle) dlclose(handle) @@ -338,6 +339,7 @@ typedef struct _GLFWlibraryWayland 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 xdg_system_bell_v1* xdg_system_bell_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; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 6847df273..76488f8b8 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -2835,3 +2835,11 @@ _glfwPlatformInputColorScheme(GLFWColorScheme appearance UNUSED) { window = window->next; } } + +GLFWAPI bool glfwWaylandBeep(GLFWwindow *handle) { + if (!_glfw.wl.xdg_system_bell_v1) return false; + _GLFWwindow *window = (_GLFWwindow*)handle; + xdg_system_bell_v1_ring(_glfw.wl.xdg_system_bell_v1, window ? window->wl.surface : NULL); + return true; +} + diff --git a/kitty/boss.py b/kitty/boss.py index 67fce663c..591379007 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -1501,7 +1501,7 @@ class Boss: else: self.visual_window_select_action_trigger(self.current_visual_select.window_ids[0] if self.current_visual_select.window_ids else 0) if get_options().enable_audio_bell: - ring_bell() + ring_bell(tab.os_window_id) def visual_window_select_action_trigger(self, window_id: int = 0) -> None: if self.current_visual_select: @@ -1545,7 +1545,7 @@ class Boss: if len(selectable_windows) < 2: self.visual_window_select_action_trigger(selectable_windows[0][0] if selectable_windows else 0) if get_options().enable_audio_bell: - ring_bell() + ring_bell(tab.os_window_id) return None cvs = self.current_visual_select diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 57a28f4dc..0144d3a74 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1012,7 +1012,7 @@ def set_active_window(os_window_id: int, tab_id: int, window_id: int) -> None: pass -def ring_bell() -> None: +def ring_bell(os_window_id: int = 0) -> None: pass diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index e12b1d437..b9d3644f2 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -488,6 +488,9 @@ load_glfw(const char* path) { *(void **) (&glfwWaylandIsWindowFullyCreated_impl) = dlsym(handle, "glfwWaylandIsWindowFullyCreated"); if (glfwWaylandIsWindowFullyCreated_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwWaylandBeep_impl) = dlsym(handle, "glfwWaylandBeep"); + if (glfwWaylandBeep_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwWaylandSetupLayerShellForNextWindow_impl) = dlsym(handle, "glfwWaylandSetupLayerShellForNextWindow"); if (glfwWaylandSetupLayerShellForNextWindow_impl == NULL) dlerror(); // clear error indicator diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 9be267f8b..2e1832c14 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -2344,6 +2344,10 @@ typedef bool (*glfwWaylandIsWindowFullyCreated_func)(GLFWwindow*); GFW_EXTERN glfwWaylandIsWindowFullyCreated_func glfwWaylandIsWindowFullyCreated_impl; #define glfwWaylandIsWindowFullyCreated glfwWaylandIsWindowFullyCreated_impl +typedef bool (*glfwWaylandBeep_func)(GLFWwindow*); +GFW_EXTERN glfwWaylandBeep_func glfwWaylandBeep_impl; +#define glfwWaylandBeep glfwWaylandBeep_impl + typedef void (*glfwWaylandSetupLayerShellForNextWindow_func)(const GLFWLayerShellConfig*); GFW_EXTERN glfwWaylandSetupLayerShellForNextWindow_func glfwWaylandSetupLayerShellForNextWindow_impl; #define glfwWaylandSetupLayerShellForNextWindow glfwWaylandSetupLayerShellForNextWindow_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index 141a81114..073b3f492 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1814,7 +1814,7 @@ cocoa_hide_other_apps(PYNOARG) { } static void -ring_audio_bell(void) { +ring_audio_bell(OSWindow *w) { static monotonic_t last_bell_at = -1; monotonic_t now = monotonic(); if (last_bell_at >= 0 && now - last_bell_at <= ms_to_monotonic_t(100ll)) return; @@ -1823,13 +1823,19 @@ ring_audio_bell(void) { cocoa_system_beep(OPT(bell_path)); #else if (OPT(bell_path)) play_canberra_sound(OPT(bell_path), "kitty bell", true, "event", OPT(bell_theme)); - else play_canberra_sound("bell", "kitty bell", false, "event", OPT(bell_theme)); + else { + if (!global_state.is_wayland || !glfwWaylandBeep(w ? w->handle : NULL)) play_canberra_sound( + "bell", "kitty bell", false, "event", OPT(bell_theme)); + } #endif } static PyObject* -ring_bell(PYNOARG) { - ring_audio_bell(); +ring_bell(PyObject *self UNUSED, PyObject *args) { + unsigned long long os_window_id = 0; + if (!PyArg_ParseTuple(args, "|K", &os_window_id)) return NULL; + OSWindow *w = os_window_for_id(os_window_id); + ring_audio_bell(w); Py_RETURN_NONE; } @@ -1918,7 +1924,7 @@ void request_window_attention(id_type kitty_window_id, bool audio_bell) { OSWindow *w = os_window_for_kitty_window(kitty_window_id); if (w) { - if (audio_bell) ring_audio_bell(); + if (audio_bell) ring_audio_bell(w); if (OPT(window_alert_on_bell)) glfwRequestWindowAttention(w->handle); glfwPostEmptyEvent(); } @@ -2408,7 +2414,7 @@ static PyMethodDef module_methods[] = { METHODB(get_clipboard_mime, METH_VARARGS), METHODB(toggle_secure_input, METH_NOARGS), METHODB(get_content_scale_for_window, METH_NOARGS), - METHODB(ring_bell, METH_NOARGS), + METHODB(ring_bell, METH_VARARGS), METHODB(toggle_fullscreen, METH_VARARGS), METHODB(toggle_maximized, METH_VARARGS), METHODB(change_os_window_state, METH_VARARGS), diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 8e6753a25..cb1482530 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1039,7 +1039,8 @@ MP3 or WAV on macOS (NSSound). opt('linux_bell_theme', '__custom', ctype='!bell_theme', long_text=''' The XDG Sound Theme kitty will use to play the bell sound. -Defaults to the custom theme name specified in the +On Wayland, when the compositor supports it, it is asked to play the system default +bell sound. Otherwise, defaults to the custom theme name specified in the :link:`XDG Sound theme specification , falling back to the default freedesktop theme if it does not exist. To change your sound theme desktop wide, create :file:`~/.local/share/sounds/__custom/index.theme` with the contents: diff --git a/kitty/tabs.py b/kitty/tabs.py index 0b04f385e..413f83174 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -451,7 +451,7 @@ class Tab: # {{{ if w is not None and self.resize_window_by( w.id, increment, is_horizontal) is not None: if get_options().enable_audio_bell: - ring_bell() + ring_bell(self.os_window_id) @ac('win', 'Reset window sizes undoing any dynamic resizing of windows') def reset_window_sizes(self) -> None: @@ -463,7 +463,7 @@ class Tab: # {{{ ret = self.current_layout.layout_action(action_name, args, self.windows) if ret is None: if get_options().enable_audio_bell: - ring_bell() + ring_bell(self.os_window_id) return self.relayout()