Add a new escape code to allow terminal programs to trigger desktop notifications
Fixes #1474
This commit is contained in:
parent
cd76d109f5
commit
eca53bfab0
@ -16,6 +16,9 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||
so people using kitty from homebrew/source are out of luck. Complain to
|
||||
Apple.
|
||||
|
||||
- Add a new extensible escape code to allow terminal programs to trigger
|
||||
desktop notifications. See :ref:`desktop_notifications` (:iss:`1474`)
|
||||
|
||||
|
||||
0.18.3 [2020-08-11]
|
||||
-------------------
|
||||
|
||||
@ -228,3 +228,126 @@ no ill-effects in other terminal emulators.
|
||||
In case you're using software that can't be easily adapted to this
|
||||
protocol extension, it can be disabled by specifying ``no-append`` to the
|
||||
:opt:`clipboard_control` setting.
|
||||
|
||||
|
||||
.. _desktop_notifications:
|
||||
|
||||
|
||||
Desktop notifications
|
||||
---------------------------------
|
||||
|
||||
|kitty| implements an extensible escape code (OSC 99) to show desktop
|
||||
notifications. It is easy to use from shell scripts and fully extensible to
|
||||
show title and body. Clicking on the notification can optionally focus the
|
||||
window it came from, and/or send an escape code back to the application running
|
||||
in that window.
|
||||
|
||||
The design of the escape code is partially based on the discussion in
|
||||
the defunct
|
||||
`terminal-wg <https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/13>`_
|
||||
|
||||
The escape code has the form::
|
||||
|
||||
<OSC> 99 ; metadata ; payload <terminator>
|
||||
|
||||
Here ``<OSC>`` is :code:`<ESC>]` and ``<terminator>`` is :code:`<ESC><backslash>`. The
|
||||
metadata is a section of colon separated :code:`key=value` pairs. Every key
|
||||
must be a single character from the set :code:`[a-zA-Z]` and every value must
|
||||
be a character from the set :code:`a-zA-Z0-9-_/\+.,(){}[]*&^%$#@!`~`. The
|
||||
payload must be interpreted based on the metadata section. The two semi-colons
|
||||
*must* always be present even when no metadata is present.
|
||||
|
||||
Before going into details, lets see how one can display a simple, single line
|
||||
notification from a shell script::
|
||||
|
||||
printf '\x1b]99;;Hello world\x1b\\'
|
||||
|
||||
To show a message with a title and a body::
|
||||
|
||||
printf '\x1b]99;i=1:d=0;Hello world\x1b\\'
|
||||
printf '\x1b]99;i=1:d=1:p=body;This is cool\x1b\\'
|
||||
|
||||
The most important key in the metadata is the ``p`` key, it controls how the
|
||||
payload is interpreted. A value of ``title`` means the payload is setting the
|
||||
title for the notification. A value of ``body`` means it is setting the body,
|
||||
and so on, see the table below for full details.
|
||||
|
||||
The design of the escape code is fundamentally chunked, this is because
|
||||
different terminal emulators have different limits on how large a single escape
|
||||
code can be. Chunking is accomplished by the ``i`` and ``d`` keys. The ``i``
|
||||
key is the *notification id* which can be any string containing the characters
|
||||
``[a-zA-Z0-9_-+.]``. The ``d`` key stands for *done* and
|
||||
can only take the values ``0`` and ``1``. A value of ``0`` means the
|
||||
notification is not yet done and the terminal emulator should hold off
|
||||
displaying it. A value of ``1`` means the notification is done, and should be
|
||||
displayed. You can specify the title or body multiple times and the terminal
|
||||
emulator will concatenate them, thereby allowing arbitrarily long text
|
||||
(terminal emulators are free to impose a sensible limit to avoid
|
||||
Denial-of-Service attacks).
|
||||
|
||||
Both the ``title`` and ``body`` payloads must be either UTF-8 encoded plain
|
||||
text with no embedded escape codes, or UTF-8 text that is base64 encoded, in
|
||||
which case there must be an ``e=1`` key in the metadata to indicate the payload
|
||||
is base64 encoded.
|
||||
|
||||
When the user clicks the notification, a couple of things can happen, the
|
||||
terminal emulator can focus the window from which the notification came, and/or
|
||||
it can send back an escape code to the application indicating the notification
|
||||
was activated. This is controlled by the ``a`` key which takes a comma
|
||||
separated set of values, ``report`` and ``focus``. The value ``focus`` means
|
||||
focus the window from which the notification was issued and is the default.
|
||||
``report`` means send an escape code back to the application. The format of the
|
||||
escape code is::
|
||||
|
||||
<OSC> 99 ; i=identifier ; <terminator>
|
||||
|
||||
The value of ``identifier`` comes from the ``i`` key in the escape code sent by
|
||||
the application. If the application sends no identifier, then the terminal
|
||||
*must* use ``i=0``. Actions can be preceded by a negative sign to turn them
|
||||
off, so for example if you do not want any action, turn off the default
|
||||
``focus`` action with::
|
||||
|
||||
a=-focus
|
||||
|
||||
Complete specification of all the metadata keys is in the table below. If a
|
||||
terminal emulator encounters a key in the metadata it does not understand,
|
||||
the key *must* be ignored, to allow for future extensibility of this escape
|
||||
code. Similarly if values for known keys are unknown, the terminal emulator
|
||||
*should* either ignore the entire escape code or perform a best guess effort
|
||||
to display it based on what it does understand.
|
||||
|
||||
.. note::
|
||||
It is possible to extend this escape code to allow specifying an icon for
|
||||
the notification, however, given that some platforms, such as macOS, dont
|
||||
allow displaying custom icons on a notification, at all, it was decided to
|
||||
leave it out of the spec for the time being.
|
||||
|
||||
Similarly, features such a scheduled notifications could be added in future
|
||||
revisions.
|
||||
|
||||
|
||||
======= ==================== ========= =================
|
||||
Key Value Default Description
|
||||
======= ==================== ========= =================
|
||||
``a`` Comma separated list ``focus`` What action to perform when the
|
||||
of ``report``, notification is clicked
|
||||
``focus``, with
|
||||
optional leading
|
||||
``-``
|
||||
|
||||
``d`` ``0`` or ``1`` ``1`` Indicates if the notification is
|
||||
complete or not.
|
||||
|
||||
``e`` ``0`` or ``1`` ``0`` If set to ``1`` means the payload is base64 encoded UTF-8,
|
||||
otherwise it is plain UTF-8 text with no C0 control codes in it
|
||||
|
||||
``i`` ``[a-zA-Z0-9-_+.]`` ``0`` Identifier for the notification
|
||||
|
||||
``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.
|
||||
======= ==================== ========= =================
|
||||
|
||||
|
||||
.. note::
|
||||
|kitty| also supports the legacy OSC 9 protocol developed by iTerm2 for
|
||||
desktop notifications.
|
||||
|
||||
@ -39,6 +39,7 @@ from .fast_data_types import (
|
||||
)
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
from .layout.base import set_layout_options
|
||||
from .notify import notification_activated
|
||||
from .options_stub import Options
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .rgb import Color, color_from_int
|
||||
@ -63,14 +64,6 @@ class OSWindowDict(TypedDict):
|
||||
tabs: List[TabDict]
|
||||
|
||||
|
||||
def notification_activated(identifier: str) -> None:
|
||||
if identifier == 'new-version':
|
||||
from .update_check import notification_activated as do
|
||||
do()
|
||||
elif identifier.startswith('test-notify-'):
|
||||
log_error(f'Test notification {identifier} activated')
|
||||
|
||||
|
||||
def listen_on(spec: str) -> int:
|
||||
import socket
|
||||
family, address, socket_path = parse_address_spec(spec)
|
||||
@ -1545,3 +1538,12 @@ class Boss:
|
||||
now = monotonic()
|
||||
ident = f'test-notify-{now}'
|
||||
notify(f'Test {now}', f'At: {now}', identifier=ident)
|
||||
|
||||
def notification_activated(self, identifier: str, window_id: int, focus: bool, report: bool) -> None:
|
||||
w = self.window_id_map.get(window_id)
|
||||
if w is None:
|
||||
return
|
||||
if focus:
|
||||
self.set_active_window(w, switch_os_window_if_needed=True)
|
||||
if report:
|
||||
w.report_notification_activated(identifier)
|
||||
|
||||
152
kitty/notify.py
152
kitty/notify.py
@ -2,9 +2,14 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Dict, Optional
|
||||
from base64 import standard_b64decode
|
||||
from collections import OrderedDict
|
||||
from itertools import count
|
||||
from typing import Dict, Optional, Callable
|
||||
|
||||
from .constants import is_macos, logo_png_file
|
||||
from .fast_data_types import get_boss
|
||||
from .utils import log_error
|
||||
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_send_notification
|
||||
@ -35,7 +40,6 @@ else:
|
||||
rmap = {v: k for k, v in identifier_map.items()}
|
||||
identifier = rmap.get(notification_id)
|
||||
if identifier is not None:
|
||||
from .boss import notification_activated
|
||||
notification_activated(identifier)
|
||||
|
||||
def notify(
|
||||
@ -52,3 +56,147 @@ else:
|
||||
alloc_id = dbus_send_notification(application, icf, title, body, 'Click to see changes', timeout)
|
||||
if alloc_id and identifier is not None:
|
||||
alloc_map[alloc_id] = identifier
|
||||
|
||||
|
||||
class NotificationCommand:
|
||||
|
||||
done: bool = True
|
||||
identifier: str = '0'
|
||||
title: str = ''
|
||||
body: str = ''
|
||||
actions: str = ''
|
||||
|
||||
|
||||
def parse_osc_9(raw: str) -> NotificationCommand:
|
||||
ans = NotificationCommand()
|
||||
ans.title = raw
|
||||
return ans
|
||||
|
||||
|
||||
def parse_osc_99(raw: str) -> NotificationCommand:
|
||||
cmd = NotificationCommand()
|
||||
metadata, payload = raw.partition(';')[::2]
|
||||
payload_is_encoded = False
|
||||
payload_type = 'title'
|
||||
if metadata:
|
||||
for part in metadata.split(':'):
|
||||
try:
|
||||
k, v = part.split('=', 1)
|
||||
except Exception:
|
||||
log_error('Malformed OSC 99: metadata is not key=value pairs')
|
||||
return cmd
|
||||
if k == 'p':
|
||||
payload_type = v
|
||||
elif k == 'i':
|
||||
cmd.identifier = v
|
||||
elif k == 'e':
|
||||
payload_is_encoded = v == '1'
|
||||
elif k == 'd':
|
||||
cmd.done = v != '0'
|
||||
elif k == 'a':
|
||||
cmd.actions += ',' + v
|
||||
if payload_type not in ('body', 'title'):
|
||||
log_error(f'Malformed OSC 99: unknown payload type: {payload_type}')
|
||||
return NotificationCommand()
|
||||
if payload_is_encoded:
|
||||
try:
|
||||
payload = standard_b64decode(payload).decode('utf-8')
|
||||
except Exception:
|
||||
log_error('Malformed OSC 99: payload is not base64 encoded UTF-8 text')
|
||||
return NotificationCommand()
|
||||
if payload_type == 'title':
|
||||
cmd.title = payload
|
||||
else:
|
||||
cmd.body = payload
|
||||
return cmd
|
||||
|
||||
|
||||
def limit_size(x: str) -> str:
|
||||
if len(x) > 1024:
|
||||
x = x[:1024]
|
||||
return x
|
||||
|
||||
|
||||
def merge_osc_99(prev: NotificationCommand, cmd: NotificationCommand) -> NotificationCommand:
|
||||
if prev.done or prev.identifier != cmd.identifier:
|
||||
return cmd
|
||||
cmd.actions = limit_size(prev.actions + ',' + cmd.actions)
|
||||
cmd.title = limit_size(prev.title + cmd.title)
|
||||
cmd.body = limit_size(prev.body + cmd.body)
|
||||
return cmd
|
||||
|
||||
|
||||
identifier_registry: "OrderedDict[str, RegisteredNotification]" = OrderedDict()
|
||||
id_counter = count()
|
||||
|
||||
|
||||
class RegisteredNotification:
|
||||
identifier: str
|
||||
window_id: int
|
||||
focus: bool = True
|
||||
report: bool = False
|
||||
|
||||
def __init__(self, cmd: NotificationCommand, window_id: int):
|
||||
self.window_id = window_id
|
||||
for x in cmd.actions.strip(',').split(','):
|
||||
val = not x.startswith('-')
|
||||
x = x.lstrip('+-')
|
||||
if x == 'focus':
|
||||
self.focus = val
|
||||
elif x == 'report':
|
||||
self.report = val
|
||||
self.identifier = cmd.identifier
|
||||
|
||||
|
||||
def register_identifier(identifier: str, cmd: NotificationCommand, window_id: int) -> None:
|
||||
identifier_registry[identifier] = RegisteredNotification(cmd, window_id)
|
||||
if len(identifier_registry) > 100:
|
||||
identifier_registry.popitem(False)
|
||||
|
||||
|
||||
def notification_activated(identifier: str, activated_implementation: Optional[Callable] = None) -> None:
|
||||
if identifier == 'new-version':
|
||||
from .update_check import notification_activated as do
|
||||
do()
|
||||
elif identifier.startswith('test-notify-'):
|
||||
log_error(f'Test notification {identifier} activated')
|
||||
else:
|
||||
r = identifier_registry.pop(identifier, None)
|
||||
if r is not None and (r.focus or r.report):
|
||||
if activated_implementation is None:
|
||||
get_boss().notification_activated(r.identifier, r.window_id, r.focus, r.report)
|
||||
else:
|
||||
activated_implementation(r.identifier, r.window_id, r.focus, r.report)
|
||||
|
||||
|
||||
def reset_registry() -> None:
|
||||
global id_counter
|
||||
identifier_registry.clear()
|
||||
id_counter = count()
|
||||
|
||||
|
||||
def notify_with_command(cmd: NotificationCommand, window_id: int, notify: Callable = notify) -> None:
|
||||
title = cmd.title or cmd.body
|
||||
body = cmd.body if cmd.title else ''
|
||||
if title:
|
||||
identifier = 'i' + str(next(id_counter))
|
||||
notify(title, body, identifier=identifier)
|
||||
register_identifier(identifier, cmd, window_id)
|
||||
|
||||
|
||||
def handle_notification_cmd(
|
||||
osc_code: int,
|
||||
raw_data: str,
|
||||
window_id: int,
|
||||
prev_cmd: NotificationCommand,
|
||||
notify_implementation: Callable = notify
|
||||
) -> Optional[NotificationCommand]:
|
||||
if osc_code == 99:
|
||||
cmd = merge_osc_99(prev_cmd, parse_osc_99(raw_data))
|
||||
if cmd.done:
|
||||
notify_with_command(cmd, window_id, notify_implementation)
|
||||
cmd = NotificationCommand()
|
||||
return cmd
|
||||
if osc_code == 9:
|
||||
cmd = parse_osc_9(raw_data)
|
||||
notify_with_command(cmd, window_id, notify_implementation)
|
||||
|
||||
@ -301,10 +301,11 @@ handle_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_cal
|
||||
} // }}}
|
||||
|
||||
// OSC mode {{{
|
||||
|
||||
static inline void
|
||||
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
#define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, string); name(screen, code, string);
|
||||
#define DISPATCH_OSC(name) REPORT_OSC(name, string); name(screen, string);
|
||||
#define SET_COLOR(name) REPORT_OSC2(name, code, string); name(screen, code, string);
|
||||
const unsigned int limit = screen->parser_buf_pos;
|
||||
unsigned int code=0, i;
|
||||
for (i = 0; i < MIN(limit, 5u); i++) {
|
||||
@ -329,7 +330,11 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
break;
|
||||
case 4:
|
||||
case 104:
|
||||
SET_COLOR(set_color_table_color);
|
||||
DISPATCH_OSC_WITH_CODE(set_color_table_color);
|
||||
break;
|
||||
case 9:
|
||||
case 99:
|
||||
DISPATCH_OSC_WITH_CODE(desktop_notify)
|
||||
break;
|
||||
case 10:
|
||||
case 11:
|
||||
@ -341,7 +346,7 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
case 112:
|
||||
case 117:
|
||||
case 119:
|
||||
SET_COLOR(set_dynamic_color);
|
||||
DISPATCH_OSC_WITH_CODE(set_dynamic_color);
|
||||
break;
|
||||
case 52:
|
||||
DISPATCH_OSC(clipboard_control);
|
||||
@ -361,7 +366,7 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
Py_CLEAR(string);
|
||||
}
|
||||
#undef DISPATCH_OSC
|
||||
#undef SET_COLOR
|
||||
#undef DISPATCH_OSC_WITH_CODE
|
||||
}
|
||||
// }}}
|
||||
|
||||
|
||||
@ -1495,6 +1495,11 @@ set_title(Screen *self, PyObject *title) {
|
||||
CALLBACK("title_changed", "O", title);
|
||||
}
|
||||
|
||||
void
|
||||
desktop_notify(Screen *self, unsigned int osc_code, PyObject *data) {
|
||||
CALLBACK("desktop_notify", "IO", osc_code, data);
|
||||
}
|
||||
|
||||
void
|
||||
set_icon(Screen *self, PyObject *icon) {
|
||||
CALLBACK("icon_changed", "O", icon);
|
||||
|
||||
@ -178,6 +178,7 @@ void screen_handle_print(Screen *, PyObject *cmd);
|
||||
void screen_designate_charset(Screen *, uint32_t which, uint32_t as);
|
||||
void screen_use_latin1(Screen *, bool);
|
||||
void set_title(Screen *self, PyObject*);
|
||||
void desktop_notify(Screen *self, unsigned int, PyObject*);
|
||||
void set_icon(Screen *self, PyObject*);
|
||||
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
|
||||
void clipboard_control(Screen *self, PyObject*);
|
||||
|
||||
@ -30,6 +30,7 @@ from .fast_data_types import (
|
||||
viewport_for_window
|
||||
)
|
||||
from .keys import defines, extended_key_event, keyboard_mode_name
|
||||
from .notify import NotificationCommand, handle_notification_cmd
|
||||
from .options_stub import Options
|
||||
from .rgb import to_color
|
||||
from .terminfo import get_capabilities
|
||||
@ -218,6 +219,7 @@ class Window:
|
||||
watchers: Optional[Watchers] = None
|
||||
):
|
||||
self.watchers = watchers or Watchers()
|
||||
self.prev_osc99_cmd = NotificationCommand()
|
||||
self.action_on_close: Optional[Callable] = None
|
||||
self.action_on_removal: Optional[Callable] = None
|
||||
self.current_marker_spec: Optional[Tuple[str, Union[str, Tuple[Tuple[int, str], ...]]]] = None
|
||||
@ -434,6 +436,11 @@ class Window:
|
||||
self.override_title = title or None
|
||||
self.title_updated()
|
||||
|
||||
def desktop_notify(self, osc_code: int, raw_data: str) -> None:
|
||||
cmd = handle_notification_cmd(osc_code, raw_data, self.id, self.prev_osc99_cmd)
|
||||
if cmd is not None and osc_code == 99:
|
||||
self.prev_osc99_cmd = cmd
|
||||
|
||||
# screen callbacks {{{
|
||||
def use_utf8(self, on: bool) -> None:
|
||||
get_boss().child_monitor.set_iutf8_winid(self.id, on)
|
||||
@ -511,6 +518,9 @@ class Window:
|
||||
b |= b << 8
|
||||
self.screen.send_escape_code_to_child(OSC, '{};rgb:{:04x}/{:04x}/{:04x}'.format(code, r, g, b))
|
||||
|
||||
def report_notification_activated(self, identifier: str) -> None:
|
||||
self.screen.send_escape_code_to_child(OSC, f'99;i={identifier};')
|
||||
|
||||
def set_dynamic_color(self, code: int, value: Union[str, bytes]) -> None:
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('utf-8')
|
||||
|
||||
@ -37,10 +37,14 @@ class Callbacks:
|
||||
def use_utf8(self, on):
|
||||
self.iutf8 = on
|
||||
|
||||
def desktop_notify(self, osc_code: int, raw_data: str) -> None:
|
||||
self.notifications.append((osc_code, raw_data))
|
||||
|
||||
def clear(self):
|
||||
self.wtcbuf = b''
|
||||
self.iconbuf = self.titlebuf = self.colorbuf = self.ctbuf = ''
|
||||
self.iutf8 = True
|
||||
self.notifications = []
|
||||
|
||||
|
||||
def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):
|
||||
|
||||
@ -3,10 +3,15 @@
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import time
|
||||
from base64 import standard_b64encode
|
||||
from binascii import hexlify
|
||||
from functools import partial
|
||||
|
||||
from kitty.fast_data_types import CURSOR_BLOCK, parse_bytes, parse_bytes_dump
|
||||
from kitty.notify import (
|
||||
NotificationCommand, handle_notification_cmd, notification_activated,
|
||||
reset_registry
|
||||
)
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
@ -195,6 +200,7 @@ class TestParser(BaseTest):
|
||||
pb('\t\033[b', ('screen_tab',), ('screen_repeat_character', 1))
|
||||
self.ae(str(s.line(0)), '\t')
|
||||
s.reset()
|
||||
b']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'
|
||||
|
||||
def test_osc_codes(self):
|
||||
s = self.create_screen()
|
||||
@ -216,6 +222,74 @@ class TestParser(BaseTest):
|
||||
self.ae(c.titlebuf, '')
|
||||
pb('\033]110\x07', ('set_dynamic_color', 110, ''))
|
||||
self.ae(c.colorbuf, '')
|
||||
c.clear()
|
||||
pb('\033]9;\x07', ('desktop_notify', 9, ''))
|
||||
pb('\033]9;test it\x07', ('desktop_notify', 9, 'test it'))
|
||||
pb('\033]99;moo=foo;test it\x07', ('desktop_notify', 99, 'moo=foo;test it'))
|
||||
self.ae(c.notifications, [(9, ''), (9, 'test it'), (99, 'moo=foo;test it')])
|
||||
|
||||
def test_desktop_notify(self):
|
||||
reset_registry()
|
||||
notifications = []
|
||||
activations = []
|
||||
prev_cmd = NotificationCommand()
|
||||
|
||||
def reset():
|
||||
nonlocal prev_cmd
|
||||
reset_registry()
|
||||
del notifications[:]
|
||||
del activations[:]
|
||||
prev_cmd = NotificationCommand()
|
||||
|
||||
def notify(title, body, identifier):
|
||||
notifications.append((title, body, identifier))
|
||||
|
||||
def h(raw_data, osc_code=99, window_id=1):
|
||||
nonlocal prev_cmd
|
||||
x = handle_notification_cmd(osc_code, raw_data, window_id, prev_cmd, notify)
|
||||
if x is not None and osc_code == 99:
|
||||
prev_cmd = x
|
||||
|
||||
def activated(identifier, window_id, focus, report):
|
||||
activations.append((identifier, window_id, focus, report))
|
||||
|
||||
h('test it', osc_code=9)
|
||||
self.ae(notifications, [('test it', '', 'i0')])
|
||||
notification_activated(notifications[-1][-1], activated)
|
||||
self.ae(activations, [('0', 1, True, False)])
|
||||
reset()
|
||||
|
||||
h('d=0:i=x;title')
|
||||
h('d=1:i=x:p=body;body')
|
||||
self.ae(notifications, [('title', 'body', 'i0')])
|
||||
notification_activated(notifications[-1][-1], activated)
|
||||
self.ae(activations, [('x', 1, True, False)])
|
||||
reset()
|
||||
|
||||
h('i=x:p=body:a=-focus;body')
|
||||
self.ae(notifications, [('body', '', 'i0')])
|
||||
notification_activated(notifications[-1][-1], activated)
|
||||
self.ae(activations, [])
|
||||
reset()
|
||||
|
||||
h('i=x:e=1;' + standard_b64encode(b'title').decode('ascii'))
|
||||
self.ae(notifications, [('title', '', 'i0')])
|
||||
notification_activated(notifications[-1][-1], activated)
|
||||
self.ae(activations, [('x', 1, True, False)])
|
||||
reset()
|
||||
|
||||
h('d=0:i=x:a=-report;title')
|
||||
h('d=1:i=x:a=report;body')
|
||||
self.ae(notifications, [('titlebody', '', 'i0')])
|
||||
notification_activated(notifications[-1][-1], activated)
|
||||
self.ae(activations, [('x', 1, True, True)])
|
||||
reset()
|
||||
|
||||
h(';title')
|
||||
self.ae(notifications, [('title', '', 'i0')])
|
||||
notification_activated(notifications[-1][-1], activated)
|
||||
self.ae(activations, [('0', 1, True, False)])
|
||||
reset()
|
||||
|
||||
def test_dcs_codes(self):
|
||||
s = self.create_screen()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user