winotify

What is winotify?

winotify is a python library and a command-line application to make Windows 10 toast notification.

Features

  • Notification stays in action center
  • Clickable notification with 5 additional buttons
  • Use function as a callback when clicking the notification

Installation

Install winotify using pip

pip install winotify

Example

A simple notification with icon

from winotify import Notification

toast = Notification(app_id="example app",
                     title="Winotify Test Toast",
                     msg="New Notification!",
                     icon=r"C:\path\to\icon.png")

toast.show()

How to ...

... add buttons to the notification

from winotify import Notification, audio

toast = Notification(...)
toast.add_actions(label="open github",
                 launch="https://github.com/versa-syahptr/winotify/")

... set sound of the notification

from winotify import Notification, audio

toast = Notification(...)
toast.set_audio(audio.Mail, loop=False)

All supported audio are in the audio module

... use callback feature

this is an advanced feature of winotify. Please follow this guide carefully

  • Declare your app id, default interpreter, and script path globally
import winotify

r = winotify.Registry("app_id", winotify.PY_EXE, r"c:\abs\path\to\script.py")
notifier = winotify.Notifier(r)
@notifier.register_callback
def say_hello():
    print("hello")
toast = notifier.create_notification(title="a notification", 
                                     msg='a notification test with callback',
                                     launch=say_hello)

# or pass it to `Notification.add_actions()`

toast.add_actions(label="say hello in console",
                  launch=say_hello)
  • Start the notifier thread
if __name__ == '__main__':
    notifier.start()

Command-line Application

winotify.exe ^
-id myApp ^
-t "A Title" ^
-m "A message" ^
-i "c:\path\to\icon.png" ^
--audio default ^
--open-url "http://google.com" ^
--action "open github" ^
--action_url "http://github.com"         

Use winotify-nc.exe instead of winotify.exe to hide the console window.

View Source
"""
.. include:: ./documentation.md
"""


import queue
import os
import subprocess
import sys
import atexit
from tempfile import gettempdir
from typing import Callable, Union

from winotify import audio
from winotify._registry import Registry, format_name, PY_EXE, PYW_EXE
from winotify._communication import Listener, Sender


__author__ = "Versa Syahputra"
__version__ = "1.1.0-dev"
__all__ = ["Notifier", "Notification", "Registry", "audio"]


TEMPLATE = r"""
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
$Template = @"
<toast {launch} duration="{duration}">
    <visual>
        <binding template="ToastImageAndText02">
            <image id="1" src="{icon}" />
            <text id="1"><![CDATA[{title}]]></text>
            <text id="2"><![CDATA[{msg}]]></text>
        </binding>
    </visual>
    <actions>
        {actions}
    </actions>
    {audio}
</toast>
"@

$SerializedXml = New-Object Windows.Data.Xml.Dom.XmlDocument
$SerializedXml.LoadXml($Template)

$Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml)
$Toast.Tag = "{tag}"
$Toast.Group = "{group}"

$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("{app_id}")
$Notifier.Show($Toast);
"""

tempdir = gettempdir()


def _run_ps(*, file='', command=''):
    si = subprocess.STARTUPINFO()
    si.dwFlags |= subprocess.STARTF_USESHOWWINDOW

    cmd = ["powershell.exe", "-ExecutionPolicy", "Bypass"]
    if file and command:
        raise ValueError
    elif file:
        cmd.extend(["-file", file])
    elif command:
        cmd.extend(['-Command', command])
    else:
        raise ValueError

    subprocess.Popen(
        cmd,
        # stdin, stdout, and stderr have to be defined here, because windows tries to duplicate these if not null
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,  # set to null because we don't need the output :)
        stderr=subprocess.DEVNULL,
        startupinfo=si
    )


