├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── setup.py └── win11toast.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build-n-publish: 7 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@main 11 | - name: Set up Python 3.10 12 | uses: actions/setup-python@v3 13 | with: 14 | python-version: "3.10" 15 | - name: Install pypa/build 16 | run: python -m pip install build --user 17 | - name: Build a binary wheel and a source tarball 18 | run: python -m build 19 | - name: Publish a Python distribution to PyPI 20 | uses: pypa/gh-action-pypi-publish@release/v1 21 | with: 22 | password: ${{ secrets.PYPI_API_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 言葉 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Python](https://img.shields.io/pypi/pyversions/win11toast.svg)](https://badge.fury.io/py/win11toast) 2 | [![PyPI](https://badge.fury.io/py/win11toast.svg)](https://badge.fury.io/py/win11toast) 3 | 4 | # win11toast 5 | Toast notifications for Windows 10 and 11 based on [WinRT](https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts) 6 | 7 | ![image](https://user-images.githubusercontent.com/12811398/183295421-59686d68-bfb2-4d9d-ad61-afc8bd4e4808.png) 8 | 9 | ## Installation 10 | 11 | ```bash 12 | pip install win11toast 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```python 18 | from win11toast import toast 19 | 20 | toast('Hello Python🐍') 21 | ``` 22 | 23 | ![image](https://user-images.githubusercontent.com/12811398/183365362-dd163b1d-d01f-4b0e-9592-44bf63c6b4c2.png) 24 | 25 | https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text 26 | 27 | ### Body 28 | 29 | ```python 30 | from win11toast import toast 31 | 32 | toast('Hello Python', 'Click to open url', on_click='https://www.python.org') 33 | ``` 34 | 35 | ![image](https://user-images.githubusercontent.com/12811398/183651326-286e1ce2-b826-41d7-8829-c46d5b64fb37.png) 36 | 37 | ### Wrap text 38 | 39 | ```python 40 | from win11toast import toast 41 | 42 | toast('Hello', 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Earum accusantium porro numquam aspernatur voluptates cum, odio in, animi nihil cupiditate molestias laborum. Consequatur exercitationem modi vitae. In voluptates quia obcaecati!') 43 | ``` 44 | 45 | ![image](https://user-images.githubusercontent.com/12811398/183363789-e5a9c2bb-adf8-438d-9ebb-1e7693971a16.png) 46 | 47 | ### Run Python script on Click 48 | ```python 49 | from win11toast import toast 50 | 51 | toast('Hello Pythonista', 'Click to run python script', on_click=r'C:\Users\Admin\Downloads\handler.pyw') 52 | # {'arguments': 'C:\\Users\\Admin\\Downloads\\handler.pyw', 'user_input': {}} 53 | ``` 54 | 55 | Since the current directory when executing the script is `C:\Windows\system32`, use `os.chdir()` accordingly. 56 | 57 | e.g. [handler.pyw](https://gist.github.com/GitHub30/dae1b257c93d8315ea38554c9554a2ad) 58 | 59 | On Windows, you can run a Python script in the background using the pythonw.exe executable, which will run your program with no visible process or way to interact with it. 60 | 61 | https://stackoverflow.com/questions/9705982/pythonw-exe-or-python-exe 62 | 63 | ![2b53d4528ff94d40a0ab6da7c7fe33c6](https://user-images.githubusercontent.com/12811398/196034007-053ffe1c-4a95-441b-af06-ffc3753a1c64.gif) 64 | 65 | ### Callback 66 | 67 | ```python 68 | from win11toast import toast 69 | 70 | toast('Hello Python', 'Click to open url', on_click=lambda args: print('clicked!', args)) 71 | # clicked! {'arguments': 'http:', 'user_input': {}} 72 | ``` 73 | 74 | ### Icon 75 | 76 | ```python 77 | from win11toast import toast 78 | 79 | toast('Hello', 'Hello from Python', icon='https://unsplash.it/64?image=669') 80 | ``` 81 | 82 | ![image](https://user-images.githubusercontent.com/12811398/183359855-aa0a8d39-8249-4055-82cb-5968ab35e125.png) 83 | 84 | #### Square 85 | 86 | ```python 87 | from win11toast import toast 88 | 89 | icon = { 90 | 'src': 'https://unsplash.it/64?image=669', 91 | 'placement': 'appLogoOverride' 92 | } 93 | 94 | toast('Hello', 'Hello from Python', icon=icon) 95 | ``` 96 | 97 | ![image](https://user-images.githubusercontent.com/12811398/183659504-e83d1110-8f38-4f8e-81d6-b99ef9c4537c.png) 98 | 99 | ### Image 100 | 101 | ```python 102 | from win11toast import toast 103 | 104 | toast('Hello', 'Hello from Python', image='https://4.bp.blogspot.com/-u-uyq3FEqeY/UkJLl773BHI/AAAAAAAAYPQ/7bY05EeF1oI/s800/cooking_toaster.png') 105 | ``` 106 | 107 | ![image](https://user-images.githubusercontent.com/12811398/183360063-36caef94-bb3e-4eef-ac15-d5d6c86e5d40.png) 108 | 109 | https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-image 110 | 111 | #### Hero 112 | 113 | ```python 114 | from win11toast import toast 115 | 116 | image = { 117 | 'src': 'https://4.bp.blogspot.com/-u-uyq3FEqeY/UkJLl773BHI/AAAAAAAAYPQ/7bY05EeF1oI/s800/cooking_toaster.png', 118 | 'placement': 'hero' 119 | } 120 | 121 | toast('Hello', 'Hello from Python', image=image) 122 | ``` 123 | 124 | ![image](https://user-images.githubusercontent.com/12811398/183660596-8bff003e-af94-4554-b188-5946e9981723.png) 125 | 126 | ### Progress 127 | 128 | ```python 129 | from time import sleep 130 | from win11toast import notify, update_progress 131 | 132 | notify(progress={ 133 | 'title': 'YouTube', 134 | 'status': 'Downloading...', 135 | 'value': '0', 136 | 'valueStringOverride': '0/15 videos' 137 | }) 138 | 139 | for i in range(1, 15+1): 140 | sleep(1) 141 | update_progress({'value': i/15, 'valueStringOverride': f'{i}/15 videos'}) 142 | 143 | update_progress({'status': 'Completed!'}) 144 | ``` 145 | 146 | ![image](https://user-images.githubusercontent.com/12811398/183574436-05e3b504-bdec-46b1-a3f5-1ef861bb856a.png) 147 | 148 | Attributes 149 | https://docs.microsoft.com/en-ca/uwp/schemas/tiles/toastschema/element-progress 150 | 151 | https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-progress-bar 152 | 153 | ### Audio 154 | 155 | ```python 156 | from win11toast import toast 157 | 158 | toast('Hello', 'Hello from Python', audio='ms-winsoundevent:Notification.Looping.Alarm') 159 | ``` 160 | 161 | Available audio 162 | https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-audio 163 | 164 | ##### From URL 165 | 166 | ```python 167 | from win11toast import toast 168 | 169 | toast('Hello', 'Hello from Python', audio='https://nyanpass.com/nyanpass.mp3') 170 | ``` 171 | 172 | ##### From file 173 | 174 | ```python 175 | from win11toast import toast 176 | 177 | toast('Hello', 'Hello from Python', audio=r"C:\Users\Admin\Downloads\nyanpass.mp3") 178 | ``` 179 | 180 | I don't know how to add custom audio please help. 181 | 182 | https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/custom-audio-on-toasts 183 | 184 | #### Loop 185 | 186 | ```python 187 | from win11toast import toast 188 | 189 | toast('Hello', 'Hello from Python', audio={'loop': 'true'}) 190 | ``` 191 | 192 | ```python 193 | from win11toast import toast 194 | 195 | toast('Hello', 'Hello from Python', audio={'src': 'ms-winsoundevent:Notification.Looping.Alarm', 'loop': 'true'}) 196 | ``` 197 | 198 | ### Silent🤫 199 | 200 | ```python 201 | from win11toast import toast 202 | 203 | toast('Hello Python🐍', audio={'silent': 'true'}) 204 | ``` 205 | 206 | ### Speak🗣 207 | 208 | ```python 209 | from win11toast import toast 210 | 211 | toast('Hello Python🐍', dialogue='Hello world') 212 | ``` 213 | 214 | ### OCR👀 215 | 216 | ```python 217 | from win11toast import toast 218 | 219 | toast(ocr='https://i.imgur.com/oYojrJW.png') 220 | ``` 221 | 222 | ![image](https://user-images.githubusercontent.com/12811398/183774255-3b78723a-a1ea-4b67-8342-5edfbb329c24.png) 223 | 224 | ```python 225 | from win11toast import toast 226 | 227 | toast(ocr={'lang': 'ja', 'ocr': r'C:\Users\Admin\Downloads\hello.png'}) 228 | ``` 229 | 230 | ![image](https://user-images.githubusercontent.com/12811398/183774317-e1bc2454-3649-4573-9212-5f18d221c162.png) 231 | 232 | ### Long duration 233 | 234 | ```python 235 | from win11toast import toast 236 | 237 | toast('Hello Python🐍', duration='long') 238 | ``` 239 | 240 | displayed for 25 seconds 241 | https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-toast 242 | 243 | ### No timeout 244 | 245 | ```python 246 | from win11toast import toast 247 | 248 | toast('Hello Python🐍', scenario='incomingCall') 249 | ``` 250 | 251 | The scenario your toast is used for, like an alarm, reminder, incomingCall or urgent. 252 | 253 | https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-toast#:~:text=None-,scenario,-The%20scenario%20your 254 | 255 | ![image](https://github.com/GitHub30/win11toast/assets/12811398/f7e65d41-7bd6-4e64-82ff-2ed7eab82922) 256 | 257 | 258 | ### Button 259 | 260 | ```python 261 | from win11toast import toast 262 | 263 | toast('Hello', 'Hello from Python', button='Dismiss') 264 | # {'arguments': 'http:Dismiss', 'user_input': {}} 265 | ``` 266 | 267 | ![image](https://user-images.githubusercontent.com/12811398/183361855-1269d017-5354-41db-9613-20ad2f22447a.png) 268 | 269 | ```python 270 | from win11toast import toast 271 | 272 | toast('Hello', 'Hello from Python', button={'activationType': 'protocol', 'arguments': 'https://google.com', 'content': 'Open Google'}) 273 | # {'arguments': 'https://google.com', 'user_input': {}} 274 | ``` 275 | 276 | ![image](https://user-images.githubusercontent.com/12811398/183655824-ee2b9001-3808-45fd-b264-8c83b07aa4a2.png) 277 | 278 | ```python 279 | from win11toast import toast 280 | 281 | toast('Hello', 'Click a button', buttons=['Approve', 'Dismiss', 'Other']) 282 | ``` 283 | 284 | ![image](https://user-images.githubusercontent.com/12811398/183363035-af9e13cc-9bb1-4e25-90b3-9f6c1c00b3dd.png) 285 | 286 | https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action 287 | 288 | #### Play music or Open Explorer 289 | 290 | ```python 291 | from win11toast import toast 292 | 293 | buttons = [ 294 | {'activationType': 'protocol', 'arguments': 'C:\Windows\Media\Alarm01.wav', 'content': 'Play'}, 295 | {'activationType': 'protocol', 'arguments': 'file:///C:/Windows/Media', 'content': 'Open Folder'} 296 | ] 297 | 298 | toast('Music Player', 'Download Finished', buttons=buttons) 299 | ``` 300 | 301 | ![image](https://user-images.githubusercontent.com/12811398/183657915-1068c0d9-fc1a-4f6d-82c2-7835c3d9e585.png) 302 | 303 | ### Input 304 | 305 | ```python 306 | from win11toast import toast 307 | 308 | toast('Hello', 'Type anything', input='reply', button='Send') 309 | # {'arguments': 'http:Send', 'user_input': {'reply': 'Hi there'}} 310 | ``` 311 | 312 | ![image](https://user-images.githubusercontent.com/12811398/183361532-b554b9ae-e426-4fb1-8080-cc7c52d499d7.png) 313 | 314 | ```python 315 | from win11toast import toast 316 | 317 | toast('Hello', 'Type anything', input='reply', button={'activationType': 'protocol', 'arguments': 'http:', 'content': 'Send', 'hint-inputId': 'reply'}) 318 | # {'arguments': 'http:', 'user_input': {'reply': 'Hi there'}} 319 | ``` 320 | 321 | ![image](https://user-images.githubusercontent.com/12811398/183655443-340593e3-41ec-40b5-96a9-d7ba69fd10a2.png) 322 | 323 | https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input 324 | 325 | ### Selection 326 | 327 | ```python 328 | from win11toast import toast 329 | 330 | toast('Hello', 'Which do you like?', selection=['Apple', 'Banana', 'Grape'], button='Submit') 331 | # {'arguments': 'dismiss', 'user_input': {'selection': 'Grape'}} 332 | ``` 333 | 334 | ![image](https://user-images.githubusercontent.com/12811398/183361008-4cdd9445-683c-432e-8094-1c2193e959db.png) 335 | 336 | ![image](https://user-images.githubusercontent.com/12811398/183361138-2b81e8aa-bcbf-4764-a396-b7787518904b.png) 337 | 338 | ### No arguments 339 | 340 | ```python 341 | from win11toast import toast 342 | 343 | toast() 344 | ``` 345 | 346 | ![image](https://user-images.githubusercontent.com/12811398/183362441-8d865a74-f930-4c16-9757-22244d22a8e2.png) 347 | 348 | ### Non blocking 349 | 350 | ```python 351 | from win11toast import notify 352 | 353 | notify('Hello Python', 'Click to open url', on_click='https://www.python.org') 354 | ``` 355 | 356 | ### Async 357 | 358 | ```python 359 | from win11toast import toast_async 360 | 361 | async def main(): 362 | await toast_async('Hello Python', 'Click to open url', on_click='https://www.python.org') 363 | ``` 364 | 365 | ### Jupyter 366 | 367 | ```python 368 | from win11toast import notify 369 | 370 | notify('Hello Python', 'Click to open url', on_click='https://www.python.org') 371 | ``` 372 | 373 | ![image](https://user-images.githubusercontent.com/12811398/183650662-3a3f56f6-4a20-48f1-8649-155948aa21e0.png) 374 | 375 | ```python 376 | from win11toast import toast_async 377 | 378 | await toast_async('Hello Python', 'Click to open url', on_click='https://www.python.org') 379 | ``` 380 | 381 | ![image](https://user-images.githubusercontent.com/12811398/183295534-82b0a6d1-8fa6-4ddc-bfb0-5021158b3cb0.png) 382 | 383 | ```python 384 | import urllib.request 385 | from pathlib import Path 386 | src = str(Path(urllib.request.urlretrieve("https://i.imgur.com/p9dRdtP.jpg")[0]).absolute()) 387 | 388 | from win11toast import toast_async 389 | await toast_async('にゃんぱすー', audio='https://nyanpass.com/nyanpass.mp3', image={'src': src, 'placement':'hero'}) 390 | ``` 391 | 392 | ```python 393 | from win11toast import toast_async 394 | 395 | await toast_async('Hello Python🐍', dialogue='にゃんぱすー') 396 | ``` 397 | 398 | ## Debug 399 | 400 | ```python 401 | from win11toast import toast 402 | 403 | xml = """ 404 | 405 | 406 | 407 | 408 | Jill Bender 409 | Check out where we camped last weekend! It was incredible, wish you could have come on the backpacking trip! 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 425 | 426 | 427 | 428 | """ 429 | 430 | toast(xml=xml) 431 | ``` 432 | 433 | ![image](https://user-images.githubusercontent.com/12811398/183369144-5007e122-2325-49b3-97d8-100906cd6e56.png) 434 | 435 | 436 | [Notifications Visualizer](https://www.microsoft.com/store/apps/notifications-visualizer/9nblggh5xsl1) 437 | ![image](https://user-images.githubusercontent.com/12811398/183335533-33562c5c-d467-4acf-92a4-5e8f6ef05e1f.png) 438 | 439 | 440 | # Acknowledgements 441 | 442 | - [winsdk_toast](https://github.com/Mo-Dabao/winsdk_toast) 443 | - [Windows-Toasts](https://github.com/DatGuy1/Windows-Toasts) 444 | - [MarcAlx/notification.py](https://gist.github.com/MarcAlx/443358d5e7167864679ffa1b7d51cd06) 445 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='win11toast', 5 | version='0.35', 6 | description='Toast notifications for Windows 10 and 11', 7 | long_description=open('README.md', encoding='utf-8').read(), 8 | long_description_content_type='text/markdown', 9 | url='https://github.com/GitHub30/win11toast', 10 | project_urls={ 'Bug Tracker': 'https://github.com/GitHub30/win11toast/issues' }, 11 | author='Tomofumi Inoue', 12 | author_email='funaox@gmail.com', 13 | license='MIT', 14 | classifiers=[ 15 | 'Programming Language :: Python :: 3', 16 | 'License :: OSI Approved :: MIT License', 17 | "Topic :: Utilities", 18 | 'Operating System :: Microsoft', 19 | 'Operating System :: Microsoft :: Windows :: Windows 10', 20 | 'Operating System :: Microsoft :: Windows :: Windows 11' 21 | ], 22 | install_requires=['winsdk'], 23 | py_modules=['win11toast'] 24 | ) 25 | 26 | # Publish commands 27 | # https://packaging.python.org/tutorials/packaging-projects/ 28 | #pip install --upgrade pip build twine 29 | #python -m build 30 | #python -m twine upload dist/* 31 | -------------------------------------------------------------------------------- /win11toast.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | from winsdk.windows.data.xml.dom import XmlDocument 4 | from winsdk.windows.foundation import IPropertyValue 5 | from winsdk.windows.ui.notifications import ( 6 | ToastNotificationManager, 7 | ToastNotification, 8 | NotificationData, 9 | ToastActivatedEventArgs, 10 | ToastDismissedEventArgs, 11 | ToastFailedEventArgs 12 | ) 13 | 14 | DEFAULT_APP_ID = 'Python' 15 | 16 | xml = """ 17 | 18 | 19 | 20 | 21 | 22 | """ 23 | 24 | 25 | def set_attribute(document, xpath, name, value): 26 | attribute = document.create_attribute(name) 27 | attribute.value = value 28 | document.select_single_node(xpath).attributes.set_named_item(attribute) 29 | 30 | 31 | def add_text(msg, document): 32 | if isinstance(msg, str): 33 | msg = { 34 | 'text': msg 35 | } 36 | binding = document.select_single_node('//binding') 37 | text = document.create_element('text') 38 | for name, value in msg.items(): 39 | if name == 'text': 40 | text.inner_text = msg['text'] 41 | else: 42 | text.set_attribute(name, value) 43 | binding.append_child(text) 44 | 45 | 46 | def add_icon(icon, document): 47 | if isinstance(icon, str): 48 | icon = { 49 | 'placement': 'appLogoOverride', 50 | 'hint-crop': 'circle', 51 | 'src': icon 52 | } 53 | binding = document.select_single_node('//binding') 54 | image = document.create_element('image') 55 | for name, value in icon.items(): 56 | image.set_attribute(name, value) 57 | binding.append_child(image) 58 | 59 | 60 | def add_image(img, document): 61 | if isinstance(img, str): 62 | img = { 63 | 'src': img 64 | } 65 | binding = document.select_single_node('//binding') 66 | image = document.create_element('image') 67 | for name, value in img.items(): 68 | image.set_attribute(name, value) 69 | binding.append_child(image) 70 | 71 | 72 | def add_progress(prog, document): 73 | binding = document.select_single_node('//binding') 74 | progress = document.create_element('progress') 75 | for name in prog: 76 | progress.set_attribute(name, '{' + name + '}') 77 | binding.append_child(progress) 78 | 79 | 80 | def add_audio(aud, document): 81 | if isinstance(aud, str): 82 | aud = { 83 | 'src': aud 84 | } 85 | toast = document.select_single_node('/toast') 86 | audio = document.create_element('audio') 87 | for name, value in aud.items(): 88 | audio.set_attribute(name, value) 89 | toast.append_child(audio) 90 | 91 | 92 | def create_actions(document): 93 | toast = document.select_single_node('/toast') 94 | actions = document.create_element('actions') 95 | toast.append_child(actions) 96 | return actions 97 | 98 | 99 | def add_button(button, document): 100 | if isinstance(button, str): 101 | button = { 102 | 'activationType': 'protocol', 103 | 'arguments': 'http:' + button, 104 | 'content': button 105 | } 106 | actions = document.select_single_node( 107 | '//actions') or create_actions(document) 108 | action = document.create_element('action') 109 | for name, value in button.items(): 110 | action.set_attribute(name, value) 111 | actions.append_child(action) 112 | 113 | 114 | def add_input(id, document): 115 | if isinstance(id, str): 116 | id = { 117 | 'id': id, 118 | 'type': 'text', 119 | 'placeHolderContent': id 120 | } 121 | actions = document.select_single_node( 122 | '//actions') or create_actions(document) 123 | input = document.create_element('input') 124 | for name, value in id.items(): 125 | input.set_attribute(name, value) 126 | actions.append_child(input) 127 | 128 | 129 | def add_selection(selection, document): 130 | if isinstance(selection, list): 131 | selection = { 132 | 'input': { 133 | 'id': 'selection', 134 | 'type': 'selection' 135 | }, 136 | 'selection': selection 137 | } 138 | actions = document.select_single_node( 139 | '//actions') or create_actions(document) 140 | input = document.create_element('input') 141 | for name, value in selection['input'].items(): 142 | input.set_attribute(name, value) 143 | actions.append_child(input) 144 | for sel in selection['selection']: 145 | if isinstance(sel, str): 146 | sel = { 147 | 'id': sel, 148 | 'content': sel 149 | } 150 | selection_element = document.create_element('selection') 151 | for name, value in sel.items(): 152 | selection_element.set_attribute(name, value) 153 | input.append_child(selection_element) 154 | 155 | result = list() 156 | 157 | def result_wrapper(*args): 158 | global result 159 | result = args 160 | return result 161 | 162 | def activated_args(_, event): 163 | global result 164 | e = ToastActivatedEventArgs._from(event) 165 | user_input = dict([(name, IPropertyValue._from( 166 | e.user_input[name]).get_string()) for name in e.user_input]) 167 | result = { 168 | 'arguments': e.arguments, 169 | 'user_input': user_input 170 | } 171 | return result 172 | 173 | 174 | async def play_sound(audio): 175 | from winsdk.windows.media.core import MediaSource 176 | from winsdk.windows.media.playback import MediaPlayer 177 | 178 | if audio.startswith('http'): 179 | from winsdk.windows.foundation import Uri 180 | source = MediaSource.create_from_uri(Uri(audio)) 181 | else: 182 | from winsdk.windows.storage import StorageFile 183 | file = await StorageFile.get_file_from_path_async(audio) 184 | source = MediaSource.create_from_storage_file(file) 185 | 186 | player = MediaPlayer() 187 | player.source = source 188 | player.play() 189 | await asyncio.sleep(7) 190 | 191 | 192 | async def speak(text): 193 | from winsdk.windows.media.core import MediaSource 194 | from winsdk.windows.media.playback import MediaPlayer 195 | from winsdk.windows.media.speechsynthesis import SpeechSynthesizer 196 | 197 | # print(list(map(lambda info: info.description, SpeechSynthesizer.get_all_voices()))) 198 | 199 | stream = await SpeechSynthesizer().synthesize_text_to_stream_async(text) 200 | player = MediaPlayer() 201 | player.source = MediaSource.create_from_stream(stream, stream.content_type) 202 | player.play() 203 | await asyncio.sleep(7) 204 | 205 | 206 | async def recognize(ocr): 207 | from winsdk.windows.media.ocr import OcrEngine 208 | from winsdk.windows.graphics.imaging import BitmapDecoder 209 | if isinstance(ocr, str): 210 | ocr = {'ocr': ocr} 211 | if ocr['ocr'].startswith('http'): 212 | from winsdk.windows.foundation import Uri 213 | from winsdk.windows.storage.streams import RandomAccessStreamReference 214 | ref = RandomAccessStreamReference.create_from_uri(Uri(ocr['ocr'])) 215 | stream = await ref.open_read_async() 216 | else: 217 | from winsdk.windows.storage import StorageFile, FileAccessMode 218 | file = await StorageFile.get_file_from_path_async(ocr['ocr']) 219 | stream = await file.open_async(FileAccessMode.READ) 220 | decoder = await BitmapDecoder.create_async(stream) 221 | bitmap = await decoder.get_software_bitmap_async() 222 | if 'lang' in ocr: 223 | from winsdk.windows.globalization import Language 224 | if OcrEngine.is_language_supported(Language(ocr['lang'])): 225 | engine = OcrEngine.try_create_from_language(Language(ocr['lang'])) 226 | else: 227 | class UnsupportedOcrResult: 228 | def __init__(self): 229 | self.text = 'Please install. Get-WindowsCapability -Online -Name "Language.OCR*"' 230 | return UnsupportedOcrResult() 231 | else: 232 | engine = OcrEngine.try_create_from_user_profile_languages() 233 | # Avaliable properties (lines, angle, word, BoundingRect(x,y,width,height)) 234 | # https://docs.microsoft.com/en-us/uwp/api/windows.media.ocr.ocrresult?view=winrt-22621#properties 235 | return await engine.recognize_async(bitmap) 236 | 237 | 238 | def available_recognizer_languages(): 239 | from winsdk.windows.media.ocr import OcrEngine 240 | for language in OcrEngine.get_available_recognizer_languages(): 241 | print(language.display_name, language.language_tag) 242 | print('Run as Administrator') 243 | print('Get-WindowsCapability -Online -Name "Language.OCR*"') 244 | print('Add-WindowsCapability -Online -Name "Language.OCR~~~en-US~0.0.1.0"') 245 | 246 | 247 | def notify(title=None, body=None, on_click=print, icon=None, image=None, progress=None, audio=None, dialogue=None, duration=None, input=None, inputs=[], selection=None, selections=[], button=None, buttons=[], xml=xml, app_id=DEFAULT_APP_ID, scenario=None, tag=None, group=None): 248 | document = XmlDocument() 249 | document.load_xml(xml.format(scenario=scenario if scenario else 'default')) 250 | if isinstance(on_click, str): 251 | set_attribute(document, '/toast', 'launch', on_click) 252 | 253 | if duration: 254 | set_attribute(document, '/toast', 'duration', duration) 255 | 256 | if title: 257 | add_text(title, document) 258 | if body: 259 | add_text(body, document) 260 | if input: 261 | add_input(input, document) 262 | if inputs: 263 | for input in inputs: 264 | add_input(input, document) 265 | if selection: 266 | add_selection(selection, document) 267 | if selections: 268 | for selection in selections: 269 | add_selection(selection, document) 270 | if button: 271 | add_button(button, document) 272 | if buttons: 273 | for button in buttons: 274 | add_button(button, document) 275 | if icon: 276 | add_icon(icon, document) 277 | if image: 278 | add_image(image, document) 279 | if progress: 280 | add_progress(progress, document) 281 | if audio: 282 | if isinstance(audio, str) and audio.startswith('ms'): 283 | add_audio(audio, document) 284 | elif isinstance(audio, str) and (path := Path(audio)).is_file(): 285 | add_audio(f"file:///{path.absolute().as_posix()}", document) 286 | elif isinstance(audio, dict) and 'src' in audio and audio['src'].startswith('ms'): 287 | add_audio(audio, document) 288 | else: 289 | add_audio({'silent': 'true'}, document) 290 | if dialogue: 291 | add_audio({'silent': 'true'}, document) 292 | 293 | notification = ToastNotification(document) 294 | if progress: 295 | data = NotificationData() 296 | for name, value in progress.items(): 297 | data.values[name] = str(value) 298 | data.sequence_number = 1 299 | notification.data = data 300 | notification.tag = 'my_tag' 301 | if tag: 302 | notification.tag = tag 303 | if group: 304 | notification.group = group 305 | if app_id == DEFAULT_APP_ID: 306 | try: 307 | notifier = ToastNotificationManager.create_toast_notifier() 308 | except Exception as e: 309 | notifier = ToastNotificationManager.create_toast_notifier(app_id) 310 | else: 311 | notifier = ToastNotificationManager.create_toast_notifier(app_id) 312 | notifier.show(notification) 313 | return notification 314 | 315 | 316 | async def toast_async(title=None, body=None, on_click=print, icon=None, image=None, progress=None, audio=None, dialogue=None, duration=None, input=None, inputs=[], selection=None, selections=[], button=None, buttons=[], xml=xml, app_id=DEFAULT_APP_ID, ocr=None, on_dismissed=print, on_failed=print, scenario=None, tag=None, group=None): 317 | """ 318 | Notify 319 | Args: 320 | title: 321 | body: 322 | on_click: 323 | on_dismissed: 324 | on_failed: 325 | inputs: > ['textbox'] 326 | selections: > ['Apple', 'Banana', 'Grape'] 327 | actions: > ['Button'] 328 | icon: https://unsplash.it/64?image=669 329 | image: https://4.bp.blogspot.com/-u-uyq3FEqeY/UkJLl773BHI/AAAAAAAAYPQ/7bY05EeF1oI/s800/cooking_toaster.png 330 | audio: ms-winsoundevent:Notification.Looping.Alarm 331 | xml: 332 | 333 | Returns: 334 | None 335 | """ 336 | if ocr: 337 | title = 'OCR Result' 338 | body = (await recognize(ocr)).text 339 | src = ocr if isinstance(ocr, str) else ocr['ocr'] 340 | image = {'placement': 'hero', 'src': src} 341 | notification = notify(title, body, on_click, icon, image, 342 | progress, audio, dialogue, duration, input, inputs, selection, selections, button, buttons, xml, app_id, scenario, tag, group) 343 | loop = asyncio.get_running_loop() 344 | futures = [] 345 | 346 | if audio and isinstance(audio, str) and not audio.startswith('ms'): 347 | futures.append(loop.create_task(play_sound(audio))) 348 | if dialogue: 349 | futures.append(loop.create_task(speak(dialogue))) 350 | 351 | if isinstance(on_click, str): 352 | on_click = print 353 | activated_future = loop.create_future() 354 | activated_token = notification.add_activated( 355 | lambda *args: loop.call_soon_threadsafe( 356 | activated_future.set_result, on_click(activated_args(*args)) 357 | ) 358 | ) 359 | futures.append(activated_future) 360 | 361 | dismissed_future = loop.create_future() 362 | dismissed_token = notification.add_dismissed( 363 | lambda _, event_args: loop.call_soon_threadsafe( 364 | dismissed_future.set_result, on_dismissed(result_wrapper(ToastDismissedEventArgs._from(event_args).reason))) 365 | ) 366 | futures.append(dismissed_future) 367 | 368 | failed_future = loop.create_future() 369 | failed_token = notification.add_failed( 370 | lambda _, event_args: loop.call_soon_threadsafe( 371 | failed_future.set_result, on_failed(result_wrapper(ToastFailedEventArgs._from(event_args).error_code))) 372 | ) 373 | futures.append(failed_future) 374 | 375 | try: 376 | _, pending = await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED) 377 | for p in pending: 378 | p.cancel() 379 | finally: 380 | if activated_token is not None: 381 | notification.remove_activated(activated_token) 382 | if dismissed_token is not None: 383 | notification.remove_dismissed(dismissed_token) 384 | if failed_token is not None: 385 | notification.remove_failed(failed_token) 386 | return result 387 | 388 | 389 | def toast(*args, **kwargs): 390 | toast_coroutine = toast_async(*args, **kwargs) 391 | 392 | # check if there is an existing loop 393 | try: 394 | loop = asyncio.get_running_loop() 395 | except RuntimeError: 396 | return asyncio.run(toast_coroutine) 397 | else: 398 | future = asyncio.Future() 399 | task = loop.create_task(toast_coroutine) 400 | 401 | def on_done(t): 402 | if t.exception() is not None: 403 | future.set_exception(t.exception()) 404 | else: 405 | future.set_result(t.result()) 406 | 407 | task.add_done_callback(on_done) 408 | return future 409 | 410 | 411 | 412 | def update_progress(progress, app_id=DEFAULT_APP_ID, tag='my_tag'): 413 | data = NotificationData() 414 | for name, value in progress.items(): 415 | data.values[name] = str(value) 416 | data.sequence_number = 2 417 | if app_id == DEFAULT_APP_ID: 418 | try: 419 | notifier = ToastNotificationManager.create_toast_notifier() 420 | except Exception as e: 421 | notifier = ToastNotificationManager.create_toast_notifier(app_id) 422 | else: 423 | notifier = ToastNotificationManager.create_toast_notifier(app_id) 424 | return notifier.update(data, tag) 425 | 426 | 427 | def clear_toast(app_id=DEFAULT_APP_ID, tag=None, group=None): 428 | # Get the notification history 429 | history = ToastNotificationManager.history 430 | 431 | if tag is None and group is None: 432 | # Clear all notifications 433 | history.clear(app_id) 434 | elif tag is not None and not group: 435 | # Cannot remove notification only using tag. Group is required. 436 | raise AttributeError('group value is required to clear a toast') 437 | elif tag is not None and group is not None: 438 | # Remove notification by tag and group 439 | history.remove(tag, group, app_id) 440 | elif tag is None and group is not None: 441 | # Remove all notifications in the group 442 | history.remove_group(group, app_id) 443 | --------------------------------------------------------------------------------