From cab296c38bbbd3236e781e415f855ed6da931e93 Mon Sep 17 00:00:00 2001 From: KramLololo Date: Wed, 5 Aug 2026 16:16:20 +0300 Subject: [PATCH] protocols/bell: play a sound by default (#15502) --------- Co-authored-by: thompty --- .github/actions/setup_base/action.yml | 1 + CMakeLists.txt | 1 + nix/default.nix | 2 + src/config/lua/LuaEventHandler.cpp | 3 ++ src/config/values/ConfigValues.cpp | 1 + src/event/EventBus.hpp | 1 + src/helpers/BellSound.cpp | 72 +++++++++++++++++++++++++++ src/helpers/BellSound.hpp | 24 +++++++++ src/protocols/XDGBell.cpp | 55 ++++++-------------- 9 files changed, 121 insertions(+), 39 deletions(-) create mode 100644 src/helpers/BellSound.cpp create mode 100644 src/helpers/BellSound.hpp diff --git a/.github/actions/setup_base/action.yml b/.github/actions/setup_base/action.yml index af988f71e..df1eb7dc5 100644 --- a/.github/actions/setup_base/action.yml +++ b/.github/actions/setup_base/action.yml @@ -29,6 +29,7 @@ runs: hyprcursor \ jq \ libc++ \ + libcanberra \ libdisplay-info \ libdrm \ libei \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 3470d4307..f9fd90c84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -285,6 +285,7 @@ pkg_check_modules(deps re2 muparser lcms2 + libcanberra ) pkg_search_module(LUA REQUIRED IMPORTED_TARGET GLOBAL lua55 lua5.5 lua-55 lua-5.5 lua>=5.5 lua<5.6) diff --git a/nix/default.nix b/nix/default.nix index 7261d3b41..c372cce3c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -23,6 +23,7 @@ hyprwayland-scanner, hyprwire, lcms2, + libcanberra, libGL, libdrm, libei, @@ -190,6 +191,7 @@ customStdenv.mkDerivation (finalAttrs: { hyprutils hyprwire lcms2 + libcanberra libdrm libgbm libGL diff --git a/src/config/lua/LuaEventHandler.cpp b/src/config/lua/LuaEventHandler.cpp index 24ec7641c..9c701e6ae 100644 --- a/src/config/lua/LuaEventHandler.cpp +++ b/src/config/lua/LuaEventHandler.cpp @@ -114,6 +114,8 @@ CLuaEventHandler::CLuaEventHandler(lua_State* L) : m_lua(L) { CLuaWorkspace::push(L, ws); }); })); + m_listeners.push_back( + bus()->m_events.window.bell.listen([this](PHLWINDOW w, Event::SCallbackInfo&) { dispatch("window.bell", 1, [&](lua_State* L) { CLuaWindow::push(L, w); }); })); m_listeners.push_back(bus()->m_events.layer.opened.listen([this](PHLLS ls) { dispatch("layer.opened", 1, [&](lua_State* L) { CLuaLayerSurface::push(L, ls); }); })); m_listeners.push_back(bus()->m_events.layer.closed.listen([this](PHLLS ls) { dispatch("layer.closed", 1, [&](lua_State* L) { CLuaLayerSurface::push(L, ls); }); })); @@ -278,6 +280,7 @@ std::unordered_set CLuaEventHandler::knownEvents() { "window.fullscreen", "window.update_rules", "window.move_to_workspace", + "window.bell", "layer.opened", "layer.closed", "monitor.added", diff --git a/src/config/values/ConfigValues.cpp b/src/config/values/ConfigValues.cpp index 649dd22ac..318a66cc0 100644 --- a/src/config/values/ConfigValues.cpp +++ b/src/config/values/ConfigValues.cpp @@ -525,6 +525,7 @@ std::vector> Values::getConfigValues() { MS("misc:screencopy_force_8b", "forces 8 bit screencopy", true), MS("misc:disable_scale_notification", "disables notification popup when a monitor fails to set a suitable scale", false), MS("misc:size_limits_tiled", "whether to apply minsize and maxsize rules to tiled windows", false), + MS("misc:bell_sound", "path to custom wav/ogg system bell. `none` or an empty string mute it. `default` uses the system's current one.", "default"), MS("misc:new_float_force_onscreen", "whether new floating windows must be placed fully/partially on-screen", 2), MS("misc:float_force_onscreen", "whether existing floating windows must remain fully/partially on-screen", 0), diff --git a/src/event/EventBus.hpp b/src/event/EventBus.hpp index 3ac0428fc..8df38694e 100644 --- a/src/event/EventBus.hpp +++ b/src/event/EventBus.hpp @@ -90,6 +90,7 @@ namespace Event { Event floating; Event updateRules; Event moveToWorkspace; + Cancellable bell; } window; struct { diff --git a/src/helpers/BellSound.cpp b/src/helpers/BellSound.cpp new file mode 100644 index 000000000..189382f1c --- /dev/null +++ b/src/helpers/BellSound.cpp @@ -0,0 +1,72 @@ +#include "BellSound.hpp" +#include +#include "../config/ConfigValue.hpp" +#include "../config/ConfigManager.hpp" +#include "./MiscFunctions.hpp" +#include "../event/EventBus.hpp" +#include "../debug/log/Logger.hpp" + +CBellSound::CBellSound() { + onNewConfig(); + m_configListener = Event::bus()->m_events.config.reloaded.listen([this] { onNewConfig(); }); +} + +CBellSound::~CBellSound() { + if (m_context) { + ca_context_destroy(m_context); + ca_proplist_destroy(m_sound); + } +} + +void CBellSound::onNewConfig() { + const auto VALUE = *CConfigValue("misc:bell_sound"); + + if (VALUE == "default") { + m_muted = false; + initializeSoundContext(); + ca_proplist_sets(m_sound, CA_PROP_EVENT_ID, "bell-window-system"); + } else if (VALUE.empty() || VALUE == "none") + m_muted = true; + else { + m_muted = false; + initializeSoundContext(); + + const auto RESOLVEDPATH = absolutePath(VALUE, Config::mgr()->getMainConfigPath()); + + if (std::filesystem::exists(RESOLVEDPATH)) { + ca_proplist_sets(m_sound, CA_PROP_MEDIA_FILENAME, RESOLVEDPATH.c_str()); + ca_proplist_set(m_sound, CA_PROP_EVENT_ID, nullptr, 0); + return; + } + + ca_proplist_sets(m_sound, CA_PROP_EVENT_ID, "bell-window-system"); + Log::logger->log(Log::WARN, "bell: resolved custom sound path '{}' doesn't exist, falling back to default", RESOLVEDPATH); + } +} + +void CBellSound::initializeSoundContext() { + if (m_context) + return; + + int result = ca_context_create(&m_context) || ca_proplist_create(&m_sound); + if UNLIKELY (result != CA_SUCCESS) + Log::logger->log(Log::ERR, "bell: failed to create canberra context, '{}'", ca_strerror(result)); + + ca_context_change_props(m_context, CA_PROP_APPLICATION_NAME, "Hyprland", CA_PROP_MEDIA_NAME, "System Bell", CA_PROP_EVENT_DESCRIPTION, "Wayland system bell", + CA_PROP_MEDIA_ROLE, "event", CA_PROP_MEDIA_ICON_NAME, "preferences-system-notifications", CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", nullptr); +} + +void CBellSound::play() { + static CBellSound instance; + + if (instance.m_muted) + return; + + int result = ca_context_play_full(instance.m_context, 0, instance.m_sound, nullptr, nullptr); + if UNLIKELY (result != CA_SUCCESS) { + if (result == CA_ERROR_CORRUPT) + Log::logger->log(Log::WARN, "bell: sound is not a wav/ogg file"); + else + Log::logger->log(Log::WARN, "bell: failed to play sound, '{}'", ca_strerror(result)); + } +} diff --git a/src/helpers/BellSound.hpp b/src/helpers/BellSound.hpp new file mode 100644 index 000000000..ff4363a56 --- /dev/null +++ b/src/helpers/BellSound.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +struct ca_context; +struct ca_proplist; + +class CBellSound { + public: + static void play(); + + private: + CBellSound(); + ~CBellSound(); + + void onNewConfig(); + void initializeSoundContext(); + + bool m_muted = false; + ca_proplist* m_sound = nullptr; + ca_context* m_context = nullptr; + + Hyprutils::Signal::CHyprSignalListener m_configListener; +}; diff --git a/src/protocols/XDGBell.cpp b/src/protocols/XDGBell.cpp index 2d7e69e60..5749e732a 100644 --- a/src/protocols/XDGBell.cpp +++ b/src/protocols/XDGBell.cpp @@ -1,52 +1,29 @@ #include "XDGBell.hpp" -#include "core/Compositor.hpp" -#include "../desktop/view/Window.hpp" +#include "../helpers/BellSound.hpp" +#include "./core/Compositor.hpp" +#include "../desktop/state/ViewState.hpp" +#include "../desktop/state/ViewQuery.hpp" +#include "../event/EventBus.hpp" #include "../ipc/s2/S2.hpp" -#include "../Compositor.hpp" +#include CXDGSystemBellManagerResource::CXDGSystemBellManagerResource(UP&& resource) : m_resource(std::move(resource)) { if UNLIKELY (!good()) return; - m_resource->setDestroy([this](CXdgSystemBellV1* r) { PROTO::xdgBell->destroyResource(this); }); - m_resource->setOnDestroy([this](CXdgSystemBellV1* r) { PROTO::xdgBell->destroyResource(this); }); + m_resource->setDestroy([this](CXdgSystemBellV1*) { PROTO::xdgBell->destroyResource(this); }); + m_resource->setOnDestroy([this](CXdgSystemBellV1*) { PROTO::xdgBell->destroyResource(this); }); - m_resource->setRing([](CXdgSystemBellV1* r, wl_resource* surface) { - if (!surface) { - IPC::Socket2::sock()->postEvent({ - .event = "bell", - .data = "", - }); + m_resource->setRing([](CXdgSystemBellV1*, wl_resource* surface) { + const auto WINDOW = Desktop::viewState()->query().surface(CWLSurfaceResource::fromResource(surface)).type(Desktop::View::VIEW_TYPE_WINDOW).runWindow(); + + Event::SCallbackInfo info; + Event::bus()->m_events.window.bell.emit(WINDOW, info); + if (info.cancelled) return; - } - const auto SURFACE = CWLSurfaceResource::fromResource(surface); - - if (!SURFACE) { - IPC::Socket2::sock()->postEvent({ - .event = "bell", - .data = "", - }); - return; - } - - for (const auto& w : Desktop::windowState()->windows()) { - if (!w->m_isMapped || w->m_isX11 || !w->m_xdgSurface || !w->wlSurface()) - continue; - - if (w->wlSurface()->resource() == SURFACE) { - IPC::Socket2::sock()->postEvent({ - .event = "bell", - .data = std::format("{:x}", rc(w.get())), - }); - return; - } - } - - IPC::Socket2::sock()->postEvent({ - .event = "bell", - .data = "", - }); + IPC::Socket2::sock()->postEvent({.event = "bell", .data = WINDOW ? std::format("{:x}", rc(WINDOW.get())) : ""}); + CBellSound::play(); }); }