class Notification(object):
    def __init__(self,
                 app_id: str,
                 title: str,
                 msg: str = "",
                 icon: str = "",
                 duration: str = 'short',
                 launch: str = ''):
        """
        Construct a new notification

        Args:
            app_id: your app name, make it readable to your user. It can contain spaces, however special characters
                    (eg. é) are not supported.
            title: The heading of the toast.
            msg: The content/message of the toast.
            icon: An optional path to an image to display on the left of the title & message.
                  Make sure the path is absolute.
            duration: How long the toast should show up for (short/long), default is short.
            launch: The url or callback to launch (invoked when the user clicks the notification)

        Notes:
            If you want to pass a callback to `launch` parameter,
            please use `create_notification` from `Notifier` object

        Raises:
            ValueError: If the duration specified is not short or long
        """

        self.app_id = app_id
        self.title = title
        self.msg = msg
        self.icon = icon
        self.duration = duration
        self.launch = launch
        self.audio = audio.Silent
        self.tag = self.title
        self.group = self.app_id
        self.actions = []
        self.script = ""
        if duration not in ("short", "long"):
            raise ValueError("Duration is not 'short' or 'long'")

    def set_audio(self, sound: audio.Sound, loop: bool):
        """
        Set the audio for the notification

        Args:
            sound: The audio to play when the notification is showing. Choose one from `winotify.audio` module,
                   (eg. audio.Default). The default for all notification is silent.
            loop: If True, the audio will play indefinitely until user click or dismis the notification.

        """

        self.audio = '<audio src="{}" loop="{}" />'.format(sound, str(loop).lower())

    def add_actions(self, label: str, launch: Union[str, Callable] = ""):
        """
        Add buttons to the notification. Each notification can have 5 buttons max.

        Args:
            label: The label of the button
            launch: The url to launch when clicking the button, 'file:///' protocol is allowed. Or a registered
                    callback function

        Returns: None

        Notes:
            Register a callback function using `Notifier.register_callback()` decorator before passing it here

        Raises:
              ValueError: If the callback function is not registered
        """

        if callable(launch):
            if hasattr(launch, 'url'):
                url = launch.url
            else:
                raise ValueError(f"{launch} is not registered")
        else:
            url = launch

        xml = '<action activationType="protocol" content="{label}" arguments="{link}" />'
        if len(self.actions) < 5:
            self.actions.append(xml.format(label=label, link=url))

    def build(self):
        """
        This method is deprecated, call `Notification.show()` directly instead.

        Warnings:
            DeprecationWarning

        """
        import warnings
        warnings.warn("build method is deprecated, call show directly instead", DeprecationWarning)
        return self

    def show(self):
        """
        Show the toast
        """
        if self.actions:
            self.actions = '\n'.join(self.actions)
        else:
            self.actions = ''

        if self.audio == audio.Silent:
            self.audio = '<audio silent="true" />'

        if self.launch:
            self.launch = 'activationType="protocol" launch="{}"'.format(self.launch)

        self.script = TEMPLATE.format(**self.__dict__)

        _run_ps(command=self.script)


