Allow well behaved applications to categorise notifications so that users can easily filter them out
This commit is contained in:
parent
45a3a655a7
commit
c59ab759a1
3 changed files with 72 additions and 52 deletions
|
|
@ -59,6 +59,18 @@ encoded, in which case there must be an ``e=1`` key in the metadata to indicate
|
|||
the payload is :rfc:`base64 <4648>` encoded. No HTML or other markup in the
|
||||
plain text is allowed. It is strictly plain text, to be interpreted as such.
|
||||
|
||||
Allowing users to filter notifications
|
||||
-------------------------------------------------------
|
||||
|
||||
Well behaved applications should identify themselves to the terminal
|
||||
by means of two keys ``f`` which is the application name and ``t``
|
||||
which is the notification type. These are free form keys, they can contain
|
||||
any values, their purpose is to allow users to easily filter out
|
||||
notifications they do not want. Both keys must have :rfc:`base64 <4648>`
|
||||
encoded UTF-8 text as their values. Terminals can then present UI to users
|
||||
to allow them to filter out notifications from applications they do not want.
|
||||
|
||||
|
||||
Being informed when user activates the notification
|
||||
-------------------------------------------------------
|
||||
|
||||
|
|
@ -234,6 +246,14 @@ Key Value Default Description
|
|||
If not specified normal is used.
|
||||
|
||||
``c`` ``0`` or ``1`` ``0`` When non-zero an escape code is sent to the application when the notification is closed.
|
||||
|
||||
``f`` :rfc:`base64 <4648>` ``unset`` The name of the application sending the notification. Can be used to filter out notifications.
|
||||
encoded UTF-8
|
||||
application name
|
||||
|
||||
``t`` :rfc:`base64 <4648>` ``unset`` The type of the notification. Can be used to filter out notifications.
|
||||
encoded UTF-8
|
||||
notification type
|
||||
======= ==================== ========== =================
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
from weakref import ReferenceType, ref
|
||||
|
||||
from .constants import cache_dir, is_macos, logo_png_file
|
||||
from .fast_data_types import ESC_OSC, StreamingBase64Decoder, current_focused_os_window_id, get_boss
|
||||
from .fast_data_types import ESC_OSC, StreamingBase64Decoder, base64_decode, current_focused_os_window_id, get_boss
|
||||
from .types import run_once
|
||||
from .typing import WindowType
|
||||
from .utils import get_custom_window_icon, log_error, sanitize_control_codes
|
||||
|
|
@ -203,6 +203,8 @@ class NotificationCommand:
|
|||
icon_data_key: str = ''
|
||||
icon_path: str = ''
|
||||
icon_name: str = ''
|
||||
application_name: str = ''
|
||||
notification_type: str = ''
|
||||
|
||||
# payload handling
|
||||
current_payload_type: PayloadType = PayloadType.title
|
||||
|
|
@ -276,6 +278,16 @@ def parse_metadata(self, metadata: str, prev: 'NotificationCommand') -> Tuple[Pa
|
|||
self.icon_data_key = sanitize_id(v)
|
||||
elif k == 'n':
|
||||
self.icon_name = v
|
||||
elif k == 'f':
|
||||
try:
|
||||
self.application_name = base64_decode(v).decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
self.log('Ignoring invalid application_name in notification: {v!r}')
|
||||
elif k == 't':
|
||||
try:
|
||||
self.notification_type = base64_decode(v).decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
self.log('Ignoring invalid notification type in notification: {v!r}')
|
||||
if not prev.done and prev.identifier == self.identifier:
|
||||
self.merge_metadata(prev)
|
||||
return payload_type, payload_is_encoded
|
||||
|
|
@ -294,6 +306,10 @@ def merge_metadata(self, prev: 'NotificationCommand') -> None:
|
|||
self.icon_data_key = prev.icon_data_key
|
||||
if not self.icon_name:
|
||||
self.icon_name = prev.icon_name
|
||||
if not self.application_name:
|
||||
self.application_name = prev.application_name
|
||||
if not self.notification_type:
|
||||
self.notification_type = prev.notification_type
|
||||
self.icon_path = prev.icon_path
|
||||
|
||||
def create_payload_buffer(self, payload_type: PayloadType) -> EncodedDataStore:
|
||||
|
|
@ -347,6 +363,12 @@ def finalise(self) -> None:
|
|||
icd = self.icon_data_cache_ref()
|
||||
if icd:
|
||||
self.icon_path = icd.get_icon(self.icon_data_key)
|
||||
if self.title:
|
||||
self.title = sanitize_text(self.title)
|
||||
self.body = sanitize_text(self.body)
|
||||
else:
|
||||
self.title = sanitize_text(self.body)
|
||||
self.body = ''
|
||||
|
||||
|
||||
class DesktopIntegration:
|
||||
|
|
@ -363,14 +385,7 @@ def initialize(self) -> None:
|
|||
def close_notification(self, desktop_notification_id: int) -> bool:
|
||||
raise NotImplementedError('Implement me in subclass')
|
||||
|
||||
def notify(self,
|
||||
title: str,
|
||||
body: str,
|
||||
timeout: int = -1,
|
||||
application: str = 'kitty',
|
||||
icon_name: str = '', icon_path: str = '',
|
||||
urgency: Urgency = Urgency.Normal,
|
||||
) -> int:
|
||||
def notify(self, nc: NotificationCommand) -> int:
|
||||
raise NotImplementedError('Implement me in subclass')
|
||||
|
||||
def on_new_version_notification_activation(self, cmd: NotificationCommand) -> None:
|
||||
|
|
@ -401,14 +416,7 @@ def close_notification(self, desktop_notification_id: int) -> bool:
|
|||
log_error(f'Close request for {desktop_notification_id=} {"succeeded" if close_succeeded else "failed"}')
|
||||
return close_succeeded
|
||||
|
||||
def notify(self,
|
||||
title: str,
|
||||
body: str,
|
||||
timeout: int = -1,
|
||||
application: str = 'kitty',
|
||||
icon_name: str = '', icon_path: str = '',
|
||||
urgency: Urgency = Urgency.Normal,
|
||||
) -> int:
|
||||
def notify(self, nc: NotificationCommand) -> int:
|
||||
desktop_notification_id = next(self.id_counter)
|
||||
from .fast_data_types import cocoa_send_notification
|
||||
# If the body is not set macos makes the title the body and uses
|
||||
|
|
@ -417,8 +425,9 @@ def notify(self,
|
|||
# says printf style strings are stripped this doesnt actually happen,
|
||||
# so dont double %
|
||||
# for %% escaping.
|
||||
body = (body or ' ')
|
||||
cocoa_send_notification(str(desktop_notification_id), title, body, '', urgency.value)
|
||||
body = (nc.body or ' ')
|
||||
urgency = Urgency.Normal if nc.urgency is None else nc.urgency
|
||||
cocoa_send_notification(str(desktop_notification_id), nc.title, body, '', urgency.value)
|
||||
return desktop_notification_id
|
||||
|
||||
def notification_activated(self, event: str, ident: str) -> None:
|
||||
|
|
@ -487,19 +496,13 @@ def dispatch_event_from_desktop(self, event_type: str, dbus_notification_id: int
|
|||
elif event_type == 'closed':
|
||||
self.notification_manager.notification_closed(desktop_notification_id)
|
||||
|
||||
def notify(self,
|
||||
title: str,
|
||||
body: str,
|
||||
timeout: int = -1,
|
||||
application: str = 'kitty',
|
||||
icon_name: str = '', icon_path: str = '',
|
||||
urgency: Urgency = Urgency.Normal,
|
||||
) -> int:
|
||||
def notify(self, nc: NotificationCommand) -> int:
|
||||
from .fast_data_types import dbus_send_notification
|
||||
app_icon = icon_name or icon_path or get_custom_window_icon()[1] or logo_png_file
|
||||
body = body.replace('<', '<\u200c') # prevent HTML tags from being recognized
|
||||
app_icon = nc.icon_name or nc.icon_path or get_custom_window_icon()[1] or logo_png_file
|
||||
body = nc.body.replace('<', '<\u200c') # prevent HTML tags from being recognized
|
||||
urgency = Urgency.Normal if nc.urgency is None else nc.urgency
|
||||
desktop_notification_id = dbus_send_notification(
|
||||
app_name=application, app_icon=app_icon, title=title, body=body, timeout=timeout, urgency=urgency.value)
|
||||
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, timeout=-1, urgency=urgency.value)
|
||||
if debug_desktop_integration:
|
||||
log_error(f'Created notification with {desktop_notification_id=}')
|
||||
return desktop_notification_id
|
||||
|
|
@ -648,15 +651,9 @@ def is_notification_allowed(self, cmd: NotificationCommand, channel_id: int) ->
|
|||
def notify_with_command(self, cmd: NotificationCommand, channel_id: int) -> Optional[int]:
|
||||
cmd.channel_id = channel_id
|
||||
cmd.finalise()
|
||||
title = cmd.title or cmd.body
|
||||
body = cmd.body if cmd.title else ''
|
||||
if not title or not self.is_notification_allowed(cmd, channel_id):
|
||||
if not cmd.title or not self.is_notification_allowed(cmd, channel_id):
|
||||
return None
|
||||
urgency = Urgency.Normal if cmd.urgency is None else cmd.urgency
|
||||
desktop_notification_id = self.desktop_integration.notify(
|
||||
title=sanitize_text(title), body=sanitize_text(body), urgency=urgency,
|
||||
icon_name=cmd.icon_name, icon_path=cmd.icon_path,
|
||||
)
|
||||
desktop_notification_id = self.desktop_integration.notify(cmd)
|
||||
self.register_in_progress_notification(cmd, desktop_notification_id)
|
||||
return desktop_notification_id
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,17 @@
|
|||
import re
|
||||
import tempfile
|
||||
from base64 import standard_b64encode
|
||||
from typing import Optional
|
||||
|
||||
from kitty.notifications import Channel, DesktopIntegration, IconDataCache, NotificationManager, UIState, Urgency
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
|
||||
def n(title='title', body='', urgency=Urgency.Normal, desktop_notification_id=1, icon_name='', icon_path=''):
|
||||
return {'title': title, 'body': body, 'urgency': urgency, 'id': desktop_notification_id, 'icon_name': icon_name, 'icon_path': icon_path}
|
||||
def n(title='title', body='', urgency=Urgency.Normal, desktop_notification_id=1, icon_name='', icon_path='', application_name='', notification_type=''):
|
||||
return {
|
||||
'title': title, 'body': body, 'urgency': urgency, 'id': desktop_notification_id, 'icon_name': icon_name, 'icon_path': icon_path,
|
||||
'application_name': application_name, 'notification_type': notification_type,
|
||||
}
|
||||
|
||||
|
||||
class DesktopIntegration(DesktopIntegration):
|
||||
|
|
@ -37,18 +39,10 @@ def close_notification(self, desktop_notification_id: int) -> bool:
|
|||
self.notification_manager.notification_closed(desktop_notification_id)
|
||||
return self.close_succeeds
|
||||
|
||||
def notify(self,
|
||||
title: str,
|
||||
body: str,
|
||||
timeout: int = -1,
|
||||
application: str = 'kitty',
|
||||
icon_name: str = '', icon_path: str = '',
|
||||
subtitle: Optional[str] = None,
|
||||
urgency: Urgency = Urgency.Normal,
|
||||
) -> int:
|
||||
def notify(self, cmd) -> int:
|
||||
self.counter += 1
|
||||
ans = n(title, body, urgency, self.counter, icon_name)
|
||||
ans['icon_path'] = os.path.basename(icon_path)
|
||||
title, body, urgency = cmd.title, cmd.body, (Urgency.Normal if cmd.urgency is None else cmd.urgency)
|
||||
ans = n(title, body, urgency, self.counter, cmd.icon_name, os.path.basename(cmd.icon_path), cmd.application_name, cmd.notification_type)
|
||||
self.notifications.append(ans)
|
||||
return self.counter
|
||||
|
||||
|
|
@ -230,6 +224,15 @@ def assert_events(focus=True, close=0, report='', close_response=''):
|
|||
self.ae(di.notifications, [n(text, text)])
|
||||
reset()
|
||||
|
||||
# Test application name and notification type
|
||||
def e(x):
|
||||
return standard_b64encode(x.encode()).decode()
|
||||
|
||||
h(f'i=t:d=0:f={e("app")};title')
|
||||
h(f'i=t:t={e("test")}')
|
||||
self.ae(di.notifications, [n(application_name='app', notification_type='test')])
|
||||
reset()
|
||||
|
||||
# Test Disk Cache
|
||||
dc = IconDataCache(base_cache_dir=tdir, max_cache_size=4)
|
||||
cache_dir = dc._ensure_state()
|
||||
|
|
|
|||
Loading…
Reference in a new issue