From 2c743dcdb2d044a13e3a4b86536c60b53ef95027 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 4 Aug 2024 20:19:39 +0530 Subject: [PATCH] Update sound support in desktop notifications spec Add a short list of standard sound names. --- docs/desktop-notifications.rst | 34 ++++++++++++++++++++++------------ kitty/cocoa_window.m | 9 +++++++++ kitty/constants.py | 10 ++++++++++ kitty/fast_data_types.pyi | 1 + kitty/notifications.py | 14 +++++++++----- kitty_tests/notifications.py | 2 +- setup.py | 1 + 7 files changed, 53 insertions(+), 18 deletions(-) diff --git a/docs/desktop-notifications.rst b/docs/desktop-notifications.rst index 8503cd988..8ee06a0d1 100644 --- a/docs/desktop-notifications.rst +++ b/docs/desktop-notifications.rst @@ -343,13 +343,25 @@ By default, notifications may or may not have a sound associated with them depending on the policies of the OS notifications service. Sometimes it might be useful to ensure a notification is not accompanied by a sound. This can be done by using the ``s`` key which accepts :ref:`base64` encoded -UTF-8 text as its value. Using a value of ``silent`` means the notification -will not be accompanied with a sound. A value of ``system`` (the default) -means that the OS notifications default policies are followed. Any other name -is implementation dependent. For example, on Linux, one can use the `standard -sound names -`__. -Support for sounds can be queried as described below. +UTF-8 text as its value. The set of known sounds names is in the table below, +any other names are implementation dependent, for instance, on Linux, terminal emulators will +probably support the `standard sound names +`__ + +.. table:: Standard sound names + + ======================== ============================================== + Name Description + ======================== ============================================== + ``system`` The default system sound for a notification, which may be some kind of beep or just silence + ``silent`` No sound must accompany the notification + ``error`` A sound associated with error messages + ``warn``, ``warning`` A sound associated with warning messages + ``info`` A sound associated with information messages + ``question`` A sound associated with questions + ======================== ============================================== + +Support for sound names can be queried as described below. Querying for support @@ -391,11 +403,9 @@ Key Value ``p`` key that the terminal implements). These must contain at least ``title``. -``s`` Comma separated list of keywords ``silent`` and ``xdg-names`` indicating - support for silent notifications and for passing of `Freedesktop - standard sound names - `__ to the - desktop notification service for custom sounds. +``s`` Comma separated list of sound names from the table of standard sound names above. + Terminals will report the list of standard sound names they support. + Terminals *should* support atleast ``system`` and ``silent``. ``u`` Comma separated list of urgency values that the terminal implements. If urgency is not supported, the ``u`` key must be absent from the diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index 7d8d9ed9b..e0286df06 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -13,6 +13,7 @@ #include #include #include +#import #include // Needed for _NSGetProgname @@ -1185,7 +1186,15 @@ - (BOOL)openFileURLs:(NSPasteboard*)pasteboard return convert_image_to_png(icon, image_size, output_path); }} +static PyObject* +play_system_sound_by_id_async(PyObject *self UNUSED, PyObject *which) { + if (!PyLong_Check(which)) { PyErr_SetString(PyExc_TypeError, "system sound id must be an integer"); return NULL; } + AudioServicesPlaySystemSound(PyLong_AsUnsignedLong(which)); + Py_RETURN_NONE; +} + static PyMethodDef module_methods[] = { + {"cocoa_play_system_sound_by_id_async", play_system_sound_by_id_async, METH_O, ""}, {"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""}, {"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""}, {"cocoa_send_notification", (PyCFunction)(void(*)(void))cocoa_send_notification, METH_VARARGS | METH_KEYWORDS, ""}, diff --git a/kitty/constants.py b/kitty/constants.py index 4971d5a19..1cebea914 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -204,6 +204,16 @@ def wakeup_io_loop() -> None: 'text-editor': ('utilities-text-editor', '📄'), } +# See https://github.com/TUNER88/iOSSystemSoundsLibrary for Apple's system +# sound ids not all of which are available on macOS. +standard_sound_names = { + 'error': ('dialog-error', 1), + 'info': ('dialog-information', 2), + 'warning': ('dialog-warning', 3), + 'warn': ('dialog-warning', 3), + 'question': ('dialog-question', 4), +} + def glfw_path(module: str) -> str: prefix = 'kitty.' if getattr(sys, 'frozen', False) else '' diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index cc5591440..c2f191730 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1702,6 +1702,7 @@ def timed_debug_print(x: str) -> None: ... def opengl_version_string() -> str: ... def systemd_move_pid_into_new_scope(pid: int, scope_name: str, description: str) -> str: ... def play_desktop_sound_async(name: str, event_id: str = 'test sound', is_path: bool = False, theme_name: str = '') -> str: ... +def cocoa_play_system_sound_by_id_async(sound_id: int) -> None: ... class MousePosition(TypedDict): cell_x: int diff --git a/kitty/notifications.py b/kitty/notifications.py index 80567bc3a..9f4d56641 100644 --- a/kitty/notifications.py +++ b/kitty/notifications.py @@ -12,7 +12,7 @@ from typing import Any, Callable, NamedTuple, Optional, Set, Union from weakref import ReferenceType, ref -from .constants import cache_dir, config_dir, is_macos, logo_png_file, standard_icon_names +from .constants import cache_dir, config_dir, is_macos, logo_png_file, standard_icon_names, standard_sound_names from .fast_data_types import ESC_OSC, StreamingBase64Decoder, add_timer, base64_decode, current_focused_os_window_id, get_boss, get_options from .types import run_once from .typing import WindowType @@ -486,7 +486,7 @@ def query_response(self, identifier: str) -> str: i = f'i={identifier or "0"}:' p = ','.join(x.value for x in PayloadType if x.value and self.payload_type_supported(x)) c = ':c=1' if self.supports_close_events else '' - s = 'silent' + (f',{self.supports_sound_names}' if self.supports_sound_names else '') + s = 'system,silent,' + ','.join(sorted(standard_sound_names)) return f'99;{i}p=?;a={actions}:o={when}:u={urgency}:p={p}{c}:w=1:s={s}' @@ -596,7 +596,7 @@ def notify(self, nc: NotificationCommand, existing_desktop_notification_id: Opti cocoa_send_notification( nc.application_name or 'kitty', str(desktop_notification_id), nc.title, body, category=category, categories=categories, image_path=image_path, urgency=nc.urgency.value, - muted=nc.sound_name == 'silent', + muted=nc.sound_name == 'silent' or nc.sound_name in standard_sound_names, ) return desktop_notification_id @@ -616,9 +616,12 @@ def notification_activated(self, event: str, ident: str, button_id: str) -> None log_error(f'Got unexpected notification activated event with id: {ident!r} from cocoa') return if event == 'created': - self.notification_manager.notification_created(desktop_notification_id) + n = self.notification_manager.notification_created(desktop_notification_id) from .fast_data_types import cocoa_live_delivered_notifications cocoa_live_delivered_notifications() # so that we purge dead notifications + if n and n.sound_name in standard_sound_names: + from .fast_data_types import cocoa_play_system_sound_by_id_async + cocoa_play_system_sound_by_id_async(standard_sound_names[n.sound_name][1]) elif event == 'activated': self.notification_manager.notification_activated(desktop_notification_id, 0) elif event == 'creation_failed': @@ -690,8 +693,9 @@ def created(self, dbus_notification_id: int, desktop_notification_id: int) -> No # supports named sounds or not so we play the named sound # ourselves and tell the server to mute any sound it might play. if n.sound_name not in ('system', 'silent'): + sn = standard_sound_names[n.sound_name][0] if n.sound_name in standard_sound_names else n.sound_name from .fast_data_types import play_desktop_sound_async - play_desktop_sound_async(n.sound_name, event_id='desktop notification') + play_desktop_sound_async(sn, event_id='desktop notification') def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int, extra: Union[int, str]) -> None: if event_type == 'capabilities': diff --git a/kitty_tests/notifications.py b/kitty_tests/notifications.py index 4fd495ada..600a7432b 100644 --- a/kitty_tests/notifications.py +++ b/kitty_tests/notifications.py @@ -249,7 +249,7 @@ def enc(x): # Test querying h('i=xyz:p=?') self.assertFalse(di.notifications) - qr = 'a=focus,report:o=always,unfocused,invisible:u=0,1,2:p=title,body,?,close,icon,alive,buttons:c=1:w=1:s=silent,xdg-names' + qr = 'a=focus,report:o=always,unfocused,invisible:u=0,1,2:p=title,body,?,close,icon,alive,buttons:c=1:w=1:s=system,silent,error,info,question,warn,warning' self.ae(ch.responses, [f'99;i=xyz:p=?;{qr}']) reset() h('p=?') diff --git a/setup.py b/setup.py index c1982189c..3a48fb233 100755 --- a/setup.py +++ b/setup.py @@ -618,6 +618,7 @@ def kitty_env(args: Options) -> Env: if is_macos: platform_libs = [ '-framework', 'Carbon', '-framework', 'CoreText', '-framework', 'CoreGraphics', + '-framework', 'AudioToolbox', ] test_program_src = '''#include int main(void) { return 0; }\n'''