class Notifier:
    def __init__(self, registry: Registry):
        """
        A `Notification` manager class.

        Args:
            registry: A `Registry` instance containing the `app_id`, default interpreter, and the script path.
        """
        self.app_id = registry.app
        self.icon = ""
        pidfile = os.path.join(tempdir, f'{self.app_id}.pid')

        # alias for callback_to_url()
        self.cb_url = self.callback_to_url

        if self._protocol_launched():
            # communicate to main process if it's alive
            self.func_to_call = sys.argv[1].split(':')[1]
            self._cb = {}  # callbacks are stored here because we have no listener
            if os.path.isfile(pidfile):
                sender = Sender(self.app_id)
                sender.send(self.func_to_call)
                sys.exit()
        else:
            self.listener = Listener(self.app_id)
            open(pidfile, 'w').write(str(os.getpid()))  # pid file
            atexit.register(os.unlink, pidfile)

    @property
    def callbacks(self):
        """
        Returns:
            A dictionary containing all registered callbacks, with each function's name as the key

        """
        if hasattr(self, 'listener'):
            return self.listener.callbacks
        else:
            return self._cb

    def set_icon(self, path: str):
        """
        Set icon globally for all notification
        Args:
            path: The absolute path of the icon

        Returns:
            None

        """
        self.icon = path

    def create_notification(self,
                            title: str,
                            msg: str = '',
                            icon: str = '',
                            duration: str = 'short',
                            launch: Union[str, Callable] = '') -> Notification:
        """

        See Also:
            `Notification`

        Notes:
            `launch` parameter can be a callback function here

        Returns:
            `Notification` object

        """
        if self.icon:
            icon = self.icon

        if callable(launch):
            url = self.callback_to_url(launch)
        else:
            url = launch

        notif = Notification(self.app_id, title, msg, icon, duration, url)
        return notif

    def start(self):
        """
        Start the listener thread. This method *must* be called first in the main function,
        Otherwise, all the callback function will never get called.

        Examples:
            ```python
            if __name__ == "__main__":
                notifier.start()
                ...
            ```
        """
        if self._protocol_launched():  # call the callback directly
            self.callbacks.get(self.func_to_call)()

        else:
            self.listener.callbacks.update(self.callbacks)
            self.listener.thread.start()

    def update(self):
        """
        check for available callback function in queue then call it
        this method *must* be called *every time* in loop.

        If all callback functions don't need to run in main thread, calling this functions is *optional*

        Examples:
            ```python
            # the main loop
            while True:
                notifier.update()
                ...
            ```
        """
        if self._protocol_launched():
            return

        q = self.listener.queue
        try:
            func = q.get_nowait()
            if callable(func):
                func()
            else:
                print(f"{func.__name__} ")
        except queue.Empty:
            pass

    def _protocol_launched(self) -> bool:
        """
        check whether the app is opened directly or via notification

        Returns:
            True, if opened from notification; False if opened directly
        """
        if len(sys.argv) > 1:
            arg = sys.argv[1]
            return format_name(self.app_id) + ':' in arg and len(arg.split(':')) > 0
        else:
            return False

    def register_callback(self, func=None, *, run_in_main_thread=False):
        """
        A decorator to register a function to be used as a callback
        Args:
            func: the function to decorate
            run_in_main_thread: If True, the callback function will run in main thread

        Examples:
            ```python
            @notifier.register_callback
            def foo(): ...
            ```

        Returns:
            The registered function

        """
        def inner(f):
            if run_in_main_thread:
                f.rimt = run_in_main_thread
            self.callbacks[f.__name__] = f
            f.url = self.callback_to_url(f)
            return f

        if func is None:
            return inner
        else:
            return inner(func)

    def callback_to_url(self, func: Callable) -> str:
        """
        Translate the registered callback function `func` to url notation.

        Args:
            func: The registered callback function

        Returns:
             url-notation string eg. `my-app-id:foo`, where **my-app-id** is the app id and **foo** is the function name

        """

        if callable(func) and func.__name__ in self.callbacks:
            url = format_name(self.app_id) + ":" + func.__name__
            return url

    def clear(self):
        """
        Clear all notification created by `Notifier` from action center

        """

        cmd = f"""\
        [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
        [Windows.UI.Notifications.ToastNotificationManager]::History.Clear('{self.app_id}')
        """
        _run_ps(command=cmd)
