├── .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
--------------------------------------------------------------------------------