├── .gitignore ├── dev_requirements.in ├── expansions ├── note_header.txt ├── tree.txt ├── before_and_after.html ├── edit_javascript.py ├── datetime_encoder.py ├── list_s3_objects.py ├── flapi.py ├── get_secrets_manager_secret.py ├── template.svg ├── create_hash.py ├── get_boto3_session.py ├── get_directories.py ├── list_dynamodb_rows.py ├── get_file_paths.py └── mit_license.txt ├── pyproject.toml ├── info.plist ├── dev_requirements.txt ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── LICENSE ├── README.md └── create_snippets.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.alfredsnippets 2 | -------------------------------------------------------------------------------- /dev_requirements.in: -------------------------------------------------------------------------------- 1 | mypy 2 | ruff 3 | -------------------------------------------------------------------------------- /expansions/note_header.txt: -------------------------------------------------------------------------------- 1 | --- 2 | date: {isodate:yyyy-MM-dd} 3 | --- 4 | -------------------------------------------------------------------------------- /expansions/tree.txt: -------------------------------------------------------------------------------- 1 | root 2 | ├─ subdir1/ 3 | │ └─ file.txt 4 | └─ file.txt -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "snippets" 3 | version = "1.0" 4 | 5 | [tool.mypy] 6 | mypy_path = "src" 7 | strict = true 8 | -------------------------------------------------------------------------------- /expansions/before_and_after.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
BeforeAfter
BEFOREAFTER
11 | -------------------------------------------------------------------------------- /expansions/edit_javascript.py: -------------------------------------------------------------------------------- 1 | from javascript_data_files import read_js, write_js 2 | 3 | value = read_js("filename.js", varname="varname") 4 | 5 | write_js("filename.js", varname="varname", value=value) 6 | -------------------------------------------------------------------------------- /expansions/datetime_encoder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class DatetimeEncoder(json.JSONEncoder): 5 | def default(self, obj): 6 | if isinstance(obj, datetime.datetime): 7 | return obj.isoformat() 8 | -------------------------------------------------------------------------------- /expansions/list_s3_objects.py: -------------------------------------------------------------------------------- 1 | def list_s3_objects(sess: boto3.Session, **kwargs): 2 | s3 = sess.client("s3") 3 | 4 | for page in s3.get_paginator("list_objects_v2").paginate(**kwargs): 5 | yield from page.get("Contents", []) 6 | -------------------------------------------------------------------------------- /expansions/flapi.py: -------------------------------------------------------------------------------- 1 | from flickr_api import FlickrApi 2 | import keyring 3 | 4 | api = FlickrApi.with_api_key( 5 | api_key=keyring.get_password("flickr_api", "key"), 6 | user_agent="Alex Chan's personal scripts ", 7 | ) 8 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | snippetkeywordprefix 6 | 7 | snippetkeywordsuffix 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /expansions/get_secrets_manager_secret.py: -------------------------------------------------------------------------------- 1 | def get_secret_string(sess: boto3.Session, **kwargs) -> str: 2 | """ 3 | Look up a SecretString from Secrets Manager, and return the string. 4 | """ 5 | secrets = sess.client("secretsmanager") 6 | 7 | resp = secrets.get_secret_value(**kwargs) 8 | 9 | return resp["SecretString"] 10 | -------------------------------------------------------------------------------- /expansions/template.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /expansions/create_hash.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from pathlib import Path 3 | 4 | 5 | def create_hash(path: Path) -> "hashlib._Hash": 6 | """ 7 | Returns the checksum of the given path. 8 | """ 9 | h = hashlib.sha256() 10 | 11 | with open(path, "rb") as infile: 12 | while chunk := infile.read(8192): 13 | h.update(chunk) 14 | 15 | return h 16 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile dev_requirements.in --output-file dev_requirements.txt 3 | mypy==1.18.2 4 | # via -r dev_requirements.in 5 | mypy-extensions==1.0.0 6 | # via mypy 7 | pathspec==0.12.1 8 | # via mypy 9 | ruff==0.13.1 10 | # via -r dev_requirements.in 11 | tomli==2.2.1 12 | # via mypy 13 | typing-extensions==4.13.1 14 | # via mypy 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | time: "09:00" 9 | - package-ecosystem: "pip" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | day: "monday" 14 | time: "09:00" 15 | ignore: 16 | - dependency-name: "mypy" 17 | - dependency-name: "ruff" 18 | -------------------------------------------------------------------------------- /expansions/get_boto3_session.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def get_aws_session(*, role_arn: str) -> boto3.Session: 5 | sts_client = boto3.client("sts") 6 | assumed_role_object = sts_client.assume_role( 7 | RoleArn=role_arn, RoleSessionName="AssumeRoleSession1" 8 | ) 9 | credentials = assumed_role_object["Credentials"] 10 | 11 | return boto3.Session( 12 | aws_access_key_id=credentials["AccessKeyId"], 13 | aws_secret_access_key=credentials["SecretAccessKey"], 14 | aws_session_token=credentials["SessionToken"], 15 | ) 16 | -------------------------------------------------------------------------------- /expansions/get_directories.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | 4 | def get_directories_under(root="."): 5 | """ 6 | Generates the absolute paths to every directory under ``root``. 7 | """ 8 | root = pathlib.Path(root) 9 | 10 | if root.exists() and not root.is_dir(): 11 | raise ValueError(f"Cannot find files under file: {root!r}") 12 | 13 | if not root.is_dir(): 14 | raise FileNotFoundError(root) 15 | 16 | for dirpath, _, _ in root.walk(): 17 | yield dirpath 18 | 19 | 20 | for p in get_directories_under(): 21 | {cursor} 22 | -------------------------------------------------------------------------------- /expansions/list_dynamodb_rows.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def scan_table(sess: boto3.Session, *, TableName: str, **kwargs): 5 | """ 6 | Generates all the items in a DynamoDB table. 7 | 8 | :param dynamo_client: A boto3 client for DynamoDB. 9 | :param TableName: The name of the table to scan. 10 | 11 | Other keyword arguments will be passed directly to the Scan operation. 12 | See https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.scan 13 | 14 | """ 15 | dynamo_client = sess.resource("dynamodb").meta.client 16 | paginator = dynamo_client.get_paginator("scan") 17 | 18 | for page in paginator.paginate(TableName=TableName, **kwargs): 19 | yield from page["Items"] 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v6 21 | with: 22 | python-version: 3.12 23 | cache: pip 24 | cache-dependency-path: dev_requirements.txt 25 | 26 | - name: Install dependencies 27 | run: pip install -r dev_requirements.txt 28 | 29 | - name: Run linting 30 | run: | 31 | ruff check *.py 32 | ruff format --check . 33 | 34 | - name: Check types 35 | run: mypy *.py 36 | 37 | - name: Build the workflow 38 | run: python3 create_snippets.py 39 | -------------------------------------------------------------------------------- /expansions/get_file_paths.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | from pathlib import Path 3 | 4 | 5 | def get_file_paths_under( 6 | root: Path | str = Path("."), *, suffix: str = "" 7 | ) -> Iterator[Path]: 8 | """ 9 | Generates the absolute paths to every matching file under ``root``. 10 | """ 11 | root = Path(root) 12 | 13 | if root.exists() and not root.is_dir(): 14 | raise ValueError(f"Cannot find files under non-directory: {root!r}") 15 | 16 | if not root.is_dir(): 17 | raise FileNotFoundError(root) 18 | 19 | for dirpath, _, filenames in root.walk(): 20 | for f in filenames: 21 | p = dirpath / f 22 | 23 | if p.is_file() and f.lower().endswith(suffix): 24 | yield p 25 | 26 | 27 | for p in get_file_paths_under(): 28 | {cursor} 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /expansions/mit_license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) {isodate:yyyy} Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snippets 2 | 3 | This repo has my (public) text expansion snippets. 4 | 5 | Text expansion apps allow me to save keystrokes when I'm typing common words – for example, if I type `intl`, my computer expands it to `international`. 6 | That saves me 9 keystrokes. 7 | It may not seem like much, but I use these snippets dozens of times a day and it quickly adds up! 8 | 9 | This repo has a script that creates a collection of snippets to use [in Alfred](https://www.alfredapp.com/help/features/snippets/). 10 | 11 | ## Why not use Alfred's in-app snippet editor? 12 | 13 | Because I want to share snippets between my home and work computers. 14 | 15 | Alfred can sync its preferences through some sort of cloud storage, e.g. Dropbox or iCloud. 16 | But I don't want to connect my home and work computers that way – I try to keep them separate. 17 | e.g. I don't log into my work email on my home computer, and I don't log in to my personal iCloud on my work laptop. 18 | 19 | By putting my snippets in a GitHub repo, I can check out the repo on both computers and get the same set of snippets, but in a way that maintains the gap between the machines. 20 | Neither machine can directly affect the other. 21 | 22 | I don't put all my snippets in this repo; just the ones I can make public. 23 | For example, my home computer has extra snippets for personal info like my phone number and my address. 24 | I configure those in the in-app editor, because I don't want to put them in a public Git repo and I don't need them on my work computer. 25 | 26 | ## Usage 27 | 28 | ```console 29 | $ python3 create_snippets.py 30 | ``` 31 | 32 | This will create a new file `Alex’s snippets.alfredsnippets`, which you can open to add these snippets to Alfred Preferences. 33 | -------------------------------------------------------------------------------- /create_snippets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import hashlib 4 | import json 5 | import os 6 | import uuid 7 | import zipfile 8 | 9 | 10 | def read(name: str) -> str: 11 | with open(os.path.join("expansions", name)) as infile: 12 | return infile.read() 13 | 14 | 15 | # fmt: off 16 | SNIPPETS = { 17 | "!bq": "
{clipboard}
", 18 | 19 | # ========================= 20 | # Get the current date/time 21 | # ========================= 22 | 23 | # pretty, e.g. 9 April 2024 24 | ";dp": "{isodate:d MMMM yyyy}", 25 | 26 | # short, e.g. 2024-04-09 27 | ";ds": "{isodate:yyyy-MM-dd}", 28 | 29 | # e.g. 2024-04-09 10:49:12 +0100 30 | ";dd": "{isodate:yyyy-MM-dd HH:mm:ss Z}", 31 | 32 | # e.g. \[10:49] 33 | # I use this for timestamped entries in my Obsidian journal 34 | ";dt": r"\[{isodate:HH:mm}] ", 35 | 36 | # e.g. 2025-02-09T09:13:58Z 37 | # I use this for timestamped entries in JavaScript/JSON files 38 | ";dj": "{isodatetime}", 39 | 40 | # ========================= 41 | # English words and phrases 42 | # ========================= 43 | "a11e": "accessible", 44 | "a11y": "accessibility", 45 | "A11y": "Accessibility", 46 | "acc't": "account", 47 | "Acc't": "Account", 48 | "acct": "account", 49 | "Acct": "Account", 50 | "afaict": "as far as I can tell", 51 | " atm": " at the moment", 52 | "avg": "average", 53 | "bdy": "boundary", 54 | "Bdy": "Boundary", 55 | "bdies": "boundaries", 56 | "Bdies": "Boundaries", 57 | "cafe": "café", 58 | "cliche": "cliché", 59 | "ctd": "continued", 60 | " cts": " continuous", 61 | "Cts": "Continuous", 62 | "Das Ubermensch": "Das Übermensch", 63 | "defn": "definition", 64 | "deja vu": "déjà vu", 65 | "dept.": "department", 66 | "distn": "distribution", 67 | "digipres": "digital preservation", 68 | "Digipres": "Digital preservation", 69 | "eqn": "equation", 70 | "expt": "experiment", 71 | "fdn": "foundation", 72 | "Fdn": "Foundation", 73 | "fiance": "fiancé", 74 | "fn": "function", 75 | "Fn": "Function", 76 | "gdn": "garden", 77 | "Gdn": "Garden", 78 | "gov't": "government", 79 | "govt": "government", 80 | "Govt": "Government", 81 | "i14n": "intersectional", 82 | "i18n": "internationalisation", 83 | " iff": " if and only if", 84 | "ina11e": "inaccessible", 85 | "indpt": "independent", 86 | "intl.": "international", 87 | "iptic": "in particular", 88 | "Iptic": "In particular", 89 | "l12n": "localisation", 90 | "lmk": "let me know", 91 | "mgmt": "management", 92 | "Mgmt": "Management", 93 | " mgr": " manager", 94 | "Mgr": "Manager", 95 | "naive": "naïve", 96 | "natl.": "national", 97 | "nbhd": "neighbourhood", 98 | "nee ": "née ", 99 | " o/w": " otherwise", 100 | " O/w": " Otherwise", 101 | "ofc": "of course", 102 | " ptic": " particular", 103 | "rec'd": "recommended", 104 | "reln": "relation", 105 | "Reln": "Relation", 106 | "reqd": "required", 107 | "s.t.": "such that", 108 | "soln": "solution", 109 | "spose": "suppose", 110 | "Spose": "Suppose", 111 | "stdlib": "standard library", 112 | " thm": " theorem", 113 | "w/b": "week beginning", 114 | "w/e": "week ending", 115 | "w/o": "without", 116 | "y'day": "yesterday", 117 | 118 | # ============================= 119 | # Fix my common typing mistakes 120 | # ============================= 121 | "cunt": "count", 122 | "EVentbridge": "EventBridge", 123 | "Flcikr": "Flickr", 124 | " ot ": " to ", 125 | "thier": "their", 126 | "WHy": "Why", 127 | 128 | # ============ 129 | # Proper nouns 130 | # ============ 131 | "Agnes": "Agnès", 132 | "Airdrop": "AirDrop", 133 | "Anais Mitchell": "Anaïs Mitchell", 134 | "B'ham": "Birmingham", 135 | "BackBlaze": "Backblaze", 136 | "Bezier": "Bézier", 137 | "Borrowbox": "BorrowBox", 138 | "CO2": "CO₂", 139 | "China Mieville": "China Miéville", 140 | "Cloudwatch": "CloudWatch", 141 | "Ebay": "eBay", 142 | "El Otro Periodico": "El Otro Periódico", 143 | "Elasticache": "ElastiCache", 144 | "Erdos": "Erdős", 145 | "Eventbridge": "EventBridge", 146 | "Facetime": "FaceTime", 147 | "FastMail": "Fastmail", 148 | "Gitbook": "GitBook", 149 | "Hashicorp": "HashiCorp", 150 | "Maciej Ceglowski": "Maciej Cegłowski", 151 | "Paypal": "PayPal", 152 | "Phylopic": "PhyloPic", 153 | "Postgresql": "PostgreSQL", 154 | "Powerpoint": "PowerPoint", 155 | "Raphaelle": "Raphaëlle", 156 | "Redmonk": "RedMonk", 157 | "Regents Canal": "Regent’s Canal", 158 | "Rubocop": "RuboCop", 159 | "Sqlite": "SQLite", 160 | "SQlite": "SQLite", 161 | "Sean": "Seán", 162 | "Sharepoint": "SharePoint", 163 | "Skoda": "Škoda", 164 | "Smugmug": "SmugMug", 165 | "Spitlip": "SpitLip", 166 | "Taf": "Tâf", 167 | "Textexpander": "TextExpander", 168 | "Tineye": "TinEye", 169 | "Whatsapp": "WhatsApp", 170 | "WikiData": "Wikidata", 171 | "Wireguard": "WireGuard", 172 | "Wordpress": "WordPress", 173 | "Youtube": "YouTube", 174 | "Zoe": "Zoë", 175 | "bhalaj": "bhålaj", 176 | " ldn": " london", 177 | "wall-e": "WALL·E", 178 | 179 | # ================================= 180 | # Symbols and other bits of Unicode 181 | # ================================= 182 | "180^": "180°", 183 | "^C": "°C", 184 | "^F": "°F", 185 | "^deg": "°", 186 | "^ft": "′", 187 | "^in": "″", 188 | ":+1:": "👍", 189 | ":wave:": "👋", 190 | ";1/2": "½", 191 | ";1/4": "¼", 192 | ";3/4": "¾", 193 | ";alt": "⌥", 194 | ";approx": "≈", 195 | ";bullet": "•", 196 | ";cmd": "⌘", 197 | ";ctrl": "⌃", 198 | ";dot": "·", 199 | ";eur": "€", 200 | ";minus": "−", 201 | ";opt": "⌥", 202 | ";pi": "π", 203 | ";pm": "±", 204 | ";sec": "§", 205 | ";shift": "⇧", 206 | ";sqrt": "√", 207 | ";times": "×", 208 | ";tm": "™", 209 | ";zwsp": "\u200b", # zero-width space 210 | 211 | # This snippet draws a basic tree with Unicode characters, which 212 | # I can copy/paste to build a more complex tree by hand. 213 | "!tree": read("tree.txt"), 214 | 215 | # ============= 216 | # Personal info 217 | # ============= 218 | "@aa": "@alexwlchan.net", 219 | ";ee": "alex@alexwlchan.net", 220 | 221 | ";ale": "alexwlchan", 222 | 223 | ";so": "Signed-off-by: Alex Chan ", 224 | 225 | # My Mastodon server 226 | ";san": "social.alexwlchan.net", 227 | 228 | # ==================== 229 | # Programming snippets 230 | # ==================== 231 | "!bash": "#!/usr/bin/env bash\n\nset -o errexit\nset -o nounset\n", 232 | "!osa": "#!/usr/bin/env osascript\n", 233 | "!py": "#!/usr/bin/env python3\n\n", 234 | "!rb": "#!/usr/bin/env ruby\n", 235 | "!swift": "#!/usr/bin/env swift\n", 236 | 237 | "!rect": '', 238 | "!svg": read("template.svg"), 239 | 240 | "!before": read("before_and_after.html"), 241 | 242 | "!mit": read("mit_license.txt"), 243 | 244 | # Git trailer 245 | # See https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors 246 | ";co": "Co-authored-by:", 247 | 248 | # =================================== 249 | # Python-related programming snippets 250 | # =================================== 251 | "!dt": "from datetime import datetime, timezone\n", 252 | "!j": "import json\n", 253 | "!pp": "from pprint import pprint; pprint({cursor})", 254 | 255 | "@param": "@pytest.mark.parametrize({cursor})", 256 | 257 | "!flapi": read("flapi.py"), 258 | 259 | "py!aws": read("get_boto3_session.py"), 260 | "py!dy": read("list_dynamodb_rows.py"), 261 | "py!h": read("create_hash.py"), 262 | "py!de": read("datetime_encoder.py"), 263 | "py!pth": read("get_file_paths.py"), 264 | "py!pd": read("get_directories.py"), 265 | "py!sec": read("get_secrets_manager_secret.py"), 266 | "py!s3": read("list_s3_objects.py"), 267 | "py!ej": read("edit_javascript.py"), 268 | 269 | # I can never remember the order of args to this function, 270 | # so when I start typing it, add a comment to help me out. 271 | "datetime.datetime.strp": "datetime.datetime.strptime({cursor}) # date_string, format", 272 | 273 | # ================= 274 | # Obsidian snippets 275 | # ================= 276 | ";nd": read("note_header.txt"), 277 | 278 | # ============ 279 | # Misc phrases 280 | # ============ 281 | "porque?": "¿Por qué no los dos?", 282 | } 283 | # fmt: on 284 | 285 | 286 | def add_snippet(zf: zipfile.ZipFile, shortcut: str, expansion: str) -> None: 287 | """ 288 | Add a single snippet to a snippets bundle. 289 | """ 290 | h = hashlib.md5() 291 | h.update(shortcut.encode("utf8")) 292 | h.update(expansion.encode("utf8")) 293 | 294 | snippet_id = uuid.UUID(hex=h.hexdigest()) 295 | 296 | snippet_data = { 297 | "alfredsnippet": { 298 | "snippet": expansion, 299 | "uid": str(snippet_id), 300 | "name": "", 301 | "keyword": shortcut, 302 | } 303 | } 304 | 305 | zf.writestr(f"{snippet_id}.json", data=json.dumps(snippet_data)) 306 | 307 | 308 | if __name__ == "__main__": 309 | with zipfile.ZipFile("Alex’s snippets.alfredsnippets", "w") as zf: 310 | zf.write("info.plist") 311 | 312 | for shortcut, expansion in SNIPPETS.items(): 313 | add_snippet(zf, shortcut, expansion) 314 | --------------------------------------------------------------------------------