#   class Notifier:
View Source
class Notifier:
    def __init__(self, registry: Registry):
        """
        A `Notification` manager class.

        Args:
            registry: A `Registry` instance containing the `app_id`, default interpreter, and the script path.
        """
        self.app_id = registry.app
        self.icon = ""
        pidfile = os.path.join(tempdir, f'{self.app_id}.pid')

        # alias for callback_to_url()
        self.cb_url = self.callback_to_url

        if self._protocol_launched():
            # communicate to main process if it's alive
            self.func_to_call = sys.argv[1].split(':')[1]
            self._cb = {}  # callbacks are stored here because we have no listener
            if os.path.isfile(pidfile):
                sender = Sender(self.app_id)
                sender.send(self.func_to_call)
                sys.exit()
        else:
            self.listener = Listener(self.app_id)
            open(pidfile, 'w').write(str(os.getpid()))  # pid file
            atexit.register(os.unlink, pidfile)

    @property
    def callbacks(self):
        """
        Returns:
            A dictionary containing all registered callbacks, with each function's name as the key

        """
        if hasattr(self, 'listener'):
            return self.listener.callbacks
        else:
            return self._cb

    def set_icon(self, path: str):
        """
        Set icon globally for all notification
        Args:
            path: The absolute path of the icon

        Returns:
            None

        """
        self.icon = path

    def create_notification(self,
                            title: str,
                            msg: str = '',
                            icon: str = '',
                            duration: str = 'short',
                            launch: Union[str, Callable] = '') -> Notification:
        """

        See Also:
            `Notification`

        Notes:
            `launch` parameter can be a callback function here

        Returns:
            `Notification` object

        """
        if self.icon:
            icon = self.icon

        if callable(launch):
            url = self.callback_to_url(launch)
        else:
            url = launch

        notif = Notification(self.app_id, title, msg, icon, duration, url)
        return notif

    def start(self):
        """
        Start the listener thread. This method *must* be called first in the main function,
        Otherwise, all the callback function will never get called.

        Examples:
            ```python
            if __name__ == "__main__":
                notifier.start()
                ...
            ```
        """
        if self._protocol_launched():  # call the callback directly
            self.callbacks.get(self.func_to_call)()

        else:
            self.listener.callbacks.update(self.callbacks)
            self.listener.thread.start()

    def update(self):
        """
        check for available callback function in queue then call it
        this method *must* be called *every time* in loop.

        If all callback functions don't need to run in main thread, calling this functions is *optional*

        Examples:
            ```python
            # the main loop
            while True:
                notifier.update()
                ...
            ```
        """
        if self._protocol_launched():
            return

        q = self.listener.queue
        try:
            func = q.get_nowait()
            if callable(func):
                func()
            else:
                print(f"{func.__name__} ")
        except queue.Empty:
            pass

    def _protocol_launched(self) -> bool:
        """
        check whether the app is opened directly or via notification

        Returns:
            True, if opened from notification; False if opened directly
        """
        if len(sys.argv) > 1:
            arg = sys.argv[1]
            return format_name(self.app_id) + ':' in arg and len(arg.split(':')) > 0
        else:
            return False

    def register_callback(self, func=None, *, run_in_main_thread=False):
        """
        A decorator to register a function to be used as a callback
        Args:
            func: the function to decorate
            run_in_main_thread: If True, the callback function will run in main thread

        Examples:
            ```python
            @notifier.register_callback
            def foo(): ...
            ```

        Returns:
            The registered function

        """
        def inner(f):
            if run_in_main_thread:
                f.rimt = run_in_main_thread
            self.callbacks[f.__name__] = f
            f.url = self.callback_to_url(f)
            return f

        if func is None:
            return inner
        else:
            return inner(func)

    def callback_to_url(self, func: Callable) -> str:
        """
        Translate the registered callback function `func` to url notation.

        Args:
            func: The registered callback function

        Returns:
             url-notation string eg. `my-app-id:foo`, where **my-app-id** is the app id and **foo** is the function name

        """

        if callable(func) and func.__name__ in self.callbacks:
            url = format_name(self.app_id) + ":" + func.__name__
            return url

    def clear(self):
        """
        Clear all notification created by `Notifier` from action center

        """

        cmd = f"""\
        [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
        [Windows.UI.Notifications.ToastNotificationManager]::History.Clear('{self.app_id}')
        """
        _run_ps(command=cmd)
#   Notifier(registry: winotify._registry.Registry)
View Source
    def __init__(self, registry: Registry):
        """
        A `Notification` manager class.

        Args:
            registry: A `Registry` instance containing the `app_id`, default interpreter, and the script path.
        """
        self.app_id = registry.app
        self.icon = ""
        pidfile = os.path.join(tempdir, f'{self.app_id}.pid')

        # alias for callback_to_url()
        self.cb_url = self.callback_to_url

        if self._protocol_launched():
            # communicate to main process if it's alive
            self.func_to_call = sys.argv[1].split(':')[1]
            self._cb = {}  # callbacks are stored here because we have no listener
            if os.path.isfile(pidfile):
                sender = Sender(self.app_id)
                sender.send(self.func_to_call)
                sys.exit()
        else:
            self.listener = Listener(self.app_id)
            open(pidfile, 'w').write(str(os.getpid()))  # pid file
            atexit.register(os.unlink, pidfile)

A Notification manager class.

Args
  • registry: A Registry instance containing the app_id, default interpreter, and the script path.
#   callbacks

Returns: A dictionary containing all registered callbacks, with each function's name as the key

#   def set_icon(self, path: str):
View Source
    def set_icon(self, path: str):
        """
        Set icon globally for all notification
        Args:
            path: The absolute path of the icon

        Returns:
            None

        """
        self.icon = path

Set icon globally for all notification

Args
  • path: The absolute path of the icon
Returns

None

#   def create_notification( self, title: str, msg: str = '', icon: str = '', duration: str = 'short', launch: Union[str, Callable] = '' ) -> winotify.Notification:
View Source
    def create_notification(self,
                            title: str,
                            msg: str = '',
                            icon: str = '',
                            duration: str = 'short',
                            launch: Union[str, Callable] = '') -> Notification:
        """

        See Also:
            `Notification`

        Notes:
            `launch` parameter can be a callback function here

        Returns:
            `Notification` object

        """
        if self.icon:
            icon = self.icon

        if callable(launch):
            url = self.callback_to_url(launch)
        else:
            url = launch

        notif = Notification(self.app_id, title, msg, icon, duration, url)
        return notif
