diff --git a/docs/changelog.rst b/docs/changelog.rst index 02dcfc2c509..45c00af11bd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -80,6 +80,8 @@ Detailed list of changes - macOS: Fix a regression in the previous release that caused kitten @ ls to not report the environment variables for the default shell (:iss:`6749`) +- desktop notification protocol: Allow applications sending notifications to specify that the notification should only be displayed if the window is currently unfocused (:iss:`6755`) + 0.30.1 [2023-10-05] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/desktop-notifications.rst b/docs/desktop-notifications.rst index 646256b43a8..e3bbe3a4b3d 100644 --- a/docs/desktop-notifications.rst +++ b/docs/desktop-notifications.rst @@ -94,9 +94,9 @@ to display it based on what it does understand. revisions. -======= ==================== ========= ================= +======= ==================== ========== ================= Key Value Default Description -======= ==================== ========= ================= +======= ==================== ========== ================= ``a`` Comma separated list ``focus`` What action to perform when the of ``report``, notification is clicked ``focus``, with @@ -113,7 +113,14 @@ Key Value Default Description ``p`` One of ``title`` or ``title`` Whether the payload is the notification title or body. If a ``body``. notification has no title, the body will be used as title. -======= ==================== ========= ================= + +``o`` One of ``always``, ``always`` When to honor the notification request. ``unfocused`` means when the window + ``unfocused`` or the notification is sent on does not have keyboard focus. ``invisible`` + ``invisible`` means the window both is unfocused + and not visible to the user, for example, because it is in an inactive tab or + its OS window is not currently active. + ``always`` is the default and always honors the request. +======= ==================== ========== ================= .. note:: diff --git a/kitty/notify.py b/kitty/notify.py index b21067839c4..3ab233674fa 100644 --- a/kitty/notify.py +++ b/kitty/notify.py @@ -4,11 +4,13 @@ import re from base64 import standard_b64decode from collections import OrderedDict +from contextlib import suppress +from enum import Enum from itertools import count from typing import Callable, Dict, Optional from .constants import is_macos, logo_png_file -from .fast_data_types import get_boss +from .fast_data_types import current_focused_os_window_id, get_boss from .types import run_once from .utils import get_custom_window_icon, log_error @@ -67,6 +69,12 @@ def notify_implementation(title: str, body: str, identifier: str) -> None: notify(title, body, identifier=identifier) +class OnlyWhen(Enum): + always = 'always' + unfocused = 'unfocused' + invisible = 'invisible' + + class NotificationCommand: done: bool = True @@ -74,6 +82,7 @@ class NotificationCommand: title: str = '' body: str = '' actions: str = '' + only_when: OnlyWhen = OnlyWhen.always def __repr__(self) -> str: return f'NotificationCommand(identifier={self.identifier!r}, title={self.title!r}, body={self.body!r}, actions={self.actions!r}, done={self.done!r})' @@ -125,6 +134,9 @@ def parse_osc_99(raw: str) -> NotificationCommand: cmd.done = v != '0' elif k == 'a': cmd.actions += f',{v}' + elif k == 'o': + with suppress(ValueError): + cmd.only_when = OnlyWhen(v) if payload_type not in ('body', 'title'): log_error(f'Malformed OSC 99: unknown payload type: {payload_type}') return NotificationCommand() @@ -208,6 +220,19 @@ def reset_registry() -> None: def notify_with_command(cmd: NotificationCommand, window_id: int, notify_implementation: NotifyImplementation = notify_implementation) -> None: title = cmd.title or cmd.body body = cmd.body if cmd.title else '' + if not title: + return + if cmd.only_when is not OnlyWhen.always: + w = get_boss().window_id_map.get(window_id) + if w is None: + return + boss = get_boss() + window_has_keyboard_focus = w.is_active and w.os_window_id == current_focused_os_window_id() + if window_has_keyboard_focus: + return + if cmd.only_when is OnlyWhen.invisible: + if w.os_window_id == current_focused_os_window_id() and w.tabref() is boss.active_tab and w.is_visible_in_layout: + return # window is in the active OS window and the active tab and is visible in the tab layout if title: identifier = f'i{next(id_counter)}' notify_implementation(title, body, identifier)