├── .DS_Store ├── Call or SMS contact.alfredworkflow ├── LICENSE ├── README.md ├── Screenshots ├── .DS_Store ├── initcall.png ├── typing.png ├── workflow.png └── workflow_old.png └── src ├── .DS_Store ├── 0FAC263F-D24B-4ACB-9D59-07329F75BCD0.png ├── 1DFF0026-0410-4E09-B829-03EA6203893F.png ├── 42AC172B-3284-46CC-A8B8-B5251D9644DB.png ├── AA05525E-1106-40EB-8CFC-66CA0AFFF10F.png ├── FF8E3BE3-2E94-4801-8A01-9AA8DE17340B.png ├── icon.png ├── info.plist ├── message.png ├── search.py └── workflow ├── .alfredversionchecked ├── Notify.tgz ├── __init__.py ├── __init__.pyc ├── background.py ├── background.pyc ├── notify.py ├── update.py ├── update.pyc ├── version ├── web.py ├── web.pyc ├── workflow.py ├── workflow.pyc ├── workflow3.py └── workflow3.pyc /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/.DS_Store -------------------------------------------------------------------------------- /Call or SMS contact.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/Call or SMS contact.alfredworkflow -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## This Workflow is no longer supported. Please see [Call or Message Contact](https://github.com/c-stephens/Call-or-Message-Contact) for future updates. 3 |
4 |

A short stand-in to add sms and calling support to for macOS Yosemite and above. Searches through contacts in real time as you type. Excludes contacts with no number. Currently no way to select between multiple numbers for a single contact. Requires Alfred V4.

5 | 6 |

Released under Apache License 2.0. If you modify this and use it in something splendid we'd appreciate a shout out.

7 | 8 |

Usage:

9 |
10 |

Call Johny Appleseed

11 |
12 |
13 |
14 |

im Little Red

15 |
16 |
17 |
18 |

sms Bill Nye

19 |
20 |
21 |
22 |

call 555-3485

23 |
24 |
25 |
26 |

im 555-3485

27 |
28 | 29 |
30 | 31 |
32 | 33 |
34 | 35 |

You can also mute/unmute and end calls. You can also accept or decline incoming calls too. These options can be invoked by the keywords "Mute", "End", and "Answer" or hotkeys. The "End" command will do all of the following: end a current call, decline an incoming call, and cancel a failed call attempt.

36 | 37 |

Known Issues:

38 | 39 |

40 | (This no longer appears to be an issue in macOS 10.15) 41 |

42 | 43 |

macOS 10.14 - The "Mute" command will not reflect that the call is muted in the Notification Center window when invoked for the first time. If you invoke the command twice, then GUI will reflect the change (the "Mute" text color changes from gray to white). I believe this is an issue with Notification Center.