See Also

Notification

Notes

launch parameter can be a callback function here

Returns

Notification object

#   def start(self):
View Source
    def start(self):
        """
        Start the listener thread. This method *must* be called first in the main function,
        Otherwise, all the callback function will never get called.

        Examples:
            ```python
            if __name__ == "__main__":
                notifier.start()
                ...
            ```
        """
        if self._protocol_launched():  # call the callback directly
            self.callbacks.get(self.func_to_call)()

        else:
            self.listener.callbacks.update(self.callbacks)
            self.listener.thread.start()

Start the listener thread. This method must be called first in the main function, Otherwise, all the callback function will never get called.

Examples
if __name__ == "__main__":
    notifier.start()
    ...
#   def update(self):
View Source
    def update(self):
        """
        check for available callback function in queue then call it
        this method *must* be called *every time* in loop.

        If all callback functions don't need to run in main thread, calling this functions is *optional*

        Examples:
            ```python
            # the main loop
            while True:
                notifier.update()
                ...
            ```
        """
        if self._protocol_launched():
            return

        q = self.listener.queue
        try:
            func = q.get_nowait()
            if callable(func):
                func()
            else:
                print(f"{func.__name__} ")
        except queue.Empty:
            pass

check for available callback function in queue then call it this method must be called every time in loop.

If all callback functions don't need to run in main thread, calling this functions is optional

Examples
# the main loop
while True:
    notifier.update()
    ...
#   def register_callback(self, func=None, *, run_in_main_thread=False):
View Source
    def register_callback(self, func=None, *, run_in_main_thread=False):
        """
        A decorator to register a function to be used as a callback
        Args:
            func: the function to decorate
            run_in_main_thread: If True, the callback function will run in main thread

        Examples:
            ```python
            @notifier.register_callback
            def foo(): ...
            ```

        Returns:
            The registered function

        """
        def inner(f):
            if run_in_main_thread:
                f.rimt = run_in_main_thread
            self.callbacks[f.__name__] = f
            f.url = self.callback_to_url(f)
            return f

        if func is None:
            return inner
        else:
            return inner(func)

A decorator to register a function to be used as a callback

Args
  • func: the function to decorate
  • run_in_main_thread: If True, the callback function will run in main thread
Examples
@notifier.register_callback
def foo(): ...
Returns

The registered function

#   def callback_to_url(self, func: Callable) -> str:
View Source
    def callback_to_url(self, func: Callable) -> str:
        """
        Translate the registered callback function `func` to url notation.

        Args:
            func: The registered callback function

        Returns:
             url-notation string eg. `my-app-id:foo`, where **my-app-id** is the app id and **foo** is the function name

        """

        if callable(func) and func.__name__ in self.callbacks:
            url = format_name(self.app_id) + ":" + func.__name__
            return url

Translate the registered callback function func to url notation.

Args
  • func: The registered callback function
Returns

url-notation string eg. my-app-id:foo, where my-app-id is the app id and foo is the function name

#   def clear(self):
View Source
    def clear(self):
        """
        Clear all notification created by `Notifier` from action center

        """

        cmd = f"""\
        [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
        [Windows.UI.Notifications.ToastNotificationManager]::History.Clear('{self.app_id}')
        """
        _run_ps(command=cmd)

Clear all notification created by Notifier from action center

