├── .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 | [](https://badge.fury.io/py/win11toast)
2 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
277 |
278 | ```python
279 | from win11toast import toast
280 |
281 | toast('Hello', 'Click a button', buttons=['Approve', 'Dismiss', 'Other'])
282 | ```
283 |
284 | 
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 | 
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 | 
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 | 
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 | 
335 |
336 | 
337 |
338 | ### No arguments
339 |
340 | ```python
341 | from win11toast import toast
342 |
343 | toast()
344 | ```
345 |
346 | 
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 | 
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 | 
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 | 
434 |
435 |
436 | [Notifications Visualizer](https://www.microsoft.com/store/apps/notifications-visualizer/9nblggh5xsl1)
437 | 
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 |
--------------------------------------------------------------------------------