shortcutkit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shortcutkit-0.1.0/LICENSE +21 -0
- shortcutkit-0.1.0/PKG-INFO +145 -0
- shortcutkit-0.1.0/README.md +124 -0
- shortcutkit-0.1.0/pyproject.toml +33 -0
- shortcutkit-0.1.0/setup.cfg +4 -0
- shortcutkit-0.1.0/src/shortcutkit/__init__.py +275 -0
- shortcutkit-0.1.0/src/shortcutkit/__main__.py +7 -0
- shortcutkit-0.1.0/src/shortcutkit/actions.py +1107 -0
- shortcutkit-0.1.0/src/shortcutkit/data/builtin-actions.json +18974 -0
- shortcutkit-0.1.0/src/shortcutkit/data/provenance.json +12 -0
- shortcutkit-0.1.0/src/shortcutkit.egg-info/PKG-INFO +145 -0
- shortcutkit-0.1.0/src/shortcutkit.egg-info/SOURCES.txt +12 -0
- shortcutkit-0.1.0/src/shortcutkit.egg-info/dependency_links.txt +1 -0
- shortcutkit-0.1.0/src/shortcutkit.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 frontboat
|
|
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.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shortcutkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build, validate and sign Apple Shortcuts (.shortcut) files.
|
|
5
|
+
Author: frontboat
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/frontboat/shortcutkit
|
|
8
|
+
Project-URL: Documentation, https://github.com/frontboat/shortcutkit/blob/main/docs/shortcut-file-format.md
|
|
9
|
+
Project-URL: Repository, https://github.com/frontboat/shortcutkit.git
|
|
10
|
+
Project-URL: Changelog, https://github.com/frontboat/shortcutkit/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: apple,shortcuts,workflow,automation,macos,ios,plist
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# shortcutkit
|
|
23
|
+
|
|
24
|
+
Build, validate and sign Apple Shortcuts (`.shortcut`) files from Python.
|
|
25
|
+
|
|
26
|
+
[](https://github.com/frontboat/shortcutkit/blob/main/LICENSE)
|
|
27
|
+
[](https://github.com/frontboat/shortcutkit/actions)
|
|
28
|
+
|
|
29
|
+
## About
|
|
30
|
+
|
|
31
|
+
Shortcuts has no public file-format specification and no public list of what its actions
|
|
32
|
+
accept. shortcutkit fills that gap with data extracted from WorkflowKit, the engine inside the
|
|
33
|
+
Shortcuts app: every one of the 339 built-in actions is bundled with its identifier, parameter
|
|
34
|
+
keys and the value kind each key accepts, and `action()` checks your parameters against them
|
|
35
|
+
before anything is written. Pure Python, no dependencies. Signing shells out to the macOS
|
|
36
|
+
`shortcuts` command; everything else runs anywhere.
|
|
37
|
+
|
|
38
|
+
A TypeScript package of the same name shares the same data and adds compile-time checking.
|
|
39
|
+
Both live at https://github.com/frontboat/shortcutkit.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Every built-in action, as a constant.** `actions.SETSTOREDCONTENT` and 338 more, so a
|
|
44
|
+
typo is an `AttributeError` rather than a silent bad file.
|
|
45
|
+
- **Parameters checked before you write anything.** `ACTIONS[identifier]` gives the name,
|
|
46
|
+
parameter keys and output name; `PARAM_KINDS[identifier]` gives the value kind each key
|
|
47
|
+
accepts. Unknown keys and wrong kinds raise `ValueError`. Attachments and token strings are
|
|
48
|
+
accepted wherever a plain value is, because that is how Shortcuts works.
|
|
49
|
+
- **Value helpers that match the engine's serialization.** `ref()` to another action's output,
|
|
50
|
+
`variable()`, `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `text()` for
|
|
51
|
+
strings with embedded references, and `picker()` for variable-picker parameters.
|
|
52
|
+
- **Control flow.** `if_()` / `otherwise()` / `end_if()` and `repeat_each()` /
|
|
53
|
+
`end_repeat_each()` manage the grouping identifiers for you.
|
|
54
|
+
- **App Intents from installed apps.** Any identifier outside the built-in set is accepted and
|
|
55
|
+
its parameters are passed through as given.
|
|
56
|
+
- **Reference tables.** `CONDITION` codes, `ICON_COLORS`, and `PROVENANCE` recording which
|
|
57
|
+
macOS and Shortcuts build the bundled data came from.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install shortcutkit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Requirements
|
|
66
|
+
|
|
67
|
+
- Python 3.9 or newer.
|
|
68
|
+
- A Mac signed into iCloud for `Shortcut.sign()`. It runs `shortcuts sign`, which refuses to
|
|
69
|
+
work without an iCloud login even in `anyone` mode. Building and writing the unsigned file
|
|
70
|
+
works on any platform.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from shortcutkit import Shortcut, actions, ref, text
|
|
76
|
+
|
|
77
|
+
s = Shortcut("Greeting", color="Teal")
|
|
78
|
+
got = s.action(actions.GETSTOREDCONTENT, WFStoredContentKey="greeting")
|
|
79
|
+
s.action(actions.SHOWRESULT, Text=text("Stored: ", ref(got)))
|
|
80
|
+
s.write() # Greeting.shortcut (unsigned)
|
|
81
|
+
Shortcut.sign("Greeting.shortcut", "Greeting-signed.shortcut") # macOS
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Open the signed file and Shortcuts imports it.
|
|
85
|
+
|
|
86
|
+
### Control flow
|
|
87
|
+
|
|
88
|
+
`if_()` returns a grouping identifier that the matching `otherwise()` and `end_if()` calls
|
|
89
|
+
take back:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
gid = s.if_(ref(got), "has_any_value")
|
|
93
|
+
s.action(actions.SHOWRESULT, Text=text("Stored value: ", ref(got)))
|
|
94
|
+
s.otherwise(gid)
|
|
95
|
+
s.action(actions.SHOWRESULT, Text=text("Nothing stored"))
|
|
96
|
+
s.end_if(gid)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Actions from other apps
|
|
100
|
+
|
|
101
|
+
App Intents actions are not in the catalogue, so pass the identifier as a string:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
s.action("com.example.app.CreateNote", title=text("Hello"), body=ref(got))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Demo
|
|
108
|
+
|
|
109
|
+
The package ships a demo that builds a working shortcut with storage, an If/Otherwise block
|
|
110
|
+
and output, then signs it:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python -m shortcutkit demo out.shortcut
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Documentation
|
|
117
|
+
|
|
118
|
+
- [Shortcut file format](https://github.com/frontboat/shortcutkit/blob/main/docs/shortcut-file-format.md):
|
|
119
|
+
the `.shortcut` format end to end, every field.
|
|
120
|
+
- [Built-in actions reference](https://github.com/frontboat/shortcutkit/blob/main/docs/builtin-actions-reference.md):
|
|
121
|
+
all 339 built-in actions with their parameters.
|
|
122
|
+
- [Parameter encodings](https://github.com/frontboat/shortcutkit/blob/main/docs/parameter-encodings.md):
|
|
123
|
+
how each parameter class is serialized.
|
|
124
|
+
- [Extraction notes](https://github.com/frontboat/shortcutkit/blob/main/docs/extraction.md):
|
|
125
|
+
how the data was obtained and what remains approximate.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
From a checkout of the repository:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/frontboat/shortcutkit.git && cd shortcutkit/python
|
|
133
|
+
uv venv .venv && uv pip install -e . --python .venv/bin/python # or: python3 -m venv .venv && .venv/bin/pip install -e .
|
|
134
|
+
.venv/bin/python -m shortcutkit demo /tmp/Demo.shortcut
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`actions.py` and `data/` are generated by the repository's extraction tools from the Shortcuts
|
|
138
|
+
engine on a Mac. Do not edit them; change the generator or the data and run `bun run extract`
|
|
139
|
+
at the repository root. The Python and TypeScript packages are versioned together with the
|
|
140
|
+
data, and are kept in feature parity.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
shortcutkit is licensed under the MIT license. See
|
|
145
|
+
[LICENSE](https://github.com/frontboat/shortcutkit/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# shortcutkit
|
|
2
|
+
|
|
3
|
+
Build, validate and sign Apple Shortcuts (`.shortcut`) files from Python.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/frontboat/shortcutkit/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/frontboat/shortcutkit/actions)
|
|
7
|
+
|
|
8
|
+
## About
|
|
9
|
+
|
|
10
|
+
Shortcuts has no public file-format specification and no public list of what its actions
|
|
11
|
+
accept. shortcutkit fills that gap with data extracted from WorkflowKit, the engine inside the
|
|
12
|
+
Shortcuts app: every one of the 339 built-in actions is bundled with its identifier, parameter
|
|
13
|
+
keys and the value kind each key accepts, and `action()` checks your parameters against them
|
|
14
|
+
before anything is written. Pure Python, no dependencies. Signing shells out to the macOS
|
|
15
|
+
`shortcuts` command; everything else runs anywhere.
|
|
16
|
+
|
|
17
|
+
A TypeScript package of the same name shares the same data and adds compile-time checking.
|
|
18
|
+
Both live at https://github.com/frontboat/shortcutkit.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Every built-in action, as a constant.** `actions.SETSTOREDCONTENT` and 338 more, so a
|
|
23
|
+
typo is an `AttributeError` rather than a silent bad file.
|
|
24
|
+
- **Parameters checked before you write anything.** `ACTIONS[identifier]` gives the name,
|
|
25
|
+
parameter keys and output name; `PARAM_KINDS[identifier]` gives the value kind each key
|
|
26
|
+
accepts. Unknown keys and wrong kinds raise `ValueError`. Attachments and token strings are
|
|
27
|
+
accepted wherever a plain value is, because that is how Shortcuts works.
|
|
28
|
+
- **Value helpers that match the engine's serialization.** `ref()` to another action's output,
|
|
29
|
+
`variable()`, `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `text()` for
|
|
30
|
+
strings with embedded references, and `picker()` for variable-picker parameters.
|
|
31
|
+
- **Control flow.** `if_()` / `otherwise()` / `end_if()` and `repeat_each()` /
|
|
32
|
+
`end_repeat_each()` manage the grouping identifiers for you.
|
|
33
|
+
- **App Intents from installed apps.** Any identifier outside the built-in set is accepted and
|
|
34
|
+
its parameters are passed through as given.
|
|
35
|
+
- **Reference tables.** `CONDITION` codes, `ICON_COLORS`, and `PROVENANCE` recording which
|
|
36
|
+
macOS and Shortcuts build the bundled data came from.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install shortcutkit
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Requirements
|
|
45
|
+
|
|
46
|
+
- Python 3.9 or newer.
|
|
47
|
+
- A Mac signed into iCloud for `Shortcut.sign()`. It runs `shortcuts sign`, which refuses to
|
|
48
|
+
work without an iCloud login even in `anyone` mode. Building and writing the unsigned file
|
|
49
|
+
works on any platform.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from shortcutkit import Shortcut, actions, ref, text
|
|
55
|
+
|
|
56
|
+
s = Shortcut("Greeting", color="Teal")
|
|
57
|
+
got = s.action(actions.GETSTOREDCONTENT, WFStoredContentKey="greeting")
|
|
58
|
+
s.action(actions.SHOWRESULT, Text=text("Stored: ", ref(got)))
|
|
59
|
+
s.write() # Greeting.shortcut (unsigned)
|
|
60
|
+
Shortcut.sign("Greeting.shortcut", "Greeting-signed.shortcut") # macOS
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open the signed file and Shortcuts imports it.
|
|
64
|
+
|
|
65
|
+
### Control flow
|
|
66
|
+
|
|
67
|
+
`if_()` returns a grouping identifier that the matching `otherwise()` and `end_if()` calls
|
|
68
|
+
take back:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
gid = s.if_(ref(got), "has_any_value")
|
|
72
|
+
s.action(actions.SHOWRESULT, Text=text("Stored value: ", ref(got)))
|
|
73
|
+
s.otherwise(gid)
|
|
74
|
+
s.action(actions.SHOWRESULT, Text=text("Nothing stored"))
|
|
75
|
+
s.end_if(gid)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Actions from other apps
|
|
79
|
+
|
|
80
|
+
App Intents actions are not in the catalogue, so pass the identifier as a string:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
s.action("com.example.app.CreateNote", title=text("Hello"), body=ref(got))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Demo
|
|
87
|
+
|
|
88
|
+
The package ships a demo that builds a working shortcut with storage, an If/Otherwise block
|
|
89
|
+
and output, then signs it:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -m shortcutkit demo out.shortcut
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
|
|
97
|
+
- [Shortcut file format](https://github.com/frontboat/shortcutkit/blob/main/docs/shortcut-file-format.md):
|
|
98
|
+
the `.shortcut` format end to end, every field.
|
|
99
|
+
- [Built-in actions reference](https://github.com/frontboat/shortcutkit/blob/main/docs/builtin-actions-reference.md):
|
|
100
|
+
all 339 built-in actions with their parameters.
|
|
101
|
+
- [Parameter encodings](https://github.com/frontboat/shortcutkit/blob/main/docs/parameter-encodings.md):
|
|
102
|
+
how each parameter class is serialized.
|
|
103
|
+
- [Extraction notes](https://github.com/frontboat/shortcutkit/blob/main/docs/extraction.md):
|
|
104
|
+
how the data was obtained and what remains approximate.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
From a checkout of the repository:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/frontboat/shortcutkit.git && cd shortcutkit/python
|
|
112
|
+
uv venv .venv && uv pip install -e . --python .venv/bin/python # or: python3 -m venv .venv && .venv/bin/pip install -e .
|
|
113
|
+
.venv/bin/python -m shortcutkit demo /tmp/Demo.shortcut
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`actions.py` and `data/` are generated by the repository's extraction tools from the Shortcuts
|
|
117
|
+
engine on a Mac. Do not edit them; change the generator or the data and run `bun run extract`
|
|
118
|
+
at the repository root. The Python and TypeScript packages are versioned together with the
|
|
119
|
+
data, and are kept in feature parity.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
shortcutkit is licensed under the MIT license. See
|
|
124
|
+
[LICENSE](https://github.com/frontboat/shortcutkit/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shortcutkit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Build, validate and sign Apple Shortcuts (.shortcut) files."
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "frontboat" }]
|
|
14
|
+
keywords = ["apple", "shortcuts", "workflow", "automation", "macos", "ios", "plist"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/frontboat/shortcutkit"
|
|
25
|
+
Documentation = "https://github.com/frontboat/shortcutkit/blob/main/docs/shortcut-file-format.md"
|
|
26
|
+
Repository = "https://github.com/frontboat/shortcutkit.git"
|
|
27
|
+
Changelog = "https://github.com/frontboat/shortcutkit/blob/main/CHANGELOG.md"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
shortcutkit = ["data/*.json"]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""shortcutkit: build, validate and sign Apple Shortcuts files from Python.
|
|
3
|
+
|
|
4
|
+
The file format is the one Shortcuts.app uses (verified against Apple's gallery
|
|
5
|
+
workflows in WorkflowKit and against this Mac's library). Actions and parameter keys
|
|
6
|
+
are validated against _builtin-actions.json produced by extract-builtin-actions.sh.
|
|
7
|
+
|
|
8
|
+
from shortcutkit import Shortcut, ref, text, ask, shortcut_input, clipboard, variable
|
|
9
|
+
|
|
10
|
+
s = Shortcut("Demo", color="Teal", glyph=0xF000)
|
|
11
|
+
stored = s.action("is.workflow.actions.setstoredcontent",
|
|
12
|
+
WFInput="hello", WFStoredContentKey="greeting", WFStoredContentGlobalValue=False)
|
|
13
|
+
got = s.action("is.workflow.actions.getstoredcontent", WFStoredContentKey="greeting")
|
|
14
|
+
s.action("is.workflow.actions.showresult", Text=text("Stored: ", ref(got)))
|
|
15
|
+
s.write("Demo.shortcut") # unsigned plist
|
|
16
|
+
s.sign("Demo.shortcut", "Demo-signed.shortcut") # runs `shortcuts sign --mode anyone`
|
|
17
|
+
|
|
18
|
+
Command line: python -m shortcutkit demo OUT.shortcut (writes and signs the demo above)
|
|
19
|
+
|
|
20
|
+
Value helpers return the exact structures WorkflowKit's state classes serialize:
|
|
21
|
+
ref(action) WFTextTokenAttachment -> that action's output
|
|
22
|
+
variable("Name") WFTextTokenAttachment -> a named variable
|
|
23
|
+
shortcut_input() WFTextTokenAttachment -> the shortcut's input
|
|
24
|
+
clipboard() WFTextTokenAttachment -> clipboard
|
|
25
|
+
current_date() WFTextTokenAttachment -> current date
|
|
26
|
+
ask("Prompt") WFTextTokenAttachment -> ask each time
|
|
27
|
+
text("a ", ref(x)) WFTextTokenString -> text with embedded references
|
|
28
|
+
picker(att) {"Type": "Variable", "Variable": att} (variable-picker parameters)
|
|
29
|
+
Plain str / int / float / bool / list are written as-is.
|
|
30
|
+
"""
|
|
31
|
+
import json
|
|
32
|
+
import pathlib
|
|
33
|
+
import plistlib
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
import uuid
|
|
37
|
+
|
|
38
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
39
|
+
DEFINITIONS = HERE / "data" / "builtin-actions.json"
|
|
40
|
+
|
|
41
|
+
# WFWorkflowIcon.backgroundColorValue for palette colors 0-14, as the unsigned 32-bit
|
|
42
|
+
# value the plist stores. Names follow Shortcuts' picker order.
|
|
43
|
+
ICON_COLORS = {
|
|
44
|
+
"Red": 4282601983, "DarkOrange": 4251333119, "Orange": 4271458815, "Yellow": 4274264319,
|
|
45
|
+
"Green": 4292093695, "Teal": 431817727, "LightBlue": 1440408063, "Blue": 463140863,
|
|
46
|
+
"DarkBlue": 946986751, "Violet": 2071128575, "Purple": 3679049983, "Pink": 3980825855,
|
|
47
|
+
"Taupe": 255, "Gray": 3031607807, "DarkGray": 2846468607,
|
|
48
|
+
}
|
|
49
|
+
DEFAULT_GLYPH = 61440 # +[WFWorkflowIcon defaultGlyphCharacter]
|
|
50
|
+
|
|
51
|
+
# Legacy WFCondition codes for is.workflow.actions.conditional. Shortcuts still reads these
|
|
52
|
+
# and migrates them to the modern WFConditions template on load. Codes marked (v) were
|
|
53
|
+
# verified against shortcuts on this Mac or Apple's gallery; the rest follow the same
|
|
54
|
+
# enumeration as documented by the community (shortcuts-js, Cherri).
|
|
55
|
+
CONDITION = {
|
|
56
|
+
"less_than": 0, # (v) numbers
|
|
57
|
+
"less_or_equal": 1,
|
|
58
|
+
"greater_than": 2, # (v) numbers
|
|
59
|
+
"greater_or_equal": 3,
|
|
60
|
+
"is": 4,
|
|
61
|
+
"is_not": 5,
|
|
62
|
+
"begins_with": 8,
|
|
63
|
+
"ends_with": 9,
|
|
64
|
+
"contains": 99, # (v) as filter Operator in the gallery
|
|
65
|
+
"has_any_value": 100, # (v) supportedComparisonOperators of every subject state
|
|
66
|
+
"has_no_value": 101, # (v)
|
|
67
|
+
"does_not_contain": 999,
|
|
68
|
+
"is_between": 1003,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Keys Shortcuts still accepts on load even though the current definition no longer lists
|
|
73
|
+
# them. Legacy If: single condition on WFInput, migrated to WFConditions by the app.
|
|
74
|
+
LEGACY_KEYS = {
|
|
75
|
+
"is.workflow.actions.conditional": {"WFInput", "WFCondition", "WFConditionalActionString", "WFNumberValue",
|
|
76
|
+
"WFConditionalLegacyComparisonBehavior", "WFEnumerationValue", "WFBooleanValue",
|
|
77
|
+
"WFDate", "WFAnotherDate", "WFDuration", "WFConditions"},
|
|
78
|
+
"is.workflow.actions.choosefrommenu": {"WFMenuPrompt", "WFMenuItems", "WFMenuItemTitle", "WFMenuItemAttributedTitle"},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_reference(value):
|
|
83
|
+
"""Attachments, token strings and picker wrappers are accepted wherever a plain value is."""
|
|
84
|
+
return isinstance(value, dict) and ("WFSerializationType" in value or value.get("Type") == "Variable")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _check_kind(kind, value):
|
|
88
|
+
"""Run-time counterpart of the TypeScript ParamTypes: returns a problem string or None."""
|
|
89
|
+
if _is_reference(value):
|
|
90
|
+
return None
|
|
91
|
+
if kind == "bool" and not isinstance(value, bool):
|
|
92
|
+
return f"expected a bool or a reference, got {type(value).__name__}"
|
|
93
|
+
if kind in ("number", "plainNumber") and (isinstance(value, bool) or not isinstance(value, (int, float))):
|
|
94
|
+
return f"expected a number{'' if kind == 'plainNumber' else ' or a reference'}, got {type(value).__name__}"
|
|
95
|
+
if kind in ("string", "text", "plainString") and not isinstance(value, str):
|
|
96
|
+
return f"expected a string{'' if kind == 'plainString' else ' or a reference'}, got {type(value).__name__}"
|
|
97
|
+
if kind == "picker":
|
|
98
|
+
return "expected picker(<attachment>) or an attachment"
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _attachment(value):
|
|
103
|
+
return {"WFSerializationType": "WFTextTokenAttachment", "Value": value}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def ref(action, output_name=None):
|
|
107
|
+
"""Reference another action's output. `action` is the dict returned by Shortcut.action()."""
|
|
108
|
+
return _attachment({"Type": "ActionOutput", "OutputUUID": action["WFWorkflowActionParameters"]["UUID"],
|
|
109
|
+
"OutputName": output_name or action.get("_outputName", "Output")})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def variable(name):
|
|
113
|
+
return _attachment({"Type": "Variable", "VariableName": name})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def shortcut_input():
|
|
117
|
+
return _attachment({"Type": "ExtensionInput"})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def clipboard():
|
|
121
|
+
return _attachment({"Type": "Clipboard"})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def current_date():
|
|
125
|
+
return _attachment({"Type": "CurrentDate"})
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def ask(prompt=None):
|
|
129
|
+
return _attachment({"Type": "Ask", **({"Prompt": prompt} if prompt else {})})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def picker(attachment):
|
|
133
|
+
"""Wrap an attachment for WFVariablePickerParameter keys (e.g. WFInput on Repeat with Each)."""
|
|
134
|
+
return {"Type": "Variable", "Variable": attachment}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def text(*parts):
|
|
138
|
+
"""A WFTextTokenString: strings and attachments interleaved. Each attachment becomes U+FFFC."""
|
|
139
|
+
string = ""
|
|
140
|
+
attachments = {}
|
|
141
|
+
for part in parts:
|
|
142
|
+
if isinstance(part, dict) and part.get("WFSerializationType") == "WFTextTokenAttachment":
|
|
143
|
+
# NSRange offsets are UTF-16 code units, not code points.
|
|
144
|
+
attachments[f"{{{len(string.encode('utf-16-le')) // 2}, 1}}"] = part["Value"]
|
|
145
|
+
string += ""
|
|
146
|
+
else:
|
|
147
|
+
string += str(part)
|
|
148
|
+
return {"WFSerializationType": "WFTextTokenString", "Value": {"string": string, "attachmentsByRange": attachments}}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Shortcut:
|
|
152
|
+
def __init__(self, name, color="Blue", glyph=DEFAULT_GLYPH, input_classes=None, definitions=DEFINITIONS):
|
|
153
|
+
self.name = name
|
|
154
|
+
self.color = ICON_COLORS[color] if isinstance(color, str) else color
|
|
155
|
+
self.glyph = glyph
|
|
156
|
+
self.input_classes = input_classes or []
|
|
157
|
+
self.actions = []
|
|
158
|
+
self.defs = json.load(open(definitions)) if pathlib.Path(definitions).exists() else {}
|
|
159
|
+
|
|
160
|
+
def action(self, identifier, **params):
|
|
161
|
+
"""Append an action. Unknown identifiers or parameter keys raise ValueError."""
|
|
162
|
+
definition = self.defs.get(identifier)
|
|
163
|
+
if self.defs and definition is None and identifier.startswith("is.workflow."):
|
|
164
|
+
raise ValueError(f"unknown built-in action {identifier}")
|
|
165
|
+
if definition:
|
|
166
|
+
known = {p.get("Key") for p in definition.get("Parameters", []) if isinstance(p, dict)}
|
|
167
|
+
known |= {"UUID", "GroupingIdentifier", "WFControlFlowMode", "CustomOutputName"}
|
|
168
|
+
known |= LEGACY_KEYS.get(identifier, set())
|
|
169
|
+
inp = definition.get("Input")
|
|
170
|
+
if isinstance(inp, dict) and inp.get("ParameterKey"):
|
|
171
|
+
known.add(inp["ParameterKey"])
|
|
172
|
+
unknown = set(params) - known
|
|
173
|
+
if unknown and known - {"UUID", "GroupingIdentifier", "WFControlFlowMode", "CustomOutputName"}:
|
|
174
|
+
raise ValueError(f"{identifier}: unknown parameter(s) {sorted(unknown)}; known: {sorted(known)}")
|
|
175
|
+
for key, value in params.items():
|
|
176
|
+
problem = _check_kind(PARAM_KINDS.get(identifier, {}).get(key, "any"), value)
|
|
177
|
+
if problem:
|
|
178
|
+
raise ValueError(f"{identifier}.{key}: {problem}")
|
|
179
|
+
entry = {"WFWorkflowActionIdentifier": identifier,
|
|
180
|
+
"WFWorkflowActionParameters": {"UUID": str(uuid.uuid4()).upper(), **params}}
|
|
181
|
+
output = (definition or {}).get("Output", {})
|
|
182
|
+
entry["_outputName"] = output.get("OutputName") if isinstance(output, dict) else None
|
|
183
|
+
if isinstance(entry["_outputName"], dict):
|
|
184
|
+
entry["_outputName"] = entry["_outputName"].get("format")
|
|
185
|
+
self.actions.append(entry)
|
|
186
|
+
return entry
|
|
187
|
+
|
|
188
|
+
# Control flow: each block is a group sharing a GroupingIdentifier, with
|
|
189
|
+
# WFControlFlowMode 0 = start, 1 = middle (Otherwise / menu item), 2 = end.
|
|
190
|
+
def if_(self, subject, condition, value=None):
|
|
191
|
+
gid = str(uuid.uuid4()).upper()
|
|
192
|
+
params = {"GroupingIdentifier": gid, "WFControlFlowMode": 0, "WFInput": picker(subject),
|
|
193
|
+
"WFCondition": CONDITION[condition] if isinstance(condition, str) else condition}
|
|
194
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
195
|
+
params["WFNumberValue"] = value
|
|
196
|
+
elif value is not None:
|
|
197
|
+
params["WFConditionalActionString"] = value
|
|
198
|
+
self.action("is.workflow.actions.conditional", **params)
|
|
199
|
+
return gid
|
|
200
|
+
|
|
201
|
+
def otherwise(self, gid):
|
|
202
|
+
self.action("is.workflow.actions.conditional", GroupingIdentifier=gid, WFControlFlowMode=1)
|
|
203
|
+
|
|
204
|
+
def end_if(self, gid):
|
|
205
|
+
self.action("is.workflow.actions.conditional", GroupingIdentifier=gid, WFControlFlowMode=2)
|
|
206
|
+
|
|
207
|
+
def repeat_each(self, items):
|
|
208
|
+
"""Open a Repeat with Each block; returns the grouping identifier to pass to end_repeat_each()."""
|
|
209
|
+
gid = str(uuid.uuid4()).upper()
|
|
210
|
+
self.action("is.workflow.actions.repeat.each", GroupingIdentifier=gid, WFControlFlowMode=0, WFInput=picker(items))
|
|
211
|
+
return gid
|
|
212
|
+
|
|
213
|
+
def end_repeat_each(self, gid):
|
|
214
|
+
self.action("is.workflow.actions.repeat.each", GroupingIdentifier=gid, WFControlFlowMode=2)
|
|
215
|
+
|
|
216
|
+
def to_plist(self):
|
|
217
|
+
actions = []
|
|
218
|
+
for a in self.actions:
|
|
219
|
+
actions.append({k: v for k, v in a.items() if not k.startswith("_")})
|
|
220
|
+
return {
|
|
221
|
+
"WFWorkflowClientVersion": "4018.0.4",
|
|
222
|
+
"WFWorkflowMinimumClientVersion": 900,
|
|
223
|
+
"WFWorkflowMinimumClientVersionString": "900",
|
|
224
|
+
"WFWorkflowIcon": {"WFWorkflowIconStartColor": self.color, "WFWorkflowIconGlyphNumber": self.glyph},
|
|
225
|
+
"WFWorkflowTypes": [],
|
|
226
|
+
"WFQuickActionSurfaces": [],
|
|
227
|
+
"WFWorkflowInputContentItemClasses": self.input_classes,
|
|
228
|
+
"WFWorkflowOutputContentItemClasses": [],
|
|
229
|
+
"WFWorkflowImportQuestions": [],
|
|
230
|
+
"WFWorkflowHasShortcutInputVariables": any(
|
|
231
|
+
json.dumps(a).find('"ExtensionInput"') >= 0 for a in actions),
|
|
232
|
+
"WFWorkflowHasOutputFallback": False,
|
|
233
|
+
"WFWorkflowActions": actions,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
def write(self, path=None):
|
|
237
|
+
"""Write the unsigned plist. Shortcuts names an imported shortcut after its file, so the
|
|
238
|
+
default file name is the shortcut's name."""
|
|
239
|
+
path = pathlib.Path(path) if path else pathlib.Path(f"{self.name}.shortcut")
|
|
240
|
+
with open(path, "wb") as f:
|
|
241
|
+
plistlib.dump(self.to_plist(), f, fmt=plistlib.FMT_BINARY)
|
|
242
|
+
return path
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def sign(src, dst, mode="anyone"):
|
|
246
|
+
subprocess.run(["shortcuts", "sign", "--mode", mode, "--input", str(src), "--output", str(dst)], check=True)
|
|
247
|
+
return dst
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def demo(out):
|
|
251
|
+
out = pathlib.Path(out)
|
|
252
|
+
s = Shortcut(out.stem, color="Teal")
|
|
253
|
+
stored = s.action("is.workflow.actions.setstoredcontent", WFInput="hello from python",
|
|
254
|
+
WFStoredContentKey="demo-greeting", WFStoredContentGlobalValue=False)
|
|
255
|
+
got = s.action("is.workflow.actions.getstoredcontent", WFStoredContentKey="demo-greeting")
|
|
256
|
+
gid = s.if_(ref(got), "has_any_value")
|
|
257
|
+
s.action("is.workflow.actions.showresult", Text=text("Stored value: ", ref(got)))
|
|
258
|
+
s.otherwise(gid)
|
|
259
|
+
s.action("is.workflow.actions.showresult", Text=text("Nothing stored"))
|
|
260
|
+
s.end_if(gid)
|
|
261
|
+
unsigned = pathlib.Path(out).with_suffix(".unsigned.shortcut")
|
|
262
|
+
s.write(unsigned)
|
|
263
|
+
s.sign(unsigned, out)
|
|
264
|
+
print(f"wrote {out} ({len(s.actions)} actions)")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
from . import actions # noqa: E402 (generated identifier constants and ACTIONS metadata)
|
|
268
|
+
from .actions import ACTIONS, PARAM_KINDS # noqa: E402
|
|
269
|
+
|
|
270
|
+
PROVENANCE = json.load(open(HERE / "data" / "provenance.json")) if (HERE / "data" / "provenance.json").exists() else {}
|
|
271
|
+
"""Which macOS and Shortcuts build the bundled data was extracted from."""
|
|
272
|
+
|
|
273
|
+
__all__ = ["actions", "ACTIONS", "PARAM_KINDS", "PROVENANCE", "Shortcut", "ref", "variable", "shortcut_input", "clipboard", "current_date", "ask", "picker", "text",
|
|
274
|
+
"ICON_COLORS", "CONDITION", "DEFAULT_GLYPH", "LEGACY_KEYS", "demo"]
|
|
275
|
+
__version__ = "0.1.0"
|