#   class Notification:
View Source
class Notification(object):
    def __init__(self,
                 app_id: str,
                 title: str,
                 msg: str = "",
                 icon: str = "",
                 duration: str = 'short',
                 launch: str = ''):
        """
        Construct a new notification

        Args:
            app_id: your app name, make it readable to your user. It can contain spaces, however special characters
                    (eg. é) are not supported.
            title: The heading of the toast.
            msg: The content/message of the toast.
            icon: An optional path to an image to display on the left of the title & message.
                  Make sure the path is absolute.
            duration: How long the toast should show up for (short/long), default is short.
            launch: The url or callback to launch (invoked when the user clicks the notification)

        Notes:
            If you want to pass a callback to `launch` parameter,
            please use `create_notification` from `Notifier` object

        Raises:
            ValueError: If the duration specified is not short or long
        """

        self.app_id = app_id
        self.title = title
        self.msg = msg
        self.icon = icon
        self.duration = duration
        self.launch = launch
        self.audio = audio.Silent
        self.tag = self.title
        self.group = self.app_id
        self.actions = []
        self.script = ""
        if duration not in ("short", "long"):
            raise ValueError("Duration is not 'short' or 'long'")

    def set_audio(self, sound: audio.Sound, loop: bool):
        """
        Set the audio for the notification

        Args:
            sound: The audio to play when the notification is showing. Choose one from `winotify.audio` module,
                   (eg. audio.Default). The default for all notification is silent.
            loop: If True, the audio will play indefinitely until user click or dismis the notification.

        """

        self.audio = '<audio src="{}" loop="{}" />'.format(sound, str(loop).lower())

    def add_actions(self, label: str, launch: Union[str, Callable] = ""):
        """
        Add buttons to the notification. Each notification can have 5 buttons max.

        Args:
            label: The label of the button
            launch: The url to launch when clicking the button, 'file:///' protocol is allowed. Or a registered
                    callback function

        Returns: None

        Notes:
            Register a callback function using `Notifier.register_callback()` decorator before passing it here

        Raises:
              ValueError: If the callback function is not registered
        """

        if callable(launch):
            if hasattr(launch, 'url'):
                url = launch.url
            else:
                raise ValueError(f"{launch} is not registered")
        else:
            url = launch

        xml = '<action activationType="protocol" content="{label}" arguments="{link}" />'
        if len(self.actions) < 5:
            self.actions.append(xml.format(label=label, link=url))

    def build(self):
        """
        This method is deprecated, call `Notification.show()` directly instead.

        Warnings:
            DeprecationWarning

        """
        import warnings
        warnings.warn("build method is deprecated, call show directly instead", DeprecationWarning)
        return self

    def show(self):
        """
        Show the toast
        """
        if self.actions:
            self.actions = '\n'.join(self.actions)
        else:
            self.actions = ''

        if self.audio == audio.Silent:
            self.audio = '<audio silent="true" />'

        if self.launch:
            self.launch = 'activationType="protocol" launch="{}"'.format(self.launch)

        self.script = TEMPLATE.format(**self.__dict__)

        _run_ps(command=self.script)
#   Notification( app_id: str, title: str, msg: str = '', icon: str = '', duration: str = 'short', launch: str = '' )
View Source
    def __init__(self,
                 app_id: str,
                 title: str,
                 msg: str = "",
                 icon: str = "",
                 duration: str = 'short',
                 launch: str = ''):
        """
        Construct a new notification

        Args:
            app_id: your app name, make it readable to your user. It can contain spaces, however special characters
                    (eg. é) are not supported.
            title: The heading of the toast.
            msg: The content/message of the toast.
            icon: An optional path to an image to display on the left of the title & message.
                  Make sure the path is absolute.
            duration: How long the toast should show up for (short/long), default is short.
            launch: The url or callback to launch (invoked when the user clicks the notification)

        Notes:
            If you want to pass a callback to `launch` parameter,
            please use `create_notification` from `Notifier` object

        Raises:
            ValueError: If the duration specified is not short or long
        """

        self.app_id = app_id
        self.title = title
        self.msg = msg
        self.icon = icon
        self.duration = duration
        self.launch = launch
        self.audio = audio.Silent
        self.tag = self.title
        self.group = self.app_id
        self.actions = []
        self.script = ""
        if duration not in ("short", "long"):
            raise ValueError("Duration is not 'short' or 'long'")

Construct a new notification

Args
  • app_id: your app name, make it readable to your user. It can contain spaces, however special characters (eg. é) are not supported.
  • title: The heading of the toast.
  • msg: The content/message of the toast.
  • icon: An optional path to an image to display on the left of the title & message. Make sure the path is absolute.
  • duration: How long the toast should show up for (short/long), default is short.
  • launch: The url or callback to launch (invoked when the user clicks the notification)
Notes

If you want to pass a callback to launch parameter, please use create_notification from Notifier object

Raises
  • ValueError: If the duration specified is not short or long
#   def set_audio(self, sound: winotify.audio.Sound, loop: bool):
View Source
    def set_audio(self, sound: audio.Sound, loop: bool):
        """
        Set the audio for the notification

        Args:
            sound: The audio to play when the notification is showing. Choose one from `winotify.audio` module,
                   (eg. audio.Default). The default for all notification is silent.
            loop: If True, the audio will play indefinitely until user click or dismis the notification.

        """

        self.audio = '<audio src="{}" loop="{}" />'.format(sound, str(loop).lower())