44 | 45 | -------------------------------------------------------------------------------- /Screenshots/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/Screenshots/.DS_Store -------------------------------------------------------------------------------- /Screenshots/initcall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/Screenshots/initcall.png -------------------------------------------------------------------------------- /Screenshots/typing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/Screenshots/typing.png -------------------------------------------------------------------------------- /Screenshots/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/Screenshots/workflow.png -------------------------------------------------------------------------------- /Screenshots/workflow_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/Screenshots/workflow_old.png -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/.DS_Store -------------------------------------------------------------------------------- /src/0FAC263F-D24B-4ACB-9D59-07329F75BCD0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/0FAC263F-D24B-4ACB-9D59-07329F75BCD0.png -------------------------------------------------------------------------------- /src/1DFF0026-0410-4E09-B829-03EA6203893F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/1DFF0026-0410-4E09-B829-03EA6203893F.png -------------------------------------------------------------------------------- /src/42AC172B-3284-46CC-A8B8-B5251D9644DB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/42AC172B-3284-46CC-A8B8-B5251D9644DB.png -------------------------------------------------------------------------------- /src/AA05525E-1106-40EB-8CFC-66CA0AFFF10F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/AA05525E-1106-40EB-8CFC-66CA0AFFF10F.png -------------------------------------------------------------------------------- /src/FF8E3BE3-2E94-4801-8A01-9AA8DE17340B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/FF8E3BE3-2E94-4801-8A01-9AA8DE17340B.png -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/icon.png -------------------------------------------------------------------------------- /src/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.hirvi74.call-or-sms-contact 7 | category 8 | Productivity 9 | connections 10 | 11 | 0FAC263F-D24B-4ACB-9D59-07329F75BCD0 12 | 13 | 14 | destinationuid 15 | A2BE7579-08DF-4914-9F8B-59D5C429C3AA 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | 10C9547D-84B9-43F9-9DA0-6AEC76C58A94 25 | 26 | 27 | destinationuid 28 | D5DDD534-314A-473C-A898-AEC5C234FC52 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | 126EBE51-784E-415A-8D65-C85B217DF930 38 | 39 | 40 | destinationuid 41 | D5DDD534-314A-473C-A898-AEC5C234FC52 42 | modifiers 43 | 0 44 | modifiersubtext 45 | 46 | vitoclose 47 | 48 | 49 | 50 | 1DFF0026-0410-4E09-B829-03EA6203893F 51 | 52 | 53 | destinationuid 54 | 9ECF0139-0E66-4EAC-8C62-1D55A508874E 55 | modifiers 56 | 0 57 | modifiersubtext 58 | 59 | vitoclose 60 | 61 | 62 | 63 | 2F166541-DACC-4370-81BF-8AD4764571F4 64 | 65 | 66 | destinationuid 67 | DD4F6A79-5EB5-4D42-8C8A-934D5706A827 68 | modifiers 69 | 0 70 | modifiersubtext 71 | 72 | vitoclose 73 | 74 | 75 | 76 | 42AC172B-3284-46CC-A8B8-B5251D9644DB 77 | 78 | 79 | destinationuid 80 | 9ECF0139-0E66-4EAC-8C62-1D55A508874E 81 | modifiers 82 | 0 83 | modifiersubtext 84 | 85 | vitoclose 86 | 87 | 88 | 89 | 543F776C-68F3-4717-B11A-76848C11E84A 90 | 91 | 92 | destinationuid 93 | DD4F6A79-5EB5-4D42-8C8A-934D5706A827 94 | modifiers 95 | 0 96 | modifiersubtext 97 | 98 | vitoclose 99 | 100 | 101 | 102 | 5C05FAE4-38A9-4B98-8863-C42A8F5CE7E5 103 | 104 | 105 | destinationuid 106 | 76CD24D4-D950-445D-A8FC-D8BD4D390299 107 | modifiers 108 | 0 109 | modifiersubtext 110 | 111 | vitoclose 112 | 113 | 114 | 115 | 5F11E541-2D4B-4B16-8D27-15CCC0EB7889 116 | 117 | 118 | destinationuid 119 | 3E36110A-9B17-4E02-AF9D-FA99A6A973F4 120 | modifiers 121 | 0 122 | modifiersubtext 123 | 124 | vitoclose 125 | 126 | 127 | 128 | 76CD24D4-D950-445D-A8FC-D8BD4D390299 129 | 130 | 131 | destinationuid 132 | E63F1F76-116F-4C39-A91D-7A1BD00B991B 133 | modifiers 134 | 0 135 | modifiersubtext 136 | 137 | vitoclose 138 | 139 | 140 | 141 | 86590F04-57CC-4B1E-96FF-A87CC14063DA 142 | 143 | 953EA110-8004-4091-9985-F0AF57D696C4 144 | 145 | 146 | destinationuid 147 | D675C802-AD81-43EF-8415-4D4FE2425415 148 | modifiers 149 | 0 150 | modifiersubtext 151 | 152 | vitoclose 153 | 154 | 155 | 156 | destinationuid 157 | 5C05FAE4-38A9-4B98-8863-C42A8F5CE7E5 158 | modifiers 159 | 1048576 160 | modifiersubtext 161 | Skip confirmation and call 162 | vitoclose 163 | 164 | 165 | 166 | 9ECF0139-0E66-4EAC-8C62-1D55A508874E 167 | 168 | 169 | destinationuid 170 | 86590F04-57CC-4B1E-96FF-A87CC14063DA 171 | modifiers 172 | 0 173 | modifiersubtext 174 | 175 | vitoclose 176 | 177 | 178 | 179 | A2BE7579-08DF-4914-9F8B-59D5C429C3AA 180 | 181 | 182 | destinationuid 183 | 9ECF0139-0E66-4EAC-8C62-1D55A508874E 184 | modifiers 185 | 0 186 | modifiersubtext 187 | 188 | vitoclose 189 | 190 | 191 | 192 | AA05525E-1106-40EB-8CFC-66CA0AFFF10F 193 | 194 | 195 | destinationuid 196 | A2BE7579-08DF-4914-9F8B-59D5C429C3AA 197 | modifiers 198 | 0 199 | modifiersubtext 200 | 201 | vitoclose 202 | 203 | 204 | 205 | B855F94C-6DC2-4999-9A4C-3DF7F330CD9F 206 | 207 | 208 | destinationuid 209 | 671ABB72-7DBC-417F-BBA7-E9C68E710CD0 210 | modifiers 211 | 0 212 | modifiersubtext 213 | 214 | vitoclose 215 | 216 | 217 | 218 | BF74F4C0-C5C8-439F-80AE-05B84814B44E 219 | 220 | 221 | destinationuid 222 | 8D17B46F-4A88-42A0-9501-07FC2FE174FA 223 | modifiers 224 | 0 225 | modifiersubtext 226 | 227 | vitoclose 228 | 229 | 230 | 231 | D0EA7276-2A9C-4469-8589-8C56510DC31F 232 | 233 | 234 | destinationuid 235 | 8D17B46F-4A88-42A0-9501-07FC2FE174FA 236 | modifiers 237 | 0 238 | modifiersubtext 239 | 240 | vitoclose 241 | 242 | 243 | 244 | D5DDD534-314A-473C-A898-AEC5C234FC52 245 | 246 | 247 | destinationuid 248 | 76CD24D4-D950-445D-A8FC-D8BD4D390299 249 | modifiers 250 | 0 251 | modifiersubtext 252 | 253 | vitoclose 254 | 255 | 256 | 257 | D675C802-AD81-43EF-8415-4D4FE2425415 258 | 259 | 260 | destinationuid 261 | 76CD24D4-D950-445D-A8FC-D8BD4D390299 262 | modifiers 263 | 0 264 | modifiersubtext 265 | 266 | vitoclose 267 | 268 | 269 | 270 | E63F1F76-116F-4C39-A91D-7A1BD00B991B 271 | 272 | 273 | destinationuid 274 | B855F94C-6DC2-4999-9A4C-3DF7F330CD9F 275 | modifiers 276 | 0 277 | modifiersubtext 278 | 279 | vitoclose 280 | 281 | 282 | 283 | FAEE1763-A240-484D-A6C4-33BC4CAF3BEC 284 | 285 | 286 | destinationuid 287 | 3E36110A-9B17-4E02-AF9D-FA99A6A973F4 288 | modifiers 289 | 0 290 | modifiersubtext 291 | 292 | vitoclose 293 | 294 | 295 | 296 | FF8E3BE3-2E94-4801-8A01-9AA8DE17340B 297 | 298 | 299 | destinationuid 300 | 126EBE51-784E-415A-8D65-C85B217DF930 301 | modifiers 302 | 0 303 | modifiersubtext 304 | 305 | vitoclose 306 | 307 | 308 | 309 | destinationuid 310 | 10C9547D-84B9-43F9-9DA0-6AEC76C58A94 311 | modifiers 312 | 1048576 313 | modifiersubtext 314 | Skip confirmation and Call 315 | vitoclose 316 | 317 | 318 | 319 | 320 | createdby 321 | Conner Stephens (@Hirvi74) & Amos Manneschmidt (@amoose136) 322 | description 323 | 324 | disabled 325 | 326 | name 327 | Call or SMS contact 328 | objects 329 | 330 | 331 | config 332 | 333 | argument 334 | {query} 335 | variables 336 | 337 | skip_confirm 338 | False 339 | 340 | 341 | type 342 | alfred.workflow.utility.argument 343 | uid 344 | 126EBE51-784E-415A-8D65-C85B217DF930 345 | version 346 | 1 347 | 348 | 349 | config 350 | 351 | applescript 352 | on alfred_script(q) 353 | 354 | -- {sys1, sys2, sys3 } = { major, minor, bugfix} 355 | set minor_ver to system attribute "sys2" 356 | 357 | set contacts_running to false 358 | if application "Contacts" is running then 359 | set contacts_running to true 360 | else 361 | -- Catalina (10.15) moved the location of Contacts.app 362 | if 14 ≤ minor_ver then 363 | do shell script "open -j /Applications/Contacts.app" 364 | else 365 | do shell script "open -j /System/Applications/Contacts.app" 366 | end if 367 | end if 368 | 369 | tell application "Contacts" 370 | set q to text ((offset of "Metadata/" in q)+9) thru ((offset of "." in q)-1) of q 371 | set phonenumber to value of phone of person id q set phonenumber to item 1 of phonenumber 372 | end tell 373 | 374 | if contacts_running is false 375 | tell application "Contacts" to quit 376 | end if 377 | 378 | return phonenumber 379 | end alfred_script 380 | cachescript 381 | 382 | 383 | type 384 | alfred.workflow.action.applescript 385 | uid 386 | D5DDD534-314A-473C-A898-AEC5C234FC52 387 | version 388 | 1 389 | 390 | 391 | config 392 | 393 | anchorfields 394 | 395 | argumenttrimmode 396 | 0 397 | argumenttype 398 | 0 399 | daterange 400 | 0 401 | fields 402 | 403 | 404 | field 405 | kMDItemDisplayName 406 | not 407 | 408 | split 409 | 410 | value 411 | {query} 412 | words 413 | 414 | 415 | 416 | field 417 | kMDItemFinderComment 418 | not 419 | 420 | split 421 | 422 | value 423 | {query} 424 | words 425 | 426 | 427 | 428 | field 429 | kMDItemPhoneNumbers 430 | not 431 | 432 | split 433 | 434 | value 435 | * 436 | words 437 | 438 | 439 | 440 | field 441 | kMDItemPhoneNumbers 442 | not 443 | 444 | split 445 | 446 | value 447 | {query} 448 | words 449 | 450 | 451 | 452 | includesystem 453 | 454 | keyword 455 | call 456 | limit 457 | 0 458 | runningsubtext 459 | 460 | scopes 461 | 462 | ~/Library/Application Support/AddressBook 463 | 464 | sortmode 465 | 0 466 | subtext 467 | Call using iPhone tethering if available 468 | title 469 | Call [Contact Name] 470 | types 471 | 472 | com.apple.addressbook.person 473 | 474 | withspace 475 | 476 | 477 | type 478 | alfred.workflow.input.filefilter 479 | uid 480 | FF8E3BE3-2E94-4801-8A01-9AA8DE17340B 481 | version 482 | 2 483 | 484 | 485 | config 486 | 487 | applescript 488 | on alfred_script(q) 489 | set tout to 8 set t to 0 repeat while t < tout tell application "System Events" set facetime_running to count (every process whose name is "FaceTime") end tell if facetime_running is 1 then try tell application "System Events" 490 | set frontWindow to front window of application process "FaceTime" delay .6 # needed to avoid weird flickering stuff. Try increasing if you still see it on your system click button "Call" of application process "FaceTime" exit repeat end tell end try else delay 0.1 end if set t to t + 0.1 end repeat 491 | end alfred_script 492 | cachescript 493 | 494 | 495 | type 496 | alfred.workflow.action.applescript 497 | uid 498 | 16865643-D0A0-48DA-961D-053770CCBB23 499 | version 500 | 1 501 | 502 | 503 | config 504 | 505 | argument 506 | {query} 507 | variables 508 | 509 | skip_confirm 510 | True 511 | 512 | 513 | type 514 | alfred.workflow.utility.argument 515 | uid 516 | 10C9547D-84B9-43F9-9DA0-6AEC76C58A94 517 | version 518 | 1 519 | 520 | 521 | config 522 | 523 | concurrently 524 | 525 | escaping 526 | 127 527 | script 528 | open 'facetime-audio://{query}' 529 | scriptargtype 530 | 0 531 | scriptfile 532 | 533 | type 534 | 0 535 | 536 | type 537 | alfred.workflow.action.script 538 | uid 539 | E63F1F76-116F-4C39-A91D-7A1BD00B991B 540 | version 541 | 2 542 | 543 | 544 | config 545 | 546 | argumenttype 547 | 0 548 | keyword 549 | call 550 | subtext 551 | Call {query} 552 | text 553 | Call [Number] 554 | withspace 555 | 556 | 557 | type 558 | alfred.workflow.input.keyword 559 | uid 560 | 953EA110-8004-4091-9985-F0AF57D696C4 561 | version 562 | 1 563 | 564 | 565 | config 566 | 567 | argument 568 | {query} 569 | variables 570 | 571 | skip_confirm 572 | False 573 | 574 | 575 | type 576 | alfred.workflow.utility.argument 577 | uid 578 | D675C802-AD81-43EF-8415-4D4FE2425415 579 | version 580 | 1 581 | 582 | 583 | config 584 | 585 | concurrently 586 | 587 | escaping 588 | 68 589 | script 590 | set minor_ver to system attribute "sys2" -- {sys1, sys2, sys3 } = { major, minor, bugfix} if 13 ≤ minor_ver then tell application "System Events" to tell process "Notification Center" delay 0.6 click button "Call" of windows end tell else -- I assume this works for 10.13.X and lower, but I haven't tested it. 591 | -- I also snagged what is below from an Alfred NSAppleScript that is still 592 | -- Left in the project for refrence. 593 | set tout to 8 set t to 0 repeat while t < tout tell application "System Events" set facetime_running to count (every process whose name is "FaceTime") end tell if facetime_running is 1 then try tell application "System Events" set frontWindow to front window of application process "FaceTime" delay 0.6 # needed to avoid weird flickering stuff. Try increasing if you still see it on your system click button "Call" of application process "FaceTime" exit repeat end tell end try else delay 0.1 end if set t to t + 0.1 end repeat end if 594 | scriptargtype 595 | 1 596 | scriptfile 597 | 598 | type 599 | 6 600 | 601 | type 602 | alfred.workflow.action.script 603 | uid 604 | 671ABB72-7DBC-417F-BBA7-E9C68E710CD0 605 | version 606 | 2 607 | 608 | 609 | config 610 | 611 | inputstring 612 | {var:skip_confirm} 613 | matchcasesensitive 614 | 615 | matchmode 616 | 0 617 | matchstring 618 | True 619 | 620 | type 621 | alfred.workflow.utility.filter 622 | uid 623 | B855F94C-6DC2-4999-9A4C-3DF7F330CD9F 624 | version 625 | 1 626 | 627 | 628 | config 629 | 630 | matchmode 631 | 1 632 | matchstring 633 | [^0-9]? 634 | regexcaseinsensitive 635 | 636 | regexmultiline 637 | 638 | replacestring 639 | 640 | 641 | type 642 | alfred.workflow.utility.replace 643 | uid 644 | 76CD24D4-D950-445D-A8FC-D8BD4D390299 645 | version 646 | 2 647 | 648 | 649 | config 650 | 651 | argument 652 | {query} 653 | variables 654 | 655 | skip_confirm 656 | True 657 | 658 | 659 | type 660 | alfred.workflow.utility.argument 661 | uid 662 | 5C05FAE4-38A9-4B98-8863-C42A8F5CE7E5 663 | version 664 | 1 665 | 666 | 667 | config 668 | 669 | argumenttype 670 | 2 671 | keyword 672 | answer 673 | subtext 674 | 675 | text 676 | Answer Current Call 677 | withspace 678 | 679 | 680 | type 681 | alfred.workflow.input.keyword 682 | uid 683 | 2F166541-DACC-4370-81BF-8AD4764571F4 684 | version 685 | 1 686 | 687 | 688 | config 689 | 690 | anchorfields 691 | 692 | argumenttrimmode 693 | 0 694 | argumenttype 695 | 0 696 | daterange 697 | 0 698 | fields 699 | 700 | 701 | field 702 | kMDItemDisplayName 703 | not 704 | 705 | split 706 | 707 | value 708 | {query} 709 | words 710 | 711 | 712 | 713 | field 714 | kMDItemFinderComment 715 | not 716 | 717 | split 718 | 719 | value 720 | {query} 721 | words 722 | 723 | 724 | 725 | field 726 | kMDItemPhoneNumbers 727 | not 728 | 729 | split 730 | 731 | value 732 | {query} 733 | words 734 | 735 | 736 | 737 | includesystem 738 | 739 | keyword 740 | sms 741 | limit 742 | 0 743 | runningsubtext 744 | 745 | scopes 746 | 747 | ~/Library/Application Support/AddressBook 748 | 749 | sortmode 750 | 0 751 | subtext 752 | Message contact with imessage 753 | title 754 | Message [Contact Name] 755 | types 756 | 757 | com.apple.addressbook.person 758 | 759 | withspace 760 | 761 | 762 | type 763 | alfred.workflow.input.filefilter 764 | uid 765 | AA05525E-1106-40EB-8CFC-66CA0AFFF10F 766 | version 767 | 2 768 | 769 | 770 | config 771 | 772 | applescript 773 | on alfred_script(q) 774 | 775 | set contacts_running to false 776 | if application "Contacts" is running then 777 | set contacts_running to true 778 | end if 779 | 780 | tell application "Contacts" 781 | set q to text ((offset of "Metadata/" in q)+9) thru ((offset of "." in q)-1) of q 782 | set phonenumber to value of phone of person id q set phonenumber to item 1 of phonenumber 783 | end tell 784 | 785 | if contacts_running is false 786 | tell application "Contacts" to quit 787 | end if 788 | 789 | return phonenumber 790 | end alfred_script 791 | cachescript 792 | 793 | 794 | type 795 | alfred.workflow.action.applescript 796 | uid 797 | A2BE7579-08DF-4914-9F8B-59D5C429C3AA 798 | version 799 | 1 800 | 801 | 802 | config 803 | 804 | applescript 805 | on alfred_script(q) 806 | tell application "System Events" to tell process "Notification Center" if button "Accept" of windows exists then click button "Accept" of windows else return false end if end tell 807 | end alfred_script 808 | cachescript 809 | 810 | 811 | type 812 | alfred.workflow.action.applescript 813 | uid 814 | DD4F6A79-5EB5-4D42-8C8A-934D5706A827 815 | version 816 | 1 817 | 818 | 819 | config 820 | 821 | action 822 | 0 823 | argument 824 | 0 825 | focusedappvariable 826 | 827 | focusedappvariablename 828 | 829 | hotkey 830 | 0 831 | hotmod 832 | 1179648 833 | hotstring 834 | A 835 | leftcursor 836 | 837 | modsmode 838 | 0 839 | relatedAppsMode 840 | 0 841 | 842 | type 843 | alfred.workflow.trigger.hotkey 844 | uid 845 | 543F776C-68F3-4717-B11A-76848C11E84A 846 | version 847 | 2 848 | 849 | 850 | config 851 | 852 | anchorfields 853 | 854 | argumenttrimmode 855 | 0 856 | argumenttype 857 | 0 858 | daterange 859 | 0 860 | fields 861 | 862 | 863 | field 864 | kMDItemDisplayName 865 | not 866 | 867 | split 868 | 869 | value 870 | {query} 871 | words 872 | 873 | 874 | 875 | field 876 | kMDItemFinderComment 877 | not 878 | 879 | split 880 | 881 | value 882 | {query} 883 | words 884 | 885 | 886 | 887 | field 888 | kMDItemPhoneNumbers 889 | not 890 | 891 | split 892 | 893 | value 894 | {query} 895 | words 896 | 897 | 898 | 899 | includesystem 900 | 901 | keyword 902 | im 903 | limit 904 | 0 905 | runningsubtext 906 | 907 | scopes 908 | 909 | ~/Library/Application Support/AddressBook 910 | 911 | sortmode 912 | 0 913 | subtext 914 | Message contact with imessage 915 | title 916 | Message [Contact Name] 917 | types 918 | 919 | com.apple.addressbook.person 920 | 921 | withspace 922 | 923 | 924 | type 925 | alfred.workflow.input.filefilter 926 | uid 927 | 0FAC263F-D24B-4ACB-9D59-07329F75BCD0 928 | version 929 | 2 930 | 931 | 932 | config 933 | 934 | argumenttype 935 | 2 936 | keyword 937 | end 938 | subtext 939 | End or Cancel Current Call 940 | text 941 | End Call 942 | withspace 943 | 944 | 945 | type 946 | alfred.workflow.input.keyword 947 | uid 948 | BF74F4C0-C5C8-439F-80AE-05B84814B44E 949 | version 950 | 1 951 | 952 | 953 | config 954 | 955 | applescript 956 | on alfred_script(q) 957 | tell application "System Events" to tell process "Notification Center" if button "Decline" of windows exists then click button "Decline" of windows else if button "End" of windows exists then click button "End" of windows else click button "Cancel" of windows end if end tell 958 | end alfred_script 959 | cachescript 960 | 961 | 962 | type 963 | alfred.workflow.action.applescript 964 | uid 965 | 8D17B46F-4A88-42A0-9501-07FC2FE174FA 966 | version 967 | 1 968 | 969 | 970 | config 971 | 972 | concurrently 973 | 974 | escaping 975 | 127 976 | script 977 | open 'imessage://{query}' 978 | scriptargtype 979 | 0 980 | scriptfile 981 | 982 | type 983 | 0 984 | 985 | type 986 | alfred.workflow.action.script 987 | uid 988 | 86590F04-57CC-4B1E-96FF-A87CC14063DA 989 | version 990 | 2 991 | 992 | 993 | config 994 | 995 | matchmode 996 | 1 997 | matchstring 998 | [^0-9]? 999 | regexcaseinsensitive 1000 | 1001 | regexmultiline 1002 | 1003 | replacestring 1004 | 1005 | 1006 | type 1007 | alfred.workflow.utility.replace 1008 | uid 1009 | 9ECF0139-0E66-4EAC-8C62-1D55A508874E 1010 | version 1011 | 2 1012 | 1013 | 1014 | config 1015 | 1016 | argumenttype 1017 | 0 1018 | keyword 1019 | sms 1020 | subtext 1021 | sms {query} 1022 | text 1023 | sms [number] 1024 | withspace 1025 | 1026 | 1027 | type 1028 | alfred.workflow.input.keyword 1029 | uid 1030 | 1DFF0026-0410-4E09-B829-03EA6203893F 1031 | version 1032 | 1 1033 | 1034 | 1035 | config 1036 | 1037 | action 1038 | 0 1039 | argument 1040 | 0 1041 | focusedappvariable 1042 | 1043 | focusedappvariablename 1044 | 1045 | hotkey 1046 | 14 1047 | hotmod 1048 | 1179648 1049 | hotstring 1050 | E 1051 | leftcursor 1052 | 1053 | modsmode 1054 | 0 1055 | relatedAppsMode 1056 | 0 1057 | 1058 | type 1059 | alfred.workflow.trigger.hotkey 1060 | uid 1061 | D0EA7276-2A9C-4469-8589-8C56510DC31F 1062 | version 1063 | 2 1064 | 1065 | 1066 | config 1067 | 1068 | applescript 1069 | on alfred_script(q) 1070 | 1071 | -- {sys1, sys2, sys3 } = { major, minor, bugfix} 1072 | set minor_ver to system attribute "sys2" 1073 | if 13 ≤ minor_ver then 1074 | 1075 | tell application "System Events" to tell process "Notification Center" 1076 | if checkbox "Mute" of window 1 exists then 1077 | click checkbox "Mute" of window 1 1078 | end if 1079 | end tell 1080 | else 1081 | tell application "System Events" 1082 | if checkbox "Mute" of window 1 of application process "FaceTime" exists then 1083 | click checkbox "Mute" of window 1 of application process "FaceTime" 1084 | end if 1085 | end tell 1086 | end if 1087 | end alfred_script 1088 | cachescript 1089 | 1090 | 1091 | type 1092 | alfred.workflow.action.applescript 1093 | uid 1094 | 3E36110A-9B17-4E02-AF9D-FA99A6A973F4 1095 | version 1096 | 1 1097 | 1098 | 1099 | config 1100 | 1101 | argumenttype 1102 | 0 1103 | keyword 1104 | im 1105 | subtext 1106 | im {query} 1107 | text 1108 | im [number] 1109 | withspace 1110 | 1111 | 1112 | type 1113 | alfred.workflow.input.keyword 1114 | uid 1115 | 42AC172B-3284-46CC-A8B8-B5251D9644DB 1116 | version 1117 | 1 1118 | 1119 | 1120 | config 1121 | 1122 | argumenttype 1123 | 2 1124 | keyword 1125 | mute 1126 | subtext 1127 | Mutes the current call 1128 | text 1129 | Mute Call 1130 | withspace 1131 | 1132 | 1133 | type 1134 | alfred.workflow.input.keyword 1135 | uid 1136 | 5F11E541-2D4B-4B16-8D27-15CCC0EB7889 1137 | version 1138 | 1 1139 | 1140 | 1141 | config 1142 | 1143 | action 1144 | 0 1145 | argument 1146 | 0 1147 | focusedappvariable 1148 | 1149 | focusedappvariablename 1150 | 1151 | hotkey 1152 | 46 1153 | hotmod 1154 | 1179648 1155 | hotstring 1156 | M 1157 | leftcursor 1158 | 1159 | modsmode 1160 | 0 1161 | relatedAppsMode 1162 | 0 1163 | 1164 | type 1165 | alfred.workflow.trigger.hotkey 1166 | uid 1167 | FAEE1763-A240-484D-A6C4-33BC4CAF3BEC 1168 | version 1169 | 2 1170 | 1171 | 1172 | config 1173 | 1174 | concurrently 1175 | 1176 | escaping 1177 | 102 1178 | script 1179 | import sys 1180 | import time 1181 | from Quartz.CoreGraphics import * 1182 | def mouseEvent(type, posx, posy): 1183 | theEvent = CGEventCreateMouseEvent(None, type, (posx,posy), kCGMouseButtonLeft) 1184 | CGEventPost(kCGHIDEventTap, theEvent) 1185 | def mousemove(posx,posy): 1186 | mouseEvent(kCGEventMouseMoved, posx,posy); 1187 | def mouseclick(posx,posy): 1188 | mouseEvent(kCGEventLeftMouseDown, posx,posy); 1189 | mouseEvent(kCGEventLeftMouseUp, posx,posy); 1190 | ourEvent = CGEventCreate(None); 1191 | currentpos=CGEventGetLocation(ourEvent); # Save current mouse position 1192 | mouseclick(1664,88); 1193 | mousemove(int(currentpos.x),int(currentpos.y)); 1194 | scriptargtype 1195 | 1 1196 | scriptfile 1197 | 1198 | type 1199 | 3 1200 | 1201 | type 1202 | alfred.workflow.action.script 1203 | uid 1204 | F81E39F9-3AB1-4299-91D7-1CDADEB3AC01 1205 | version 1206 | 2 1207 | 1208 | 1209 | readme 1210 | I wanted Alfred to better integrate with continuity features in the iPhone so I made this. Released under Apache License 2.0. 1211 | 1212 | Written by @amoose136 2015-2018. 1213 | 1214 | Modified and maintained by Conner Stephens (@Hirvi74) 2019. 1215 | 1216 | Released under Apache License 2.0. 1217 | uidata 1218 | 1219 | 0FAC263F-D24B-4ACB-9D59-07329F75BCD0 1220 | 1221 | xpos 1222 | 570 1223 | ypos 1224 | 550 1225 | 1226 | 10C9547D-84B9-43F9-9DA0-6AEC76C58A94 1227 | 1228 | colorindex 1229 | 4 1230 | note 1231 | skip confirmation 1232 | xpos 1233 | 290 1234 | ypos 1235 | 120 1236 | 1237 | 126EBE51-784E-415A-8D65-C85B217DF930 1238 | 1239 | colorindex 1240 | 4 1241 | note 1242 | show confirmation popup before calling 1243 | xpos 1244 | 290 1245 | ypos 1246 | 10 1247 | 1248 | 16865643-D0A0-48DA-961D-053770CCBB23 1249 | 1250 | colorindex 1251 | 4 1252 | note 1253 | click call 1254 | xpos 1255 | 910 1256 | ypos 1257 | 90 1258 | 1259 | 1DFF0026-0410-4E09-B829-03EA6203893F 1260 | 1261 | xpos 1262 | 580 1263 | ypos 1264 | 680 1265 | 1266 | 2F166541-DACC-4370-81BF-8AD4764571F4 1267 | 1268 | note 1269 | Answers the current call. 1270 | xpos 1271 | 30 1272 | ypos 1273 | 400 1274 | 1275 | 3E36110A-9B17-4E02-AF9D-FA99A6A973F4 1276 | 1277 | xpos 1278 | 370 1279 | ypos 1280 | 790 1281 | 1282 | 42AC172B-3284-46CC-A8B8-B5251D9644DB 1283 | 1284 | xpos 1285 | 580 1286 | ypos 1287 | 810 1288 | 1289 | 543F776C-68F3-4717-B11A-76848C11E84A 1290 | 1291 | xpos 1292 | 170 1293 | ypos 1294 | 510 1295 | 1296 | 5C05FAE4-38A9-4B98-8863-C42A8F5CE7E5 1297 | 1298 | colorindex 1299 | 4 1300 | note 1301 | skip confirmation 1302 | xpos 1303 | 290 1304 | ypos 1305 | 350 1306 | 1307 | 5F11E541-2D4B-4B16-8D27-15CCC0EB7889 1308 | 1309 | note 1310 | This keyword will mute/unmute the current call. 1311 | 1312 | * For some reason, on MacOS 10.14 this needs to be used twice (per call) for the GUI mute button symbol to toggle its color. However, based on my testing, it works regardless of the GUI changing. Again, this only happens on the first time 'mute' is invoked.* 1313 | xpos 1314 | 40 1315 | ypos 1316 | 870 1317 | 1318 | 671ABB72-7DBC-417F-BBA7-E9C68E710CD0 1319 | 1320 | xpos 1321 | 910 1322 | ypos 1323 | 270 1324 | 1325 | 76CD24D4-D950-445D-A8FC-D8BD4D390299 1326 | 1327 | colorindex 1328 | 4 1329 | note 1330 | Remove letters and spaces (leaving numbers) 1331 | xpos 1332 | 460 1333 | ypos 1334 | 320 1335 | 1336 | 86590F04-57CC-4B1E-96FF-A87CC14063DA 1337 | 1338 | colorindex 1339 | 7 1340 | note 1341 | send to Messages.app 1342 | xpos 1343 | 1020 1344 | ypos 1345 | 620 1346 | 1347 | 8D17B46F-4A88-42A0-9501-07FC2FE174FA 1348 | 1349 | xpos 1350 | 360 1351 | ypos 1352 | 620 1353 | 1354 | 953EA110-8004-4091-9985-F0AF57D696C4 1355 | 1356 | note 1357 | For numbers 1358 | xpos 1359 | 40 1360 | ypos 1361 | 190 1362 | 1363 | 9ECF0139-0E66-4EAC-8C62-1D55A508874E 1364 | 1365 | colorindex 1366 | 7 1367 | note 1368 | Remove letters and spaces (leaving numbers) 1369 | xpos 1370 | 870 1371 | ypos 1372 | 670 1373 | 1374 | A2BE7579-08DF-4914-9F8B-59D5C429C3AA 1375 | 1376 | colorindex 1377 | 7 1378 | note 1379 | Extract number 1380 | xpos 1381 | 770 1382 | ypos 1383 | 450 1384 | 1385 | AA05525E-1106-40EB-8CFC-66CA0AFFF10F 1386 | 1387 | xpos 1388 | 570 1389 | ypos 1390 | 420 1391 | 1392 | B855F94C-6DC2-4999-9A4C-3DF7F330CD9F 1393 | 1394 | colorindex 1395 | 4 1396 | xpos 1397 | 800 1398 | ypos 1399 | 300 1400 | 1401 | BF74F4C0-C5C8-439F-80AE-05B84814B44E 1402 | 1403 | note 1404 | The 'end' keyword will also end or cancel depending on which option is available. 1405 | xpos 1406 | 30 1407 | ypos 1408 | 600 1409 | 1410 | D0EA7276-2A9C-4469-8589-8C56510DC31F 1411 | 1412 | xpos 1413 | 180 1414 | ypos 1415 | 710 1416 | 1417 | D5DDD534-314A-473C-A898-AEC5C234FC52 1418 | 1419 | colorindex 1420 | 4 1421 | note 1422 | Extract number 1423 | xpos 1424 | 400 1425 | ypos 1426 | 20 1427 | 1428 | D675C802-AD81-43EF-8415-4D4FE2425415 1429 | 1430 | colorindex 1431 | 4 1432 | note 1433 | show confirmation popup before calling 1434 | xpos 1435 | 290 1436 | ypos 1437 | 210 1438 | 1439 | DD4F6A79-5EB5-4D42-8C8A-934D5706A827 1440 | 1441 | xpos 1442 | 360 1443 | ypos 1444 | 460 1445 | 1446 | E63F1F76-116F-4C39-A91D-7A1BD00B991B 1447 | 1448 | colorindex 1449 | 4 1450 | note 1451 | Make call button appear 1452 | xpos 1453 | 620 1454 | ypos 1455 | 130 1456 | 1457 | F81E39F9-3AB1-4299-91D7-1CDADEB3AC01 1458 | 1459 | note 1460 | This Python script fixes the mute issue but is slow. 1461 | xpos 1462 | 370 1463 | ypos 1464 | 980 1465 | 1466 | FAEE1763-A240-484D-A6C4-33BC4CAF3BEC 1467 | 1468 | xpos 1469 | 180 1470 | ypos 1471 | 950 1472 | 1473 | FF8E3BE3-2E94-4801-8A01-9AA8DE17340B 1474 | 1475 | note 1476 | For contacts 1477 | xpos 1478 | 40 1479 | ypos 1480 | 30 1481 | 1482 | 1483 | variablesdontexport 1484 | 1485 | version 1486 | 2.22 1487 | webaddress 1488 | 1489 | 1490 | 1491 | -------------------------------------------------------------------------------- /src/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/message.png -------------------------------------------------------------------------------- /src/search.py: -------------------------------------------------------------------------------- 1 | import sys, os, plistlib as pl 2 | import workflow 3 | from workflow import Workflow, ICON_INFO 4 | from workflow.background import run_in_background, is_running 5 | from plistlib import readPlist 6 | 7 | def main(wf): 8 | wf.send_feedback() 9 | 10 | if __name__ == '__main__': 11 | wf = Workflow(capture_args=True) 12 | log=wf.logger 13 | wf.magic_prefix='wf:' 14 | sys.exit(wf.run(main)) -------------------------------------------------------------------------------- /src/workflow/.alfredversionchecked: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/.alfredversionchecked -------------------------------------------------------------------------------- /src/workflow/Notify.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/Notify.tgz -------------------------------------------------------------------------------- /src/workflow/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2014-02-15 9 | # 10 | 11 | """A helper library for `Alfred `_ workflows.""" 12 | 13 | import os 14 | 15 | # Workflow objects 16 | from .workflow import Workflow, manager 17 | from .workflow3 import Variables, Workflow3 18 | 19 | # Exceptions 20 | from .workflow import PasswordNotFound, KeychainError 21 | 22 | # Icons 23 | from .workflow import ( 24 | ICON_ACCOUNT, 25 | ICON_BURN, 26 | ICON_CLOCK, 27 | ICON_COLOR, 28 | ICON_COLOUR, 29 | ICON_EJECT, 30 | ICON_ERROR, 31 | ICON_FAVORITE, 32 | ICON_FAVOURITE, 33 | ICON_GROUP, 34 | ICON_HELP, 35 | ICON_HOME, 36 | ICON_INFO, 37 | ICON_NETWORK, 38 | ICON_NOTE, 39 | ICON_SETTINGS, 40 | ICON_SWIRL, 41 | ICON_SWITCH, 42 | ICON_SYNC, 43 | ICON_TRASH, 44 | ICON_USER, 45 | ICON_WARNING, 46 | ICON_WEB, 47 | ) 48 | 49 | # Filter matching rules 50 | from .workflow import ( 51 | MATCH_ALL, 52 | MATCH_ALLCHARS, 53 | MATCH_ATOM, 54 | MATCH_CAPITALS, 55 | MATCH_INITIALS, 56 | MATCH_INITIALS_CONTAIN, 57 | MATCH_INITIALS_STARTSWITH, 58 | MATCH_STARTSWITH, 59 | MATCH_SUBSTRING, 60 | ) 61 | 62 | 63 | __title__ = 'Alfred-Workflow' 64 | __version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read() 65 | __author__ = 'Dean Jackson' 66 | __licence__ = 'MIT' 67 | __copyright__ = 'Copyright 2014-2017 Dean Jackson' 68 | 69 | __all__ = [ 70 | 'Variables', 71 | 'Workflow', 72 | 'Workflow3', 73 | 'manager', 74 | 'PasswordNotFound', 75 | 'KeychainError', 76 | 'ICON_ACCOUNT', 77 | 'ICON_BURN', 78 | 'ICON_CLOCK', 79 | 'ICON_COLOR', 80 | 'ICON_COLOUR', 81 | 'ICON_EJECT', 82 | 'ICON_ERROR', 83 | 'ICON_FAVORITE', 84 | 'ICON_FAVOURITE', 85 | 'ICON_GROUP', 86 | 'ICON_HELP', 87 | 'ICON_HOME', 88 | 'ICON_INFO', 89 | 'ICON_NETWORK', 90 | 'ICON_NOTE', 91 | 'ICON_SETTINGS', 92 | 'ICON_SWIRL', 93 | 'ICON_SWITCH', 94 | 'ICON_SYNC', 95 | 'ICON_TRASH', 96 | 'ICON_USER', 97 | 'ICON_WARNING', 98 | 'ICON_WEB', 99 | 'MATCH_ALL', 100 | 'MATCH_ALLCHARS', 101 | 'MATCH_ATOM', 102 | 'MATCH_CAPITALS', 103 | 'MATCH_INITIALS', 104 | 'MATCH_INITIALS_CONTAIN', 105 | 'MATCH_INITIALS_STARTSWITH', 106 | 'MATCH_STARTSWITH', 107 | 'MATCH_SUBSTRING', 108 | ] 109 | -------------------------------------------------------------------------------- /src/workflow/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/__init__.pyc -------------------------------------------------------------------------------- /src/workflow/background.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 deanishe@deanishe.net 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2014-04-06 9 | # 10 | 11 | """ 12 | This module provides an API to run commands in background processes. 13 | Combine with the :ref:`caching API ` to work from cached data 14 | while you fetch fresh data in the background. 15 | 16 | See :ref:`the User Manual ` for more information 17 | and examples. 18 | """ 19 | 20 | from __future__ import print_function, unicode_literals 21 | 22 | import signal 23 | import sys 24 | import os 25 | import subprocess 26 | import pickle 27 | 28 | from workflow import Workflow 29 | 30 | __all__ = ['is_running', 'run_in_background'] 31 | 32 | _wf = None 33 | 34 | 35 | def wf(): 36 | global _wf 37 | if _wf is None: 38 | _wf = Workflow() 39 | return _wf 40 | 41 | 42 | def _log(): 43 | return wf().logger 44 | 45 | 46 | def _arg_cache(name): 47 | """Return path to pickle cache file for arguments. 48 | 49 | :param name: name of task 50 | :type name: ``unicode`` 51 | :returns: Path to cache file 52 | :rtype: ``unicode`` filepath 53 | 54 | """ 55 | return wf().cachefile(name + '.argcache') 56 | 57 | 58 | def _pid_file(name): 59 | """Return path to PID file for ``name``. 60 | 61 | :param name: name of task 62 | :type name: ``unicode`` 63 | :returns: Path to PID file for task 64 | :rtype: ``unicode`` filepath 65 | 66 | """ 67 | return wf().cachefile(name + '.pid') 68 | 69 | 70 | def _process_exists(pid): 71 | """Check if a process with PID ``pid`` exists. 72 | 73 | :param pid: PID to check 74 | :type pid: ``int`` 75 | :returns: ``True`` if process exists, else ``False`` 76 | :rtype: ``Boolean`` 77 | 78 | """ 79 | try: 80 | os.kill(pid, 0) 81 | except OSError: # not running 82 | return False 83 | return True 84 | 85 | 86 | def _job_pid(name): 87 | """Get PID of job or `None` if job does not exist. 88 | 89 | Args: 90 | name (str): Name of job. 91 | 92 | Returns: 93 | int: PID of job process (or `None` if job doesn't exist). 94 | """ 95 | pidfile = _pid_file(name) 96 | if not os.path.exists(pidfile): 97 | return 98 | 99 | with open(pidfile, 'rb') as fp: 100 | pid = int(fp.read()) 101 | 102 | if _process_exists(pid): 103 | return pid 104 | 105 | try: 106 | os.unlink(pidfile) 107 | except Exception: # pragma: no cover 108 | pass 109 | 110 | 111 | def is_running(name): 112 | """Test whether task ``name`` is currently running. 113 | 114 | :param name: name of task 115 | :type name: unicode 116 | :returns: ``True`` if task with name ``name`` is running, else ``False`` 117 | :rtype: bool 118 | 119 | """ 120 | if _job_pid(name) is not None: 121 | return True 122 | 123 | return False 124 | 125 | 126 | def _background(pidfile, stdin='/dev/null', stdout='/dev/null', 127 | stderr='/dev/null'): # pragma: no cover 128 | """Fork the current process into a background daemon. 129 | 130 | :param pidfile: file to write PID of daemon process to. 131 | :type pidfile: filepath 132 | :param stdin: where to read input 133 | :type stdin: filepath 134 | :param stdout: where to write stdout output 135 | :type stdout: filepath 136 | :param stderr: where to write stderr output 137 | :type stderr: filepath 138 | 139 | """ 140 | def _fork_and_exit_parent(errmsg, wait=False, write=False): 141 | try: 142 | pid = os.fork() 143 | if pid > 0: 144 | if write: # write PID of child process to `pidfile` 145 | tmp = pidfile + '.tmp' 146 | with open(tmp, 'wb') as fp: 147 | fp.write(str(pid)) 148 | os.rename(tmp, pidfile) 149 | if wait: # wait for child process to exit 150 | os.waitpid(pid, 0) 151 | os._exit(0) 152 | except OSError as err: 153 | _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror) 154 | raise err 155 | 156 | # Do first fork and wait for second fork to finish. 157 | _fork_and_exit_parent('fork #1 failed', wait=True) 158 | 159 | # Decouple from parent environment. 160 | os.chdir(wf().workflowdir) 161 | os.setsid() 162 | 163 | # Do second fork and write PID to pidfile. 164 | _fork_and_exit_parent('fork #2 failed', write=True) 165 | 166 | # Now I am a daemon! 167 | # Redirect standard file descriptors. 168 | si = open(stdin, 'r', 0) 169 | so = open(stdout, 'a+', 0) 170 | se = open(stderr, 'a+', 0) 171 | if hasattr(sys.stdin, 'fileno'): 172 | os.dup2(si.fileno(), sys.stdin.fileno()) 173 | if hasattr(sys.stdout, 'fileno'): 174 | os.dup2(so.fileno(), sys.stdout.fileno()) 175 | if hasattr(sys.stderr, 'fileno'): 176 | os.dup2(se.fileno(), sys.stderr.fileno()) 177 | 178 | 179 | def kill(name, sig=signal.SIGTERM): 180 | """Send a signal to job ``name`` via :func:`os.kill`. 181 | 182 | .. versionadded:: 1.29 183 | 184 | Args: 185 | name (str): Name of the job 186 | sig (int, optional): Signal to send (default: SIGTERM) 187 | 188 | Returns: 189 | bool: `False` if job isn't running, `True` if signal was sent. 190 | """ 191 | pid = _job_pid(name) 192 | if pid is None: 193 | return False 194 | 195 | os.kill(pid, sig) 196 | return True 197 | 198 | 199 | def run_in_background(name, args, **kwargs): 200 | r"""Cache arguments then call this script again via :func:`subprocess.call`. 201 | 202 | :param name: name of job 203 | :type name: unicode 204 | :param args: arguments passed as first argument to :func:`subprocess.call` 205 | :param \**kwargs: keyword arguments to :func:`subprocess.call` 206 | :returns: exit code of sub-process 207 | :rtype: int 208 | 209 | When you call this function, it caches its arguments and then calls 210 | ``background.py`` in a subprocess. The Python subprocess will load the 211 | cached arguments, fork into the background, and then run the command you 212 | specified. 213 | 214 | This function will return as soon as the ``background.py`` subprocess has 215 | forked, returning the exit code of *that* process (i.e. not of the command 216 | you're trying to run). 217 | 218 | If that process fails, an error will be written to the log file. 219 | 220 | If a process is already running under the same name, this function will 221 | return immediately and will not run the specified command. 222 | 223 | """ 224 | if is_running(name): 225 | _log().info('[%s] job already running', name) 226 | return 227 | 228 | argcache = _arg_cache(name) 229 | 230 | # Cache arguments 231 | with open(argcache, 'wb') as fp: 232 | pickle.dump({'args': args, 'kwargs': kwargs}, fp) 233 | _log().debug('[%s] command cached: %s', name, argcache) 234 | 235 | # Call this script 236 | cmd = ['/usr/bin/python', __file__, name] 237 | _log().debug('[%s] passing job to background runner: %r', name, cmd) 238 | retcode = subprocess.call(cmd) 239 | 240 | if retcode: # pragma: no cover 241 | _log().error('[%s] background runner failed with %d', name, retcode) 242 | else: 243 | _log().debug('[%s] background job started', name) 244 | 245 | return retcode 246 | 247 | 248 | def main(wf): # pragma: no cover 249 | """Run command in a background process. 250 | 251 | Load cached arguments, fork into background, then call 252 | :meth:`subprocess.call` with cached arguments. 253 | 254 | """ 255 | log = wf.logger 256 | name = wf.args[0] 257 | argcache = _arg_cache(name) 258 | if not os.path.exists(argcache): 259 | msg = '[{0}] command cache not found: {1}'.format(name, argcache) 260 | log.critical(msg) 261 | raise IOError(msg) 262 | 263 | # Fork to background and run command 264 | pidfile = _pid_file(name) 265 | _background(pidfile) 266 | 267 | # Load cached arguments 268 | with open(argcache, 'rb') as fp: 269 | data = pickle.load(fp) 270 | 271 | # Cached arguments 272 | args = data['args'] 273 | kwargs = data['kwargs'] 274 | 275 | # Delete argument cache file 276 | os.unlink(argcache) 277 | 278 | try: 279 | # Run the command 280 | log.debug('[%s] running command: %r', name, args) 281 | 282 | retcode = subprocess.call(args, **kwargs) 283 | 284 | if retcode: 285 | log.error('[%s] command failed with status %d', name, retcode) 286 | finally: 287 | os.unlink(pidfile) 288 | 289 | log.debug('[%s] job complete', name) 290 | 291 | 292 | if __name__ == '__main__': # pragma: no cover 293 | wf().run(main) 294 | -------------------------------------------------------------------------------- /src/workflow/background.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/background.pyc -------------------------------------------------------------------------------- /src/workflow/notify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2015 deanishe@deanishe.net 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2015-11-26 9 | # 10 | 11 | # TODO: Exclude this module from test and code coverage in py2.6 12 | 13 | """ 14 | Post notifications via the macOS Notification Center. This feature 15 | is only available on Mountain Lion (10.8) and later. It will 16 | silently fail on older systems. 17 | 18 | The main API is a single function, :func:`~workflow.notify.notify`. 19 | 20 | It works by copying a simple application to your workflow's data 21 | directory. It replaces the application's icon with your workflow's 22 | icon and then calls the application to post notifications. 23 | """ 24 | 25 | from __future__ import print_function, unicode_literals 26 | 27 | import os 28 | import plistlib 29 | import shutil 30 | import subprocess 31 | import sys 32 | import tarfile 33 | import tempfile 34 | import uuid 35 | 36 | import workflow 37 | 38 | 39 | _wf = None 40 | _log = None 41 | 42 | 43 | #: Available system sounds from System Preferences > Sound > Sound Effects 44 | SOUNDS = ( 45 | 'Basso', 46 | 'Blow', 47 | 'Bottle', 48 | 'Frog', 49 | 'Funk', 50 | 'Glass', 51 | 'Hero', 52 | 'Morse', 53 | 'Ping', 54 | 'Pop', 55 | 'Purr', 56 | 'Sosumi', 57 | 'Submarine', 58 | 'Tink', 59 | ) 60 | 61 | 62 | def wf(): 63 | """Return Workflow object for this module. 64 | 65 | Returns: 66 | workflow.Workflow: Workflow object for current workflow. 67 | """ 68 | global _wf 69 | if _wf is None: 70 | _wf = workflow.Workflow() 71 | return _wf 72 | 73 | 74 | def log(): 75 | """Return logger for this module. 76 | 77 | Returns: 78 | logging.Logger: Logger for this module. 79 | """ 80 | global _log 81 | if _log is None: 82 | _log = wf().logger 83 | return _log 84 | 85 | 86 | def notifier_program(): 87 | """Return path to notifier applet executable. 88 | 89 | Returns: 90 | unicode: Path to Notify.app ``applet`` executable. 91 | """ 92 | return wf().datafile('Notify.app/Contents/MacOS/applet') 93 | 94 | 95 | def notifier_icon_path(): 96 | """Return path to icon file in installed Notify.app. 97 | 98 | Returns: 99 | unicode: Path to ``applet.icns`` within the app bundle. 100 | """ 101 | return wf().datafile('Notify.app/Contents/Resources/applet.icns') 102 | 103 | 104 | def install_notifier(): 105 | """Extract ``Notify.app`` from the workflow to data directory. 106 | 107 | Changes the bundle ID of the installed app and gives it the 108 | workflow's icon. 109 | """ 110 | archive = os.path.join(os.path.dirname(__file__), 'Notify.tgz') 111 | destdir = wf().datadir 112 | app_path = os.path.join(destdir, 'Notify.app') 113 | n = notifier_program() 114 | log().debug('installing Notify.app to %r ...', destdir) 115 | # z = zipfile.ZipFile(archive, 'r') 116 | # z.extractall(destdir) 117 | tgz = tarfile.open(archive, 'r:gz') 118 | tgz.extractall(destdir) 119 | assert os.path.exists(n), \ 120 | 'Notify.app could not be installed in %s' % destdir 121 | 122 | # Replace applet icon 123 | icon = notifier_icon_path() 124 | workflow_icon = wf().workflowfile('icon.png') 125 | if os.path.exists(icon): 126 | os.unlink(icon) 127 | 128 | png_to_icns(workflow_icon, icon) 129 | 130 | # Set file icon 131 | # PyObjC isn't available for 2.6, so this is 2.7 only. Actually, 132 | # none of this code will "work" on pre-10.8 systems. Let it run 133 | # until I figure out a better way of excluding this module 134 | # from coverage in py2.6. 135 | if sys.version_info >= (2, 7): # pragma: no cover 136 | from AppKit import NSWorkspace, NSImage 137 | 138 | ws = NSWorkspace.sharedWorkspace() 139 | img = NSImage.alloc().init() 140 | img.initWithContentsOfFile_(icon) 141 | ws.setIcon_forFile_options_(img, app_path, 0) 142 | 143 | # Change bundle ID of installed app 144 | ip_path = os.path.join(app_path, 'Contents/Info.plist') 145 | bundle_id = '{0}.{1}'.format(wf().bundleid, uuid.uuid4().hex) 146 | data = plistlib.readPlist(ip_path) 147 | log().debug('changing bundle ID to %r', bundle_id) 148 | data['CFBundleIdentifier'] = bundle_id 149 | plistlib.writePlist(data, ip_path) 150 | 151 | 152 | def validate_sound(sound): 153 | """Coerce ``sound`` to valid sound name. 154 | 155 | Returns ``None`` for invalid sounds. Sound names can be found 156 | in ``System Preferences > Sound > Sound Effects``. 157 | 158 | Args: 159 | sound (str): Name of system sound. 160 | 161 | Returns: 162 | str: Proper name of sound or ``None``. 163 | """ 164 | if not sound: 165 | return None 166 | 167 | # Case-insensitive comparison of `sound` 168 | if sound.lower() in [s.lower() for s in SOUNDS]: 169 | # Title-case is correct for all system sounds as of macOS 10.11 170 | return sound.title() 171 | return None 172 | 173 | 174 | def notify(title='', text='', sound=None): 175 | """Post notification via Notify.app helper. 176 | 177 | Args: 178 | title (str, optional): Notification title. 179 | text (str, optional): Notification body text. 180 | sound (str, optional): Name of sound to play. 181 | 182 | Raises: 183 | ValueError: Raised if both ``title`` and ``text`` are empty. 184 | 185 | Returns: 186 | bool: ``True`` if notification was posted, else ``False``. 187 | """ 188 | if title == text == '': 189 | raise ValueError('Empty notification') 190 | 191 | sound = validate_sound(sound) or '' 192 | 193 | n = notifier_program() 194 | 195 | if not os.path.exists(n): 196 | install_notifier() 197 | 198 | env = os.environ.copy() 199 | enc = 'utf-8' 200 | env['NOTIFY_TITLE'] = title.encode(enc) 201 | env['NOTIFY_MESSAGE'] = text.encode(enc) 202 | env['NOTIFY_SOUND'] = sound.encode(enc) 203 | cmd = [n] 204 | retcode = subprocess.call(cmd, env=env) 205 | if retcode == 0: 206 | return True 207 | 208 | log().error('Notify.app exited with status {0}.'.format(retcode)) 209 | return False 210 | 211 | 212 | def convert_image(inpath, outpath, size): 213 | """Convert an image file using ``sips``. 214 | 215 | Args: 216 | inpath (str): Path of source file. 217 | outpath (str): Path to destination file. 218 | size (int): Width and height of destination image in pixels. 219 | 220 | Raises: 221 | RuntimeError: Raised if ``sips`` exits with non-zero status. 222 | """ 223 | cmd = [ 224 | b'sips', 225 | b'-z', str(size), str(size), 226 | inpath, 227 | b'--out', outpath] 228 | # log().debug(cmd) 229 | with open(os.devnull, 'w') as pipe: 230 | retcode = subprocess.call(cmd, stdout=pipe, stderr=subprocess.STDOUT) 231 | 232 | if retcode != 0: 233 | raise RuntimeError('sips exited with %d' % retcode) 234 | 235 | 236 | def png_to_icns(png_path, icns_path): 237 | """Convert PNG file to ICNS using ``iconutil``. 238 | 239 | Create an iconset from the source PNG file. Generate PNG files 240 | in each size required by macOS, then call ``iconutil`` to turn 241 | them into a single ICNS file. 242 | 243 | Args: 244 | png_path (str): Path to source PNG file. 245 | icns_path (str): Path to destination ICNS file. 246 | 247 | Raises: 248 | RuntimeError: Raised if ``iconutil`` or ``sips`` fail. 249 | """ 250 | tempdir = tempfile.mkdtemp(prefix='aw-', dir=wf().datadir) 251 | 252 | try: 253 | iconset = os.path.join(tempdir, 'Icon.iconset') 254 | 255 | assert not os.path.exists(iconset), \ 256 | 'iconset already exists: ' + iconset 257 | os.makedirs(iconset) 258 | 259 | # Copy source icon to icon set and generate all the other 260 | # sizes needed 261 | configs = [] 262 | for i in (16, 32, 128, 256, 512): 263 | configs.append(('icon_{0}x{0}.png'.format(i), i)) 264 | configs.append((('icon_{0}x{0}@2x.png'.format(i), i * 2))) 265 | 266 | shutil.copy(png_path, os.path.join(iconset, 'icon_256x256.png')) 267 | shutil.copy(png_path, os.path.join(iconset, 'icon_128x128@2x.png')) 268 | 269 | for name, size in configs: 270 | outpath = os.path.join(iconset, name) 271 | if os.path.exists(outpath): 272 | continue 273 | convert_image(png_path, outpath, size) 274 | 275 | cmd = [ 276 | b'iconutil', 277 | b'-c', b'icns', 278 | b'-o', icns_path, 279 | iconset] 280 | 281 | retcode = subprocess.call(cmd) 282 | if retcode != 0: 283 | raise RuntimeError('iconset exited with %d' % retcode) 284 | 285 | assert os.path.exists(icns_path), \ 286 | 'generated ICNS file not found: ' + repr(icns_path) 287 | finally: 288 | try: 289 | shutil.rmtree(tempdir) 290 | except OSError: # pragma: no cover 291 | pass 292 | 293 | 294 | if __name__ == '__main__': # pragma: nocover 295 | # Simple command-line script to test module with 296 | # This won't work on 2.6, as `argparse` isn't available 297 | # by default. 298 | import argparse 299 | 300 | from unicodedata import normalize 301 | 302 | def ustr(s): 303 | """Coerce `s` to normalised Unicode.""" 304 | return normalize('NFD', s.decode('utf-8')) 305 | 306 | p = argparse.ArgumentParser() 307 | p.add_argument('-p', '--png', help="PNG image to convert to ICNS.") 308 | p.add_argument('-l', '--list-sounds', help="Show available sounds.", 309 | action='store_true') 310 | p.add_argument('-t', '--title', 311 | help="Notification title.", type=ustr, 312 | default='') 313 | p.add_argument('-s', '--sound', type=ustr, 314 | help="Optional notification sound.", default='') 315 | p.add_argument('text', type=ustr, 316 | help="Notification body text.", default='', nargs='?') 317 | o = p.parse_args() 318 | 319 | # List available sounds 320 | if o.list_sounds: 321 | for sound in SOUNDS: 322 | print(sound) 323 | sys.exit(0) 324 | 325 | # Convert PNG to ICNS 326 | if o.png: 327 | icns = os.path.join( 328 | os.path.dirname(o.png), 329 | os.path.splitext(os.path.basename(o.png))[0] + '.icns') 330 | 331 | print('converting {0!r} to {1!r} ...'.format(o.png, icns), 332 | file=sys.stderr) 333 | 334 | assert not os.path.exists(icns), \ 335 | 'destination file already exists: ' + icns 336 | 337 | png_to_icns(o.png, icns) 338 | sys.exit(0) 339 | 340 | # Post notification 341 | if o.title == o.text == '': 342 | print('ERROR: empty notification.', file=sys.stderr) 343 | sys.exit(1) 344 | else: 345 | notify(o.title, o.text, o.sound) 346 | -------------------------------------------------------------------------------- /src/workflow/update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 Fabio Niephaus , 5 | # Dean Jackson 6 | # 7 | # MIT Licence. See http://opensource.org/licenses/MIT 8 | # 9 | # Created on 2014-08-16 10 | # 11 | 12 | """Self-updating from GitHub. 13 | 14 | .. versionadded:: 1.9 15 | 16 | .. note:: 17 | 18 | This module is not intended to be used directly. Automatic updates 19 | are controlled by the ``update_settings`` :class:`dict` passed to 20 | :class:`~workflow.workflow.Workflow` objects. 21 | 22 | """ 23 | 24 | from __future__ import print_function, unicode_literals 25 | 26 | import os 27 | import tempfile 28 | import re 29 | import subprocess 30 | 31 | import workflow 32 | import web 33 | 34 | # __all__ = [] 35 | 36 | 37 | RELEASES_BASE = 'https://api.github.com/repos/{0}/releases' 38 | 39 | 40 | _wf = None 41 | 42 | 43 | def wf(): 44 | """Lazy `Workflow` object.""" 45 | global _wf 46 | if _wf is None: 47 | _wf = workflow.Workflow() 48 | return _wf 49 | 50 | 51 | class Version(object): 52 | """Mostly semantic versioning. 53 | 54 | The main difference to proper :ref:`semantic versioning ` 55 | is that this implementation doesn't require a minor or patch version. 56 | 57 | Version strings may also be prefixed with "v", e.g.: 58 | 59 | >>> v = Version('v1.1.1') 60 | >>> v.tuple 61 | (1, 1, 1, '') 62 | 63 | >>> v = Version('2.0') 64 | >>> v.tuple 65 | (2, 0, 0, '') 66 | 67 | >>> Version('3.1-beta').tuple 68 | (3, 1, 0, 'beta') 69 | 70 | >>> Version('1.0.1') > Version('0.0.1') 71 | True 72 | """ 73 | 74 | #: Match version and pre-release/build information in version strings 75 | match_version = re.compile(r'([0-9\.]+)(.+)?').match 76 | 77 | def __init__(self, vstr): 78 | """Create new `Version` object. 79 | 80 | Args: 81 | vstr (basestring): Semantic version string. 82 | """ 83 | self.vstr = vstr 84 | self.major = 0 85 | self.minor = 0 86 | self.patch = 0 87 | self.suffix = '' 88 | self.build = '' 89 | self._parse(vstr) 90 | 91 | def _parse(self, vstr): 92 | if vstr.startswith('v'): 93 | m = self.match_version(vstr[1:]) 94 | else: 95 | m = self.match_version(vstr) 96 | if not m: 97 | raise ValueError('invalid version number: {0}'.format(vstr)) 98 | 99 | version, suffix = m.groups() 100 | parts = self._parse_dotted_string(version) 101 | self.major = parts.pop(0) 102 | if len(parts): 103 | self.minor = parts.pop(0) 104 | if len(parts): 105 | self.patch = parts.pop(0) 106 | if not len(parts) == 0: 107 | raise ValueError('invalid version (too long) : {0}'.format(vstr)) 108 | 109 | if suffix: 110 | # Build info 111 | idx = suffix.find('+') 112 | if idx > -1: 113 | self.build = suffix[idx+1:] 114 | suffix = suffix[:idx] 115 | if suffix: 116 | if not suffix.startswith('-'): 117 | raise ValueError( 118 | 'suffix must start with - : {0}'.format(suffix)) 119 | self.suffix = suffix[1:] 120 | 121 | # wf().logger.debug('version str `{}` -> {}'.format(vstr, repr(self))) 122 | 123 | def _parse_dotted_string(self, s): 124 | """Parse string ``s`` into list of ints and strings.""" 125 | parsed = [] 126 | parts = s.split('.') 127 | for p in parts: 128 | if p.isdigit(): 129 | p = int(p) 130 | parsed.append(p) 131 | return parsed 132 | 133 | @property 134 | def tuple(self): 135 | """Version number as a tuple of major, minor, patch, pre-release.""" 136 | return (self.major, self.minor, self.patch, self.suffix) 137 | 138 | def __lt__(self, other): 139 | """Implement comparison.""" 140 | if not isinstance(other, Version): 141 | raise ValueError('not a Version instance: {0!r}'.format(other)) 142 | t = self.tuple[:3] 143 | o = other.tuple[:3] 144 | if t < o: 145 | return True 146 | if t == o: # We need to compare suffixes 147 | if self.suffix and not other.suffix: 148 | return True 149 | if other.suffix and not self.suffix: 150 | return False 151 | return (self._parse_dotted_string(self.suffix) < 152 | self._parse_dotted_string(other.suffix)) 153 | # t > o 154 | return False 155 | 156 | def __eq__(self, other): 157 | """Implement comparison.""" 158 | if not isinstance(other, Version): 159 | raise ValueError('not a Version instance: {0!r}'.format(other)) 160 | return self.tuple == other.tuple 161 | 162 | def __ne__(self, other): 163 | """Implement comparison.""" 164 | return not self.__eq__(other) 165 | 166 | def __gt__(self, other): 167 | """Implement comparison.""" 168 | if not isinstance(other, Version): 169 | raise ValueError('not a Version instance: {0!r}'.format(other)) 170 | return other.__lt__(self) 171 | 172 | def __le__(self, other): 173 | """Implement comparison.""" 174 | if not isinstance(other, Version): 175 | raise ValueError('not a Version instance: {0!r}'.format(other)) 176 | return not other.__lt__(self) 177 | 178 | def __ge__(self, other): 179 | """Implement comparison.""" 180 | return not self.__lt__(other) 181 | 182 | def __str__(self): 183 | """Return semantic version string.""" 184 | vstr = '{0}.{1}.{2}'.format(self.major, self.minor, self.patch) 185 | if self.suffix: 186 | vstr = '{0}-{1}'.format(vstr, self.suffix) 187 | if self.build: 188 | vstr = '{0}+{1}'.format(vstr, self.build) 189 | return vstr 190 | 191 | def __repr__(self): 192 | """Return 'code' representation of `Version`.""" 193 | return "Version('{0}')".format(str(self)) 194 | 195 | 196 | def download_workflow(url): 197 | """Download workflow at ``url`` to a local temporary file. 198 | 199 | :param url: URL to .alfredworkflow file in GitHub repo 200 | :returns: path to downloaded file 201 | 202 | """ 203 | filename = url.split('/')[-1] 204 | 205 | if (not filename.endswith('.alfredworkflow') and 206 | not filename.endswith('.alfred3workflow')): 207 | raise ValueError('attachment not a workflow: {0}'.format(filename)) 208 | 209 | local_path = os.path.join(tempfile.gettempdir(), filename) 210 | 211 | wf().logger.debug( 212 | 'downloading updated workflow from `%s` to `%s` ...', url, local_path) 213 | 214 | response = web.get(url) 215 | 216 | with open(local_path, 'wb') as output: 217 | output.write(response.content) 218 | 219 | return local_path 220 | 221 | 222 | def build_api_url(slug): 223 | """Generate releases URL from GitHub slug. 224 | 225 | :param slug: Repo name in form ``username/repo`` 226 | :returns: URL to the API endpoint for the repo's releases 227 | 228 | """ 229 | if len(slug.split('/')) != 2: 230 | raise ValueError('invalid GitHub slug: {0}'.format(slug)) 231 | 232 | return RELEASES_BASE.format(slug) 233 | 234 | 235 | def _validate_release(release): 236 | """Return release for running version of Alfred.""" 237 | alf3 = wf().alfred_version.major == 3 238 | 239 | downloads = {'.alfredworkflow': [], '.alfred3workflow': []} 240 | dl_count = 0 241 | version = release['tag_name'] 242 | 243 | for asset in release.get('assets', []): 244 | url = asset.get('browser_download_url') 245 | if not url: # pragma: nocover 246 | continue 247 | 248 | ext = os.path.splitext(url)[1].lower() 249 | if ext not in downloads: 250 | continue 251 | 252 | # Ignore Alfred 3-only files if Alfred 2 is running 253 | if ext == '.alfred3workflow' and not alf3: 254 | continue 255 | 256 | downloads[ext].append(url) 257 | dl_count += 1 258 | 259 | # download_urls.append(url) 260 | 261 | if dl_count == 0: 262 | wf().logger.warning( 263 | 'invalid release (no workflow file): %s', version) 264 | return None 265 | 266 | for k in downloads: 267 | if len(downloads[k]) > 1: 268 | wf().logger.warning( 269 | 'invalid release (multiple %s files): %s', k, version) 270 | return None 271 | 272 | # Prefer .alfred3workflow file if there is one and Alfred 3 is 273 | # running. 274 | if alf3 and len(downloads['.alfred3workflow']): 275 | download_url = downloads['.alfred3workflow'][0] 276 | 277 | else: 278 | download_url = downloads['.alfredworkflow'][0] 279 | 280 | wf().logger.debug('release %s: %s', version, download_url) 281 | 282 | return { 283 | 'version': version, 284 | 'download_url': download_url, 285 | 'prerelease': release['prerelease'] 286 | } 287 | 288 | 289 | def get_valid_releases(github_slug, prereleases=False): 290 | """Return list of all valid releases. 291 | 292 | :param github_slug: ``username/repo`` for workflow's GitHub repo 293 | :param prereleases: Whether to include pre-releases. 294 | :returns: list of dicts. Each :class:`dict` has the form 295 | ``{'version': '1.1', 'download_url': 'http://github.com/...', 296 | 'prerelease': False }`` 297 | 298 | 299 | A valid release is one that contains one ``.alfredworkflow`` file. 300 | 301 | If the GitHub version (i.e. tag) is of the form ``v1.1``, the leading 302 | ``v`` will be stripped. 303 | 304 | """ 305 | api_url = build_api_url(github_slug) 306 | releases = [] 307 | 308 | wf().logger.debug('retrieving releases list: %s', api_url) 309 | 310 | def retrieve_releases(): 311 | wf().logger.info( 312 | 'retrieving releases: %s', github_slug) 313 | return web.get(api_url).json() 314 | 315 | slug = github_slug.replace('/', '-') 316 | for release in wf().cached_data('gh-releases-' + slug, retrieve_releases): 317 | 318 | release = _validate_release(release) 319 | if release is None: 320 | wf().logger.debug('invalid release: %r', release) 321 | continue 322 | 323 | elif release['prerelease'] and not prereleases: 324 | wf().logger.debug('ignoring prerelease: %s', release['version']) 325 | continue 326 | 327 | wf().logger.debug('release: %r', release) 328 | 329 | releases.append(release) 330 | 331 | return releases 332 | 333 | 334 | def check_update(github_slug, current_version, prereleases=False): 335 | """Check whether a newer release is available on GitHub. 336 | 337 | :param github_slug: ``username/repo`` for workflow's GitHub repo 338 | :param current_version: the currently installed version of the 339 | workflow. :ref:`Semantic versioning ` is required. 340 | :param prereleases: Whether to include pre-releases. 341 | :type current_version: ``unicode`` 342 | :returns: ``True`` if an update is available, else ``False`` 343 | 344 | If an update is available, its version number and download URL will 345 | be cached. 346 | 347 | """ 348 | releases = get_valid_releases(github_slug, prereleases) 349 | 350 | if not len(releases): 351 | raise ValueError('no valid releases for %s', github_slug) 352 | 353 | wf().logger.info('%d releases for %s', len(releases), github_slug) 354 | 355 | # GitHub returns releases newest-first 356 | latest_release = releases[0] 357 | 358 | # (latest_version, download_url) = get_latest_release(releases) 359 | vr = Version(latest_release['version']) 360 | vl = Version(current_version) 361 | wf().logger.debug('latest=%r, installed=%r', vr, vl) 362 | if vr > vl: 363 | 364 | wf().cache_data('__workflow_update_status', { 365 | 'version': latest_release['version'], 366 | 'download_url': latest_release['download_url'], 367 | 'available': True 368 | }) 369 | 370 | return True 371 | 372 | wf().cache_data('__workflow_update_status', {'available': False}) 373 | return False 374 | 375 | 376 | def install_update(): 377 | """If a newer release is available, download and install it. 378 | 379 | :returns: ``True`` if an update is installed, else ``False`` 380 | 381 | """ 382 | update_data = wf().cached_data('__workflow_update_status', max_age=0) 383 | 384 | if not update_data or not update_data.get('available'): 385 | wf().logger.info('no update available') 386 | return False 387 | 388 | local_file = download_workflow(update_data['download_url']) 389 | 390 | wf().logger.info('installing updated workflow ...') 391 | subprocess.call(['open', local_file]) 392 | 393 | update_data['available'] = False 394 | wf().cache_data('__workflow_update_status', update_data) 395 | return True 396 | 397 | 398 | if __name__ == '__main__': # pragma: nocover 399 | import sys 400 | 401 | def show_help(status=0): 402 | """Print help message.""" 403 | print('Usage : update.py (check|install) ' 404 | '[--prereleases] ') 405 | sys.exit(status) 406 | 407 | argv = sys.argv[:] 408 | if '-h' in argv or '--help' in argv: 409 | show_help() 410 | 411 | prereleases = '--prereleases' in argv 412 | 413 | if prereleases: 414 | argv.remove('--prereleases') 415 | 416 | if len(argv) != 4: 417 | show_help(1) 418 | 419 | action, github_slug, version = argv[1:] 420 | 421 | if action == 'check': 422 | check_update(github_slug, version, prereleases) 423 | elif action == 'install': 424 | install_update() 425 | else: 426 | show_help(1) 427 | -------------------------------------------------------------------------------- /src/workflow/update.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/update.pyc -------------------------------------------------------------------------------- /src/workflow/version: -------------------------------------------------------------------------------- 1 | 1.29 -------------------------------------------------------------------------------- /src/workflow/web.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright (c) 2014 Dean Jackson 4 | # 5 | # MIT Licence. See http://opensource.org/licenses/MIT 6 | # 7 | # Created on 2014-02-15 8 | # 9 | 10 | """Lightweight HTTP library with a requests-like interface.""" 11 | 12 | import codecs 13 | import json 14 | import mimetypes 15 | import os 16 | import random 17 | import re 18 | import socket 19 | import string 20 | import unicodedata 21 | import urllib 22 | import urllib2 23 | import urlparse 24 | import zlib 25 | 26 | 27 | USER_AGENT = u'Alfred-Workflow/1.19 (+http://www.deanishe.net/alfred-workflow)' 28 | 29 | # Valid characters for multipart form data boundaries 30 | BOUNDARY_CHARS = string.digits + string.ascii_letters 31 | 32 | # HTTP response codes 33 | RESPONSES = { 34 | 100: 'Continue', 35 | 101: 'Switching Protocols', 36 | 200: 'OK', 37 | 201: 'Created', 38 | 202: 'Accepted', 39 | 203: 'Non-Authoritative Information', 40 | 204: 'No Content', 41 | 205: 'Reset Content', 42 | 206: 'Partial Content', 43 | 300: 'Multiple Choices', 44 | 301: 'Moved Permanently', 45 | 302: 'Found', 46 | 303: 'See Other', 47 | 304: 'Not Modified', 48 | 305: 'Use Proxy', 49 | 307: 'Temporary Redirect', 50 | 400: 'Bad Request', 51 | 401: 'Unauthorized', 52 | 402: 'Payment Required', 53 | 403: 'Forbidden', 54 | 404: 'Not Found', 55 | 405: 'Method Not Allowed', 56 | 406: 'Not Acceptable', 57 | 407: 'Proxy Authentication Required', 58 | 408: 'Request Timeout', 59 | 409: 'Conflict', 60 | 410: 'Gone', 61 | 411: 'Length Required', 62 | 412: 'Precondition Failed', 63 | 413: 'Request Entity Too Large', 64 | 414: 'Request-URI Too Long', 65 | 415: 'Unsupported Media Type', 66 | 416: 'Requested Range Not Satisfiable', 67 | 417: 'Expectation Failed', 68 | 500: 'Internal Server Error', 69 | 501: 'Not Implemented', 70 | 502: 'Bad Gateway', 71 | 503: 'Service Unavailable', 72 | 504: 'Gateway Timeout', 73 | 505: 'HTTP Version Not Supported' 74 | } 75 | 76 | 77 | def str_dict(dic): 78 | """Convert keys and values in ``dic`` into UTF-8-encoded :class:`str`. 79 | 80 | :param dic: Mapping of Unicode strings 81 | :type dic: dict 82 | :returns: Dictionary containing only UTF-8 strings 83 | :rtype: dict 84 | 85 | """ 86 | if isinstance(dic, CaseInsensitiveDictionary): 87 | dic2 = CaseInsensitiveDictionary() 88 | else: 89 | dic2 = {} 90 | for k, v in dic.items(): 91 | if isinstance(k, unicode): 92 | k = k.encode('utf-8') 93 | if isinstance(v, unicode): 94 | v = v.encode('utf-8') 95 | dic2[k] = v 96 | return dic2 97 | 98 | 99 | class NoRedirectHandler(urllib2.HTTPRedirectHandler): 100 | """Prevent redirections.""" 101 | 102 | def redirect_request(self, *args): 103 | return None 104 | 105 | 106 | # Adapted from https://gist.github.com/babakness/3901174 107 | class CaseInsensitiveDictionary(dict): 108 | """Dictionary with caseless key search. 109 | 110 | Enables case insensitive searching while preserving case sensitivity 111 | when keys are listed, ie, via keys() or items() methods. 112 | 113 | Works by storing a lowercase version of the key as the new key and 114 | stores the original key-value pair as the key's value 115 | (values become dictionaries). 116 | 117 | """ 118 | 119 | def __init__(self, initval=None): 120 | """Create new case-insensitive dictionary.""" 121 | if isinstance(initval, dict): 122 | for key, value in initval.iteritems(): 123 | self.__setitem__(key, value) 124 | 125 | elif isinstance(initval, list): 126 | for (key, value) in initval: 127 | self.__setitem__(key, value) 128 | 129 | def __contains__(self, key): 130 | return dict.__contains__(self, key.lower()) 131 | 132 | def __getitem__(self, key): 133 | return dict.__getitem__(self, key.lower())['val'] 134 | 135 | def __setitem__(self, key, value): 136 | return dict.__setitem__(self, key.lower(), {'key': key, 'val': value}) 137 | 138 | def get(self, key, default=None): 139 | try: 140 | v = dict.__getitem__(self, key.lower()) 141 | except KeyError: 142 | return default 143 | else: 144 | return v['val'] 145 | 146 | def update(self, other): 147 | for k, v in other.items(): 148 | self[k] = v 149 | 150 | def items(self): 151 | return [(v['key'], v['val']) for v in dict.itervalues(self)] 152 | 153 | def keys(self): 154 | return [v['key'] for v in dict.itervalues(self)] 155 | 156 | def values(self): 157 | return [v['val'] for v in dict.itervalues(self)] 158 | 159 | def iteritems(self): 160 | for v in dict.itervalues(self): 161 | yield v['key'], v['val'] 162 | 163 | def iterkeys(self): 164 | for v in dict.itervalues(self): 165 | yield v['key'] 166 | 167 | def itervalues(self): 168 | for v in dict.itervalues(self): 169 | yield v['val'] 170 | 171 | 172 | class Response(object): 173 | """ 174 | Returned by :func:`request` / :func:`get` / :func:`post` functions. 175 | 176 | Simplified version of the ``Response`` object in the ``requests`` library. 177 | 178 | >>> r = request('http://www.google.com') 179 | >>> r.status_code 180 | 200 181 | >>> r.encoding 182 | ISO-8859-1 183 | >>> r.content # bytes 184 | ... 185 | >>> r.text # unicode, decoded according to charset in HTTP header/meta tag 186 | u' ...' 187 | >>> r.json() # content parsed as JSON 188 | 189 | """ 190 | 191 | def __init__(self, request, stream=False): 192 | """Call `request` with :mod:`urllib2` and process results. 193 | 194 | :param request: :class:`urllib2.Request` instance 195 | :param stream: Whether to stream response or retrieve it all at once 196 | :type stream: bool 197 | 198 | """ 199 | self.request = request 200 | self._stream = stream 201 | self.url = None 202 | self.raw = None 203 | self._encoding = None 204 | self.error = None 205 | self.status_code = None 206 | self.reason = None 207 | self.headers = CaseInsensitiveDictionary() 208 | self._content = None 209 | self._content_loaded = False 210 | self._gzipped = False 211 | 212 | # Execute query 213 | try: 214 | self.raw = urllib2.urlopen(request) 215 | except urllib2.HTTPError as err: 216 | self.error = err 217 | try: 218 | self.url = err.geturl() 219 | # sometimes (e.g. when authentication fails) 220 | # urllib can't get a URL from an HTTPError 221 | # This behaviour changes across Python versions, 222 | # so no test cover (it isn't important). 223 | except AttributeError: # pragma: no cover 224 | pass 225 | self.status_code = err.code 226 | else: 227 | self.status_code = self.raw.getcode() 228 | self.url = self.raw.geturl() 229 | self.reason = RESPONSES.get(self.status_code) 230 | 231 | # Parse additional info if request succeeded 232 | if not self.error: 233 | headers = self.raw.info() 234 | self.transfer_encoding = headers.getencoding() 235 | self.mimetype = headers.gettype() 236 | for key in headers.keys(): 237 | self.headers[key.lower()] = headers.get(key) 238 | 239 | # Is content gzipped? 240 | # Transfer-Encoding appears to not be used in the wild 241 | # (contrary to the HTTP standard), but no harm in testing 242 | # for it 243 | if ('gzip' in headers.get('content-encoding', '') or 244 | 'gzip' in headers.get('transfer-encoding', '')): 245 | self._gzipped = True 246 | 247 | @property 248 | def stream(self): 249 | """Whether response is streamed. 250 | 251 | Returns: 252 | bool: `True` if response is streamed. 253 | """ 254 | return self._stream 255 | 256 | @stream.setter 257 | def stream(self, value): 258 | if self._content_loaded: 259 | raise RuntimeError("`content` has already been read from " 260 | "this Response.") 261 | 262 | self._stream = value 263 | 264 | def json(self): 265 | """Decode response contents as JSON. 266 | 267 | :returns: object decoded from JSON 268 | :rtype: list, dict or unicode 269 | 270 | """ 271 | return json.loads(self.content, self.encoding or 'utf-8') 272 | 273 | @property 274 | def encoding(self): 275 | """Text encoding of document or ``None``. 276 | 277 | :returns: Text encoding if found. 278 | :rtype: str or ``None`` 279 | 280 | """ 281 | if not self._encoding: 282 | self._encoding = self._get_encoding() 283 | 284 | return self._encoding 285 | 286 | @property 287 | def content(self): 288 | """Raw content of response (i.e. bytes). 289 | 290 | :returns: Body of HTTP response 291 | :rtype: str 292 | 293 | """ 294 | if not self._content: 295 | 296 | # Decompress gzipped content 297 | if self._gzipped: 298 | decoder = zlib.decompressobj(16 + zlib.MAX_WBITS) 299 | self._content = decoder.decompress(self.raw.read()) 300 | 301 | else: 302 | self._content = self.raw.read() 303 | 304 | self._content_loaded = True 305 | 306 | return self._content 307 | 308 | @property 309 | def text(self): 310 | """Unicode-decoded content of response body. 311 | 312 | If no encoding can be determined from HTTP headers or the content 313 | itself, the encoded response body will be returned instead. 314 | 315 | :returns: Body of HTTP response 316 | :rtype: unicode or str 317 | 318 | """ 319 | if self.encoding: 320 | return unicodedata.normalize('NFC', unicode(self.content, 321 | self.encoding)) 322 | return self.content 323 | 324 | def iter_content(self, chunk_size=4096, decode_unicode=False): 325 | """Iterate over response data. 326 | 327 | .. versionadded:: 1.6 328 | 329 | :param chunk_size: Number of bytes to read into memory 330 | :type chunk_size: int 331 | :param decode_unicode: Decode to Unicode using detected encoding 332 | :type decode_unicode: bool 333 | :returns: iterator 334 | 335 | """ 336 | if not self.stream: 337 | raise RuntimeError("You cannot call `iter_content` on a " 338 | "Response unless you passed `stream=True`" 339 | " to `get()`/`post()`/`request()`.") 340 | 341 | if self._content_loaded: 342 | raise RuntimeError( 343 | "`content` has already been read from this Response.") 344 | 345 | def decode_stream(iterator, r): 346 | 347 | decoder = codecs.getincrementaldecoder(r.encoding)(errors='replace') 348 | 349 | for chunk in iterator: 350 | data = decoder.decode(chunk) 351 | if data: 352 | yield data 353 | 354 | data = decoder.decode(b'', final=True) 355 | if data: # pragma: no cover 356 | yield data 357 | 358 | def generate(): 359 | 360 | if self._gzipped: 361 | decoder = zlib.decompressobj(16 + zlib.MAX_WBITS) 362 | 363 | while True: 364 | chunk = self.raw.read(chunk_size) 365 | if not chunk: 366 | break 367 | 368 | if self._gzipped: 369 | chunk = decoder.decompress(chunk) 370 | 371 | yield chunk 372 | 373 | chunks = generate() 374 | 375 | if decode_unicode and self.encoding: 376 | chunks = decode_stream(chunks, self) 377 | 378 | return chunks 379 | 380 | def save_to_path(self, filepath): 381 | """Save retrieved data to file at ``filepath``. 382 | 383 | .. versionadded: 1.9.6 384 | 385 | :param filepath: Path to save retrieved data. 386 | 387 | """ 388 | filepath = os.path.abspath(filepath) 389 | dirname = os.path.dirname(filepath) 390 | if not os.path.exists(dirname): 391 | os.makedirs(dirname) 392 | 393 | self.stream = True 394 | 395 | with open(filepath, 'wb') as fileobj: 396 | for data in self.iter_content(): 397 | fileobj.write(data) 398 | 399 | def raise_for_status(self): 400 | """Raise stored error if one occurred. 401 | 402 | error will be instance of :class:`urllib2.HTTPError` 403 | """ 404 | if self.error is not None: 405 | raise self.error 406 | return 407 | 408 | def _get_encoding(self): 409 | """Get encoding from HTTP headers or content. 410 | 411 | :returns: encoding or `None` 412 | :rtype: unicode or ``None`` 413 | 414 | """ 415 | headers = self.raw.info() 416 | encoding = None 417 | 418 | if headers.getparam('charset'): 419 | encoding = headers.getparam('charset') 420 | 421 | # HTTP Content-Type header 422 | for param in headers.getplist(): 423 | if param.startswith('charset='): 424 | encoding = param[8:] 425 | break 426 | 427 | if not self.stream: # Try sniffing response content 428 | # Encoding declared in document should override HTTP headers 429 | if self.mimetype == 'text/html': # sniff HTML headers 430 | m = re.search("""""", 431 | self.content) 432 | if m: 433 | encoding = m.group(1) 434 | 435 | elif ((self.mimetype.startswith('application/') or 436 | self.mimetype.startswith('text/')) and 437 | 'xml' in self.mimetype): 438 | m = re.search("""]*\?>""", 439 | self.content) 440 | if m: 441 | encoding = m.group(1) 442 | 443 | # Format defaults 444 | if self.mimetype == 'application/json' and not encoding: 445 | # The default encoding for JSON 446 | encoding = 'utf-8' 447 | 448 | elif self.mimetype == 'application/xml' and not encoding: 449 | # The default for 'application/xml' 450 | encoding = 'utf-8' 451 | 452 | if encoding: 453 | encoding = encoding.lower() 454 | 455 | return encoding 456 | 457 | 458 | def request(method, url, params=None, data=None, headers=None, cookies=None, 459 | files=None, auth=None, timeout=60, allow_redirects=False, 460 | stream=False): 461 | """Initiate an HTTP(S) request. Returns :class:`Response` object. 462 | 463 | :param method: 'GET' or 'POST' 464 | :type method: unicode 465 | :param url: URL to open 466 | :type url: unicode 467 | :param params: mapping of URL parameters 468 | :type params: dict 469 | :param data: mapping of form data ``{'field_name': 'value'}`` or 470 | :class:`str` 471 | :type data: dict or str 472 | :param headers: HTTP headers 473 | :type headers: dict 474 | :param cookies: cookies to send to server 475 | :type cookies: dict 476 | :param files: files to upload (see below). 477 | :type files: dict 478 | :param auth: username, password 479 | :type auth: tuple 480 | :param timeout: connection timeout limit in seconds 481 | :type timeout: int 482 | :param allow_redirects: follow redirections 483 | :type allow_redirects: bool 484 | :param stream: Stream content instead of fetching it all at once. 485 | :type stream: bool 486 | :returns: Response object 487 | :rtype: :class:`Response` 488 | 489 | 490 | The ``files`` argument is a dictionary:: 491 | 492 | {'fieldname' : { 'filename': 'blah.txt', 493 | 'content': '', 494 | 'mimetype': 'text/plain'} 495 | } 496 | 497 | * ``fieldname`` is the name of the field in the HTML form. 498 | * ``mimetype`` is optional. If not provided, :mod:`mimetypes` will 499 | be used to guess the mimetype, or ``application/octet-stream`` 500 | will be used. 501 | 502 | """ 503 | # TODO: cookies 504 | socket.setdefaulttimeout(timeout) 505 | 506 | # Default handlers 507 | openers = [] 508 | 509 | if not allow_redirects: 510 | openers.append(NoRedirectHandler()) 511 | 512 | if auth is not None: # Add authorisation handler 513 | username, password = auth 514 | password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() 515 | password_manager.add_password(None, url, username, password) 516 | auth_manager = urllib2.HTTPBasicAuthHandler(password_manager) 517 | openers.append(auth_manager) 518 | 519 | # Install our custom chain of openers 520 | opener = urllib2.build_opener(*openers) 521 | urllib2.install_opener(opener) 522 | 523 | if not headers: 524 | headers = CaseInsensitiveDictionary() 525 | else: 526 | headers = CaseInsensitiveDictionary(headers) 527 | 528 | if 'user-agent' not in headers: 529 | headers['user-agent'] = USER_AGENT 530 | 531 | # Accept gzip-encoded content 532 | encodings = [s.strip() for s in 533 | headers.get('accept-encoding', '').split(',')] 534 | if 'gzip' not in encodings: 535 | encodings.append('gzip') 536 | 537 | headers['accept-encoding'] = ', '.join(encodings) 538 | 539 | # Force POST by providing an empty data string 540 | if method == 'POST' and not data: 541 | data = '' 542 | 543 | if files: 544 | if not data: 545 | data = {} 546 | new_headers, data = encode_multipart_formdata(data, files) 547 | headers.update(new_headers) 548 | elif data and isinstance(data, dict): 549 | data = urllib.urlencode(str_dict(data)) 550 | 551 | # Make sure everything is encoded text 552 | headers = str_dict(headers) 553 | 554 | if isinstance(url, unicode): 555 | url = url.encode('utf-8') 556 | 557 | if params: # GET args (POST args are handled in encode_multipart_formdata) 558 | 559 | scheme, netloc, path, query, fragment = urlparse.urlsplit(url) 560 | 561 | if query: # Combine query string and `params` 562 | url_params = urlparse.parse_qs(query) 563 | # `params` take precedence over URL query string 564 | url_params.update(params) 565 | params = url_params 566 | 567 | query = urllib.urlencode(str_dict(params), doseq=True) 568 | url = urlparse.urlunsplit((scheme, netloc, path, query, fragment)) 569 | 570 | req = urllib2.Request(url, data, headers) 571 | return Response(req, stream) 572 | 573 | 574 | def get(url, params=None, headers=None, cookies=None, auth=None, 575 | timeout=60, allow_redirects=True, stream=False): 576 | """Initiate a GET request. Arguments as for :func:`request`. 577 | 578 | :returns: :class:`Response` instance 579 | 580 | """ 581 | return request('GET', url, params, headers=headers, cookies=cookies, 582 | auth=auth, timeout=timeout, allow_redirects=allow_redirects, 583 | stream=stream) 584 | 585 | 586 | def post(url, params=None, data=None, headers=None, cookies=None, files=None, 587 | auth=None, timeout=60, allow_redirects=False, stream=False): 588 | """Initiate a POST request. Arguments as for :func:`request`. 589 | 590 | :returns: :class:`Response` instance 591 | 592 | """ 593 | return request('POST', url, params, data, headers, cookies, files, auth, 594 | timeout, allow_redirects, stream) 595 | 596 | 597 | def encode_multipart_formdata(fields, files): 598 | """Encode form data (``fields``) and ``files`` for POST request. 599 | 600 | :param fields: mapping of ``{name : value}`` pairs for normal form fields. 601 | :type fields: dict 602 | :param files: dictionary of fieldnames/files elements for file data. 603 | See below for details. 604 | :type files: dict of :class:`dict` 605 | :returns: ``(headers, body)`` ``headers`` is a 606 | :class:`dict` of HTTP headers 607 | :rtype: 2-tuple ``(dict, str)`` 608 | 609 | The ``files`` argument is a dictionary:: 610 | 611 | {'fieldname' : { 'filename': 'blah.txt', 612 | 'content': '', 613 | 'mimetype': 'text/plain'} 614 | } 615 | 616 | - ``fieldname`` is the name of the field in the HTML form. 617 | - ``mimetype`` is optional. If not provided, :mod:`mimetypes` will 618 | be used to guess the mimetype, or ``application/octet-stream`` 619 | will be used. 620 | 621 | """ 622 | def get_content_type(filename): 623 | """Return or guess mimetype of ``filename``. 624 | 625 | :param filename: filename of file 626 | :type filename: unicode/str 627 | :returns: mime-type, e.g. ``text/html`` 628 | :rtype: str 629 | 630 | """ 631 | 632 | return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 633 | 634 | boundary = '-----' + ''.join(random.choice(BOUNDARY_CHARS) 635 | for i in range(30)) 636 | CRLF = '\r\n' 637 | output = [] 638 | 639 | # Normal form fields 640 | for (name, value) in fields.items(): 641 | if isinstance(name, unicode): 642 | name = name.encode('utf-8') 643 | if isinstance(value, unicode): 644 | value = value.encode('utf-8') 645 | output.append('--' + boundary) 646 | output.append('Content-Disposition: form-data; name="%s"' % name) 647 | output.append('') 648 | output.append(value) 649 | 650 | # Files to upload 651 | for name, d in files.items(): 652 | filename = d[u'filename'] 653 | content = d[u'content'] 654 | if u'mimetype' in d: 655 | mimetype = d[u'mimetype'] 656 | else: 657 | mimetype = get_content_type(filename) 658 | if isinstance(name, unicode): 659 | name = name.encode('utf-8') 660 | if isinstance(filename, unicode): 661 | filename = filename.encode('utf-8') 662 | if isinstance(mimetype, unicode): 663 | mimetype = mimetype.encode('utf-8') 664 | output.append('--' + boundary) 665 | output.append('Content-Disposition: form-data; ' 666 | 'name="%s"; filename="%s"' % (name, filename)) 667 | output.append('Content-Type: %s' % mimetype) 668 | output.append('') 669 | output.append(content) 670 | 671 | output.append('--' + boundary + '--') 672 | output.append('') 673 | body = CRLF.join(output) 674 | headers = { 675 | 'Content-Type': 'multipart/form-data; boundary=%s' % boundary, 676 | 'Content-Length': str(len(body)), 677 | } 678 | return (headers, body) 679 | -------------------------------------------------------------------------------- /src/workflow/web.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/web.pyc -------------------------------------------------------------------------------- /src/workflow/workflow.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/workflow.pyc -------------------------------------------------------------------------------- /src/workflow/workflow3.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright (c) 2016 Dean Jackson 4 | # 5 | # MIT Licence. See http://opensource.org/licenses/MIT 6 | # 7 | # Created on 2016-06-25 8 | # 9 | 10 | """An Alfred 3-only version of :class:`~workflow.Workflow`. 11 | 12 | :class:`~workflow.Workflow3` supports Alfred 3's new features, such as 13 | setting :ref:`workflow-variables` and 14 | :class:`the more advanced modifiers ` supported by Alfred 3. 15 | 16 | In order for the feedback mechanism to work correctly, it's important 17 | to create :class:`Item3` and :class:`Modifier` objects via the 18 | :meth:`Workflow3.add_item()` and :meth:`Item3.add_modifier()` methods 19 | respectively. If you instantiate :class:`Item3` or :class:`Modifier` 20 | objects directly, the current :class:`Workflow3` object won't be aware 21 | of them, and they won't be sent to Alfred when you call 22 | :meth:`Workflow3.send_feedback()`. 23 | 24 | """ 25 | 26 | from __future__ import print_function, unicode_literals, absolute_import 27 | 28 | import json 29 | import os 30 | import sys 31 | 32 | from .workflow import Workflow 33 | 34 | 35 | class Variables(dict): 36 | """Workflow variables for Run Script actions. 37 | 38 | .. versionadded: 1.26 39 | 40 | This class allows you to set workflow variables from 41 | Run Script actions. 42 | 43 | It is a subclass of :class:`dict`. 44 | 45 | >>> v = Variables(username='deanishe', password='hunter2') 46 | >>> v.arg = u'output value' 47 | >>> print(v) 48 | 49 | See :ref:`variables-run-script` in the User Guide for more 50 | information. 51 | 52 | Args: 53 | arg (unicode, optional): Main output/``{query}``. 54 | **variables: Workflow variables to set. 55 | 56 | 57 | Attributes: 58 | arg (unicode): Output value (``{query}``). 59 | config (dict): Configuration for downstream workflow element. 60 | 61 | """ 62 | 63 | def __init__(self, arg=None, **variables): 64 | """Create a new `Variables` object.""" 65 | self.arg = arg 66 | self.config = {} 67 | super(Variables, self).__init__(**variables) 68 | 69 | @property 70 | def obj(self): 71 | """Return ``alfredworkflow`` `dict`.""" 72 | o = {} 73 | if self: 74 | d2 = {} 75 | for k, v in self.items(): 76 | d2[k] = v 77 | o['variables'] = d2 78 | 79 | if self.config: 80 | o['config'] = self.config 81 | 82 | if self.arg is not None: 83 | o['arg'] = self.arg 84 | 85 | return {'alfredworkflow': o} 86 | 87 | def __unicode__(self): 88 | """Convert to ``alfredworkflow`` JSON object. 89 | 90 | Returns: 91 | unicode: ``alfredworkflow`` JSON object 92 | 93 | """ 94 | if not self and not self.config: 95 | if self.arg: 96 | return self.arg 97 | else: 98 | return u'' 99 | 100 | return json.dumps(self.obj) 101 | 102 | def __str__(self): 103 | """Convert to ``alfredworkflow`` JSON object. 104 | 105 | Returns: 106 | str: UTF-8 encoded ``alfredworkflow`` JSON object 107 | 108 | """ 109 | return unicode(self).encode('utf-8') 110 | 111 | 112 | class Modifier(object): 113 | """Modify :class:`Item3` arg/icon/variables when modifier key is pressed. 114 | 115 | Don't use this class directly (as it won't be associated with any 116 | :class:`Item3`), but rather use :meth:`Item3.add_modifier()` 117 | to add modifiers to results. 118 | 119 | >>> it = wf.add_item('Title', 'Subtitle', valid=True) 120 | >>> it.setvar('name', 'default') 121 | >>> m = it.add_modifier('cmd') 122 | >>> m.setvar('name', 'alternate') 123 | 124 | See :ref:`workflow-variables` in the User Guide for more information 125 | and :ref:`example usage `. 126 | 127 | Args: 128 | key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc. 129 | subtitle (unicode, optional): Override default subtitle. 130 | arg (unicode, optional): Argument to pass for this modifier. 131 | valid (bool, optional): Override item's validity. 132 | icon (unicode, optional): Filepath/UTI of icon to use 133 | icontype (unicode, optional): Type of icon. See 134 | :meth:`Workflow.add_item() ` 135 | for valid values. 136 | 137 | Attributes: 138 | arg (unicode): Arg to pass to following action. 139 | config (dict): Configuration for a downstream element, such as 140 | a File Filter. 141 | icon (unicode): Filepath/UTI of icon. 142 | icontype (unicode): Type of icon. See 143 | :meth:`Workflow.add_item() ` 144 | for valid values. 145 | key (unicode): Modifier key (see above). 146 | subtitle (unicode): Override item subtitle. 147 | valid (bool): Override item validity. 148 | variables (dict): Workflow variables set by this modifier. 149 | 150 | """ 151 | 152 | def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None, 153 | icontype=None): 154 | """Create a new :class:`Modifier`. 155 | 156 | Don't use this class directly (as it won't be associated with any 157 | :class:`Item3`), but rather use :meth:`Item3.add_modifier()` 158 | to add modifiers to results. 159 | 160 | Args: 161 | key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc. 162 | subtitle (unicode, optional): Override default subtitle. 163 | arg (unicode, optional): Argument to pass for this modifier. 164 | valid (bool, optional): Override item's validity. 165 | icon (unicode, optional): Filepath/UTI of icon to use 166 | icontype (unicode, optional): Type of icon. See 167 | :meth:`Workflow.add_item() ` 168 | for valid values. 169 | 170 | """ 171 | self.key = key 172 | self.subtitle = subtitle 173 | self.arg = arg 174 | self.valid = valid 175 | self.icon = icon 176 | self.icontype = icontype 177 | 178 | self.config = {} 179 | self.variables = {} 180 | 181 | def setvar(self, name, value): 182 | """Set a workflow variable for this Item. 183 | 184 | Args: 185 | name (unicode): Name of variable. 186 | value (unicode): Value of variable. 187 | 188 | """ 189 | self.variables[name] = value 190 | 191 | def getvar(self, name, default=None): 192 | """Return value of workflow variable for ``name`` or ``default``. 193 | 194 | Args: 195 | name (unicode): Variable name. 196 | default (None, optional): Value to return if variable is unset. 197 | 198 | Returns: 199 | unicode or ``default``: Value of variable if set or ``default``. 200 | 201 | """ 202 | return self.variables.get(name, default) 203 | 204 | @property 205 | def obj(self): 206 | """Modifier formatted for JSON serialization for Alfred 3. 207 | 208 | Returns: 209 | dict: Modifier for serializing to JSON. 210 | 211 | """ 212 | o = {} 213 | 214 | if self.subtitle is not None: 215 | o['subtitle'] = self.subtitle 216 | 217 | if self.arg is not None: 218 | o['arg'] = self.arg 219 | 220 | if self.valid is not None: 221 | o['valid'] = self.valid 222 | 223 | if self.variables: 224 | o['variables'] = self.variables 225 | 226 | if self.config: 227 | o['config'] = self.config 228 | 229 | icon = self._icon() 230 | if icon: 231 | o['icon'] = icon 232 | 233 | return o 234 | 235 | def _icon(self): 236 | """Return `icon` object for item. 237 | 238 | Returns: 239 | dict: Mapping for item `icon` (may be empty). 240 | 241 | """ 242 | icon = {} 243 | if self.icon is not None: 244 | icon['path'] = self.icon 245 | 246 | if self.icontype is not None: 247 | icon['type'] = self.icontype 248 | 249 | return icon 250 | 251 | 252 | class Item3(object): 253 | """Represents a feedback item for Alfred 3. 254 | 255 | Generates Alfred-compliant JSON for a single item. 256 | 257 | Don't use this class directly (as it then won't be associated with 258 | any :class:`Workflow3 ` object), but rather use 259 | :meth:`Workflow3.add_item() `. 260 | See :meth:`~workflow.Workflow3.add_item` for details of arguments. 261 | 262 | """ 263 | 264 | def __init__(self, title, subtitle='', arg=None, autocomplete=None, 265 | match=None, valid=False, uid=None, icon=None, icontype=None, 266 | type=None, largetext=None, copytext=None, quicklookurl=None): 267 | """Create a new :class:`Item3` object. 268 | 269 | Use same arguments as for 270 | :class:`Workflow.Item `. 271 | 272 | Argument ``subtitle_modifiers`` is not supported. 273 | 274 | """ 275 | self.title = title 276 | self.subtitle = subtitle 277 | self.arg = arg 278 | self.autocomplete = autocomplete 279 | self.match = match 280 | self.valid = valid 281 | self.uid = uid 282 | self.icon = icon 283 | self.icontype = icontype 284 | self.type = type 285 | self.quicklookurl = quicklookurl 286 | self.largetext = largetext 287 | self.copytext = copytext 288 | 289 | self.modifiers = {} 290 | 291 | self.config = {} 292 | self.variables = {} 293 | 294 | def setvar(self, name, value): 295 | """Set a workflow variable for this Item. 296 | 297 | Args: 298 | name (unicode): Name of variable. 299 | value (unicode): Value of variable. 300 | 301 | """ 302 | self.variables[name] = value 303 | 304 | def getvar(self, name, default=None): 305 | """Return value of workflow variable for ``name`` or ``default``. 306 | 307 | Args: 308 | name (unicode): Variable name. 309 | default (None, optional): Value to return if variable is unset. 310 | 311 | Returns: 312 | unicode or ``default``: Value of variable if set or ``default``. 313 | 314 | """ 315 | return self.variables.get(name, default) 316 | 317 | def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None, 318 | icontype=None): 319 | """Add alternative values for a modifier key. 320 | 321 | Args: 322 | key (unicode): Modifier key, e.g. ``"cmd"`` or ``"alt"`` 323 | subtitle (unicode, optional): Override item subtitle. 324 | arg (unicode, optional): Input for following action. 325 | valid (bool, optional): Override item validity. 326 | icon (unicode, optional): Filepath/UTI of icon. 327 | icontype (unicode, optional): Type of icon. See 328 | :meth:`Workflow.add_item() ` 329 | for valid values. 330 | 331 | Returns: 332 | Modifier: Configured :class:`Modifier`. 333 | 334 | """ 335 | mod = Modifier(key, subtitle, arg, valid, icon, icontype) 336 | 337 | for k in self.variables: 338 | mod.setvar(k, self.variables[k]) 339 | 340 | self.modifiers[key] = mod 341 | 342 | return mod 343 | 344 | @property 345 | def obj(self): 346 | """Item formatted for JSON serialization. 347 | 348 | Returns: 349 | dict: Data suitable for Alfred 3 feedback. 350 | 351 | """ 352 | # Required values 353 | o = { 354 | 'title': self.title, 355 | 'subtitle': self.subtitle, 356 | 'valid': self.valid, 357 | } 358 | 359 | # Optional values 360 | if self.arg is not None: 361 | o['arg'] = self.arg 362 | 363 | if self.autocomplete is not None: 364 | o['autocomplete'] = self.autocomplete 365 | 366 | if self.match is not None: 367 | o['match'] = self.match 368 | 369 | if self.uid is not None: 370 | o['uid'] = self.uid 371 | 372 | if self.type is not None: 373 | o['type'] = self.type 374 | 375 | if self.quicklookurl is not None: 376 | o['quicklookurl'] = self.quicklookurl 377 | 378 | if self.variables: 379 | o['variables'] = self.variables 380 | 381 | if self.config: 382 | o['config'] = self.config 383 | 384 | # Largetype and copytext 385 | text = self._text() 386 | if text: 387 | o['text'] = text 388 | 389 | icon = self._icon() 390 | if icon: 391 | o['icon'] = icon 392 | 393 | # Modifiers 394 | mods = self._modifiers() 395 | if mods: 396 | o['mods'] = mods 397 | 398 | return o 399 | 400 | def _icon(self): 401 | """Return `icon` object for item. 402 | 403 | Returns: 404 | dict: Mapping for item `icon` (may be empty). 405 | 406 | """ 407 | icon = {} 408 | if self.icon is not None: 409 | icon['path'] = self.icon 410 | 411 | if self.icontype is not None: 412 | icon['type'] = self.icontype 413 | 414 | return icon 415 | 416 | def _text(self): 417 | """Return `largetext` and `copytext` object for item. 418 | 419 | Returns: 420 | dict: `text` mapping (may be empty) 421 | 422 | """ 423 | text = {} 424 | if self.largetext is not None: 425 | text['largetype'] = self.largetext 426 | 427 | if self.copytext is not None: 428 | text['copy'] = self.copytext 429 | 430 | return text 431 | 432 | def _modifiers(self): 433 | """Build `mods` dictionary for JSON feedback. 434 | 435 | Returns: 436 | dict: Modifier mapping or `None`. 437 | 438 | """ 439 | if self.modifiers: 440 | mods = {} 441 | for k, mod in self.modifiers.items(): 442 | mods[k] = mod.obj 443 | 444 | return mods 445 | 446 | return None 447 | 448 | 449 | class Workflow3(Workflow): 450 | """Workflow class that generates Alfred 3 feedback. 451 | 452 | ``Workflow3`` is a subclass of :class:`~workflow.Workflow` and 453 | most of its methods are documented there. 454 | 455 | Attributes: 456 | item_class (class): Class used to generate feedback items. 457 | variables (dict): Top level workflow variables. 458 | 459 | """ 460 | 461 | item_class = Item3 462 | 463 | def __init__(self, **kwargs): 464 | """Create a new :class:`Workflow3` object. 465 | 466 | See :class:`~workflow.Workflow` for documentation. 467 | 468 | """ 469 | Workflow.__init__(self, **kwargs) 470 | self.variables = {} 471 | self._rerun = 0 472 | # Get session ID from environment if present 473 | self._session_id = os.getenv('_WF_SESSION_ID') or None 474 | if self._session_id: 475 | self.setvar('_WF_SESSION_ID', self._session_id) 476 | 477 | @property 478 | def _default_cachedir(self): 479 | """Alfred 3's default cache directory.""" 480 | return os.path.join( 481 | os.path.expanduser( 482 | '~/Library/Caches/com.runningwithcrayons.Alfred-3/' 483 | 'Workflow Data/'), 484 | self.bundleid) 485 | 486 | @property 487 | def _default_datadir(self): 488 | """Alfred 3's default data directory.""" 489 | return os.path.join(os.path.expanduser( 490 | '~/Library/Application Support/Alfred 3/Workflow Data/'), 491 | self.bundleid) 492 | 493 | @property 494 | def rerun(self): 495 | """How often (in seconds) Alfred should re-run the Script Filter.""" 496 | return self._rerun 497 | 498 | @rerun.setter 499 | def rerun(self, seconds): 500 | """Interval at which Alfred should re-run the Script Filter. 501 | 502 | Args: 503 | seconds (int): Interval between runs. 504 | """ 505 | self._rerun = seconds 506 | 507 | @property 508 | def session_id(self): 509 | """A unique session ID every time the user uses the workflow. 510 | 511 | .. versionadded:: 1.25 512 | 513 | The session ID persists while the user is using this workflow. 514 | It expires when the user runs a different workflow or closes 515 | Alfred. 516 | 517 | """ 518 | if not self._session_id: 519 | from uuid import uuid4 520 | self._session_id = uuid4().hex 521 | self.setvar('_WF_SESSION_ID', self._session_id) 522 | 523 | return self._session_id 524 | 525 | def setvar(self, name, value): 526 | """Set a "global" workflow variable. 527 | 528 | These variables are always passed to downstream workflow objects. 529 | 530 | If you have set :attr:`rerun`, these variables are also passed 531 | back to the script when Alfred runs it again. 532 | 533 | Args: 534 | name (unicode): Name of variable. 535 | value (unicode): Value of variable. 536 | 537 | """ 538 | self.variables[name] = value 539 | 540 | def getvar(self, name, default=None): 541 | """Return value of workflow variable for ``name`` or ``default``. 542 | 543 | Args: 544 | name (unicode): Variable name. 545 | default (None, optional): Value to return if variable is unset. 546 | 547 | Returns: 548 | unicode or ``default``: Value of variable if set or ``default``. 549 | 550 | """ 551 | return self.variables.get(name, default) 552 | 553 | def add_item(self, title, subtitle='', arg=None, autocomplete=None, 554 | valid=False, uid=None, icon=None, icontype=None, type=None, 555 | largetext=None, copytext=None, quicklookurl=None, match=None): 556 | """Add an item to be output to Alfred. 557 | 558 | Args: 559 | match (unicode, optional): If you have "Alfred filters results" 560 | turned on for your Script Filter, Alfred (version 3.5 and 561 | above) will filter against this field, not ``title``. 562 | 563 | See :meth:`Workflow.add_item() ` for 564 | the main documentation and other parameters. 565 | 566 | The key difference is that this method does not support the 567 | ``modifier_subtitles`` argument. Use the :meth:`~Item3.add_modifier()` 568 | method instead on the returned item instead. 569 | 570 | Returns: 571 | Item3: Alfred feedback item. 572 | 573 | """ 574 | item = self.item_class(title, subtitle, arg, autocomplete, 575 | match, valid, uid, icon, icontype, type, 576 | largetext, copytext, quicklookurl) 577 | 578 | self._items.append(item) 579 | return item 580 | 581 | @property 582 | def _session_prefix(self): 583 | """Filename prefix for current session.""" 584 | return '_wfsess-{0}-'.format(self.session_id) 585 | 586 | def _mk_session_name(self, name): 587 | """New cache name/key based on session ID.""" 588 | return self._session_prefix + name 589 | 590 | def cache_data(self, name, data, session=False): 591 | """Cache API with session-scoped expiry. 592 | 593 | .. versionadded:: 1.25 594 | 595 | Args: 596 | name (str): Cache key 597 | data (object): Data to cache 598 | session (bool, optional): Whether to scope the cache 599 | to the current session. 600 | 601 | ``name`` and ``data`` are the same as for the 602 | :meth:`~workflow.Workflow.cache_data` method on 603 | :class:`~workflow.Workflow`. 604 | 605 | If ``session`` is ``True``, then ``name`` is prefixed 606 | with :attr:`session_id`. 607 | 608 | """ 609 | if session: 610 | name = self._mk_session_name(name) 611 | 612 | return super(Workflow3, self).cache_data(name, data) 613 | 614 | def cached_data(self, name, data_func=None, max_age=60, session=False): 615 | """Cache API with session-scoped expiry. 616 | 617 | .. versionadded:: 1.25 618 | 619 | Args: 620 | name (str): Cache key 621 | data_func (callable): Callable that returns fresh data. It 622 | is called if the cache has expired or doesn't exist. 623 | max_age (int): Maximum allowable age of cache in seconds. 624 | session (bool, optional): Whether to scope the cache 625 | to the current session. 626 | 627 | ``name``, ``data_func`` and ``max_age`` are the same as for the 628 | :meth:`~workflow.Workflow.cached_data` method on 629 | :class:`~workflow.Workflow`. 630 | 631 | If ``session`` is ``True``, then ``name`` is prefixed 632 | with :attr:`session_id`. 633 | 634 | """ 635 | if session: 636 | name = self._mk_session_name(name) 637 | 638 | return super(Workflow3, self).cached_data(name, data_func, max_age) 639 | 640 | def clear_session_cache(self, current=False): 641 | """Remove session data from the cache. 642 | 643 | .. versionadded:: 1.25 644 | .. versionchanged:: 1.27 645 | 646 | By default, data belonging to the current session won't be 647 | deleted. Set ``current=True`` to also clear current session. 648 | 649 | Args: 650 | current (bool, optional): If ``True``, also remove data for 651 | current session. 652 | 653 | """ 654 | def _is_session_file(filename): 655 | if current: 656 | return filename.startswith('_wfsess-') 657 | return filename.startswith('_wfsess-') \ 658 | and not filename.startswith(self._session_prefix) 659 | 660 | self.clear_cache(_is_session_file) 661 | 662 | @property 663 | def obj(self): 664 | """Feedback formatted for JSON serialization. 665 | 666 | Returns: 667 | dict: Data suitable for Alfred 3 feedback. 668 | 669 | """ 670 | items = [] 671 | for item in self._items: 672 | items.append(item.obj) 673 | 674 | o = {'items': items} 675 | if self.variables: 676 | o['variables'] = self.variables 677 | if self.rerun: 678 | o['rerun'] = self.rerun 679 | return o 680 | 681 | def send_feedback(self): 682 | """Print stored items to console/Alfred as JSON.""" 683 | json.dump(self.obj, sys.stdout) 684 | sys.stdout.flush() 685 | -------------------------------------------------------------------------------- /src/workflow/workflow3.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amoose136/call_or_sms_contact/b404044127cd5e207ca960b8b240bac90a7197d6/src/workflow/workflow3.pyc --------------------------------------------------------------------------------