Set the audio for the notification

Args
  • sound: The audio to play when the notification is showing. Choose one from winotify.audio module, (eg. audio.Default). The default for all notification is silent.
  • loop: If True, the audio will play indefinitely until user click or dismis the notification.
#   def add_actions(self, label: str, launch: Union[str, Callable] = ''):
View Source
    def add_actions(self, label: str, launch: Union[str, Callable] = ""):
        """
        Add buttons to the notification. Each notification can have 5 buttons max.

        Args:
            label: The label of the button
            launch: The url to launch when clicking the button, 'file:///' protocol is allowed. Or a registered
                    callback function

        Returns: None

        Notes:
            Register a callback function using `Notifier.register_callback()` decorator before passing it here

        Raises:
              ValueError: If the callback function is not registered
        """

        if callable(launch):
            if hasattr(launch, 'url'):
                url = launch.url
            else:
                raise ValueError(f"{launch} is not registered")
        else:
            url = launch

        xml = '<action activationType="protocol" content="{label}" arguments="{link}" />'
        if len(self.actions) < 5:
            self.actions.append(xml.format(label=label, link=url))

Add buttons to the notification. Each notification can have 5 buttons max.

Args
  • label: The label of the button
  • launch: The url to launch when clicking the button, 'file:///' protocol is allowed. Or a registered callback function

Returns: None

Notes

Register a callback function using Notifier.register_callback() decorator before passing it here

Raises
  • ValueError: If the callback function is not registered
#   def build(self):
View Source
    def build(self):
        """
        This method is deprecated, call `Notification.show()` directly instead.

        Warnings:
            DeprecationWarning

        """
        import warnings
        warnings.warn("build method is deprecated, call show directly instead", DeprecationWarning)
        return self

This method is deprecated, call Notification.show() directly instead.

Warnings

DeprecationWarning

#   def show(self):
View Source
    def show(self):
        """
        Show the toast
        """
        if self.actions:
            self.actions = '\n'.join(self.actions)
        else:
            self.actions = ''

        if self.audio == audio.Silent:
            self.audio = '<audio silent="true" />'

        if self.launch:
            self.launch = 'activationType="protocol" launch="{}"'.format(self.launch)

        self.script = TEMPLATE.format(**self.__dict__)

        _run_ps(command=self.script)

Show the toast

#   class Registry:
View Source
class Registry:
    def __init__(self, app_id: str, executable=PY_EXE, script_path: str = '', *, force_override=False):
        """
        register app_id to Windows Registry as a protocol,
        eg. the app_id is "My Awesome App" can be called from browser or run.exe by typing "my-awesome-app:[Params]"
        Params can be a function name to call

        Args:
            app_id: your app name, make it readable to your user. It can contain spaces, however special characters
                    (eg. é) are not supported.
            executable: set the default interpreter or executable to run when a notification is clicked,
                        default is `PY_EXE` which is python.exe. To hide cmd flashing when a notification is clicked,
                        use `PYW_EXE`.
            script_path: The script path, usually `__file__`.
            force_override: If True, force replace the exists registry value in Windows Registry. Default is False.
                            Set it True if you want to change default interpreter or script path.

        Raises:
            InvalidKeyStructure: If `force_override` is True but the registry value is not created by winotify or
                                 the key structure is invalid.
        """

        self.app = format_name(app_id)
        self._key = SUBKEY.format(self.app)
        self.executable = executable
        self.path = script_path
        self._override = force_override

        self.reg = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
        self._register()

    def _validate_structure(self):
        key = winreg.OpenKey(self.reg, self._key)
        try:
            winreg.OpenKey(key, SHELLKEY).Close()
        except WindowsError:
            raise InvalidKeyStructure(f'The registry from "{self.app}" was not created by winotify or the structure '
                                      f'is invalid')

    def _key_exist(self) -> bool:
        try:
            winreg.OpenKey(HKEY, self._key).Close()
            return True
        except WindowsError:
            return False

    def _register(self):

        if self._key_exist() and self._override:
            self._validate_structure()  # validate

        key = winreg.CreateKey(self.reg, self._key)
        with key:
            winreg.SetValueEx(key, '', 0, winreg.REG_SZ, f"URL:{self.app}")
            winreg.SetValueEx(key, 'URL Protocol', 0, winreg.REG_SZ, '')
            subkey = winreg.CreateKey(key, SHELLKEY)
            with subkey:
                winreg.SetValueEx(subkey, '', 0, winreg.REG_SZ, f'{self.executable} {self.path} %1')
#   Registry( app_id: str, executable=PY_EXE, script_path: str = '', *, force_override=False )
View Source
    def __init__(self, app_id: str, executable=PY_EXE, script_path: str = '', *, force_override=False):
        """
        register app_id to Windows Registry as a protocol,
        eg. the app_id is "My Awesome App" can be called from browser or run.exe by typing "my-awesome-app:[Params]"
        Params can be a function name to call

        Args:
            app_id: your app name, make it readable to your user. It can contain spaces, however special characters
                    (eg. é) are not supported.
            executable: set the default interpreter or executable to run when a notification is clicked,
                        default is `PY_EXE` which is python.exe. To hide cmd flashing when a notification is clicked,
                        use `PYW_EXE`.
            script_path: The script path, usually `__file__`.
            force_override: If True, force replace the exists registry value in Windows Registry. Default is False.
                            Set it True if you want to change default interpreter or script path.

        Raises:
            InvalidKeyStructure: If `force_override` is True but the registry value is not created by winotify or
                                 the key structure is invalid.
        """

        self.app = format_name(app_id)
        self._key = SUBKEY.format(self.app)
        self.executable = executable
        self.path = script_path
        self._override = force_override

        self.reg = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
        self._register()

register app_id to Windows Registry as a protocol, eg. the app_id is "My Awesome App" can be called from browser or run.exe by typing "my-awesome-app:[Params]" Params can be a function name to call

Args
  • app_id: your app name, make it readable to your user. It can contain spaces, however special characters (eg. é) are not supported.
  • executable: set the default interpreter or executable to run when a notification is clicked, default is PY_EXE which is python.exe. To hide cmd flashing when a notification is clicked, use PYW_EXE.
  • script_path: The script path, usually __file__.
  • force_override: If True, force replace the exists registry value in Windows Registry. Default is False. Set it True if you want to change default interpreter or script path.
Raises
  • InvalidKeyStructure: If force_override is True but the registry value is not created by winotify or the key structure is invalid.
View Source
class Sound:
    def __init__(self, s):
        self.s = s

    def __str__(self):
        return self.s


Default = Sound("ms-winsoundevent:Notification.Default")
IM = Sound("ms-winsoundevent:Notification.IM")
Mail = Sound("ms-winsoundevent:Notification.Mail")
Reminder = Sound("ms-winsoundevent:Notification.Reminder")
SMS = Sound("ms-winsoundevent:Notification.SMS")
LoopingAlarm = Sound("ms-winsoundevent:Notification.Looping.Alarm")
LoopingAlarm2 = Sound("ms-winsoundevent:Notification.Looping.Alarm2")
LoopingAlarm3 = Sound("ms-winsoundevent:Notification.Looping.Alarm3")
LoopingAlarm4 = Sound("ms-winsoundevent:Notification.Looping.Alarm4")
LoopingAlarm6 = Sound("ms-winsoundevent:Notification.Looping.Alarm6")
LoopingAlarm8 = Sound("ms-winsoundevent:Notification.Looping.Alarm8")
LoopingAlarm9 = Sound("ms-winsoundevent:Notification.Looping.Alarm9")
LoopingAlarm10 = Sound("ms-winsoundevent:Notification.Looping.Alarm10")
LoopingCall = Sound("ms-winsoundevent:Notification.Looping.Call")
LoopingCall2 = Sound("ms-winsoundevent:Notification.Looping.Call2")
LoopingCall3 = Sound("ms-winsoundevent:Notification.Looping.Call3")
LoopingCall4 = Sound("ms-winsoundevent:Notification.Looping.Call4")
LoopingCall5 = Sound("ms-winsoundevent:Notification.Looping.Call5")
LoopingCall6 = Sound("ms-winsoundevent:Notification.Looping.Call6")
LoopingCall7 = Sound("ms-winsoundevent:Notification.Looping.Call7")
LoopingCall8 = Sound("ms-winsoundevent:Notification.Looping.Call8")
LoopingCall9 = Sound("ms-winsoundevent:Notification.Looping.Call9")
LoopingCall10 = Sound("ms-winsoundevent:Notification.Looping.Call10")
Silent = Sound("silent")