├── .gitignore
├── LICENSE
├── Linggle.alfredworkflow
├── README.md
├── snapshot1.png
├── snapshot2.png
└── src
├── 0B3C3531-021F-4008-9760-04C866481BE1.png
├── Alfred_Workflow-1.40.0.dist-info
├── INSTALLER
├── METADATA
├── RECORD
├── REQUESTED
├── WHEEL
└── top_level.txt
├── icon.png
├── info.plist
├── linggle.py
├── linggle_example.py
└── workflow
├── .alfredversionchecked
├── Notify.tgz
├── __init__.py
├── background.py
├── notify.py
├── update.py
├── util.py
├── version
├── web.py
├── workflow.py
└── workflow3.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/Linggle.alfredworkflow:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/Linggle.alfredworkflow
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Linggle for Alfred
2 |
3 | An Alfred 3 workflow for seaching collocations in [Linggle](https://linggle.com)!
4 |
5 | ## Requirements
6 |
7 | - Alfred >= 3.8
8 | - Python 2.x
9 | - `requests`
10 |
11 | ## Quick Start
12 |
13 | - Toggle by `lin `.
14 | - Press `↩` to copy the results.
15 |
16 | ## Snapshots
17 |
18 | 
19 |
20 | 
--------------------------------------------------------------------------------
/snapshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/snapshot1.png
--------------------------------------------------------------------------------
/snapshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/snapshot2.png
--------------------------------------------------------------------------------
/src/0B3C3531-021F-4008-9760-04C866481BE1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/src/0B3C3531-021F-4008-9760-04C866481BE1.png
--------------------------------------------------------------------------------
/src/Alfred_Workflow-1.40.0.dist-info/INSTALLER:
--------------------------------------------------------------------------------
1 | pip
2 |
--------------------------------------------------------------------------------
/src/Alfred_Workflow-1.40.0.dist-info/METADATA:
--------------------------------------------------------------------------------
1 | Metadata-Version: 2.1
2 | Name: Alfred-Workflow
3 | Version: 1.40.0
4 | Summary: Full-featured helper library for writing Alfred 2/3/4 workflows
5 | Home-page: http://www.deanishe.net/alfred-workflow/
6 | Author: Dean Jackson
7 | Author-email: deanishe@deanishe.net
8 | License: UNKNOWN
9 | Keywords: alfred workflow alfred4
10 | Platform: UNKNOWN
11 | Classifier: Development Status :: 5 - Production/Stable
12 | Classifier: License :: OSI Approved :: MIT License
13 | Classifier: Operating System :: MacOS :: MacOS X
14 | Classifier: Intended Audience :: Developers
15 | Classifier: Natural Language :: English
16 | Classifier: Programming Language :: Python :: 2.7
17 | Classifier: Topic :: Software Development :: Libraries
18 | Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
19 |
20 | A helper library for writing `Alfred 2, 3 and 4`_ workflows.
21 |
22 | Supports macOS 10.7+ and Python 2.7 (Alfred 3 is 10.9+/2.7 only).
23 |
24 | Alfred-Workflow is designed to take the grunt work out of writing a workflow.
25 |
26 | It gives you the tools to create a fast and featureful Alfred workflow from an
27 | API, application or library in minutes.
28 |
29 | http://www.deanishe.net/alfred-workflow/
30 |
31 |
32 | Features
33 | ========
34 |
35 | * Catches and logs workflow errors for easier development and support
36 | * "Magic" arguments to help development/debugging
37 | * Auto-saves settings
38 | * Super-simple data caching
39 | * Fuzzy, Alfred-like search/filtering with diacritic folding
40 | * Keychain support for secure storage (and syncing) of passwords, API keys etc.
41 | * Simple generation of Alfred feedback (XML output)
42 | * Input/output decoding for handling non-ASCII text
43 | * Lightweight web API with modelled on `requests`_
44 | * Pre-configured logging
45 | * Painlessly add directories to ``sys.path``
46 | * Easily launch background tasks (daemons) to keep your workflow responsive
47 | * Check for new versions and update workflows hosted on GitHub.
48 | * Post notifications via Notification Center.
49 |
50 |
51 | Alfred 3-only features
52 | ----------------------
53 |
54 | * Set `workflow variables`_ from code
55 | * Advanced modifiers
56 | * Alfred 3-only updates (won't break Alfred 2 installs)
57 | * Re-running Script Filters
58 |
59 |
60 | Quick Example
61 | =============
62 |
63 | Here's how to show recent `Pinboard.in `_ posts
64 | in Alfred.
65 |
66 | Create a new workflow in Alfred's preferences. Add a **Script Filter** with
67 | Language ``/usr/bin/python`` and paste the following into the **Script**
68 | field (changing ``API_KEY``):
69 |
70 |
71 | .. code-block:: python
72 |
73 | import sys
74 | from workflow import Workflow, ICON_WEB, web
75 |
76 | API_KEY = 'your-pinboard-api-key'
77 |
78 | def main(wf):
79 | url = 'https://api.pinboard.in/v1/posts/recent'
80 | params = dict(auth_token=API_KEY, count=20, format='json')
81 | r = web.get(url, params)
82 | r.raise_for_status()
83 | for post in r.json()['posts']:
84 | wf.add_item(post['description'], post['href'], arg=post['href'],
85 | uid=post['hash'], valid=True, icon=ICON_WEB)
86 | wf.send_feedback()
87 |
88 |
89 | if __name__ == u"__main__":
90 | wf = Workflow()
91 | sys.exit(wf.run(main))
92 |
93 |
94 | Add an **Open URL** action to your workflow with ``{query}`` as the **URL**,
95 | connect your **Script Filter** to it, and you can now hit **ENTER** on a
96 | Pinboard item in Alfred to open it in your browser.
97 |
98 |
99 | Installation
100 | ============
101 |
102 | **Note**: If you intend to distribute your workflow to other users, you
103 | should include Alfred-Workflow (and other Python libraries your workflow
104 | requires) within your workflow's directory as described below. **Do not**
105 | ask users to install anything into their system Python. Python installations
106 | cannot support multiple versions of the same library, so if you rely on
107 | globally-installed libraries, the chances are very good that your workflow
108 | will sooner or later break—or be broken by—some other software doing the
109 | same naughty thing.
110 |
111 |
112 | With pip
113 | --------
114 |
115 | You can install Alfred-Workflow directly into your workflow with::
116 |
117 | # from within your workflow directory
118 | pip install --target=. Alfred-Workflow
119 |
120 | You can install any other library available on the `Cheese Shop`_ the
121 | same way. See the `pip documentation`_ for more information.
122 |
123 |
124 | From source
125 | -----------
126 |
127 | Download the ``alfred-workflow-X.X.X.zip`` file from the `GitHub releases`_
128 | page and extract the ZIP to the root directory of your workflow (where
129 | ``info.plist`` is).
130 |
131 | Alternatively, you can download `the source code`_ from the
132 | `GitHub repository`_ and copy the ``workflow`` subfolder to the root
133 | directory of your workflow.
134 |
135 | Your workflow directory should look something like this (where
136 | ``yourscript.py`` contains your workflow code and ``info.plist`` is
137 | the workflow information file generated by Alfred)::
138 |
139 | Your Workflow/
140 | info.plist
141 | icon.png
142 | workflow/
143 | __init__.py
144 | background.py
145 | notify.py
146 | Notify.tgz
147 | update.py
148 | version
149 | web.py
150 | workflow.py
151 | yourscript.py
152 | etc.
153 |
154 |
155 | Documentation
156 | =============
157 |
158 | Detailed documentation, including a tutorial, is available at
159 | http://www.deanishe.net/alfred-workflow/.
160 |
161 | .. _v2 branch: https://github.com/deanishe/alfred-workflow/tree/v2
162 | .. _requests: http://docs.python-requests.org/en/latest/
163 | .. _Alfred 2, 3 and 4: http://www.alfredapp.com/
164 | .. _GitHub releases: https://github.com/deanishe/alfred-workflow/releases
165 | .. _the source code: https://github.com/deanishe/alfred-workflow/archive/master.zip
166 | .. _GitHub repository: https://github.com/deanishe/alfred-workflow
167 | .. _Cheese Shop: https://pypi.python.org/pypi
168 | .. _pip documentation: https://pip.pypa.io/en/latest/
169 | .. _workflow variables: http://www.deanishe.net/alfred-workflow/user-manual/workflow-variables.html
170 |
171 |
172 |
--------------------------------------------------------------------------------
/src/Alfred_Workflow-1.40.0.dist-info/RECORD:
--------------------------------------------------------------------------------
1 | Alfred_Workflow-1.40.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
2 | Alfred_Workflow-1.40.0.dist-info/METADATA,sha256=DOq1DTBb8GmWrOOo5OJf_G1IqswdOEM0hCjlhi7ZxQ0,5609
3 | Alfred_Workflow-1.40.0.dist-info/RECORD,,
4 | Alfred_Workflow-1.40.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5 | Alfred_Workflow-1.40.0.dist-info/WHEEL,sha256=pqI-DBMA-Z6OTNov1nVxs7mwm6Yj2kHZGNp_6krVn1E,92
6 | Alfred_Workflow-1.40.0.dist-info/top_level.txt,sha256=jT-znOUjxvwdr-w5ECrvROWZ9y_Doiz0yVYSI0VxpXA,9
7 | workflow/Notify.tgz,sha256=dfcN09jNo0maLZLIZDSsBDouynsjgtDMSnSL3UfFcRE,35556
8 | workflow/__init__.py,sha256=Ae2f8xQxpZE3ijEYgSNir8h-XW04_sNUxkY3vmplOcQ,2068
9 | workflow/__init__.pyc,,
10 | workflow/background.py,sha256=DxSQ3NSJADuW94BWiykKJbcWywhWKdyjSfT4HczhiCw,7532
11 | workflow/background.pyc,,
12 | workflow/notify.py,sha256=OuD5wDd0qwxcH2hLD6RoqFZyG4I-yrMvoaUmtrkQ-to,9670
13 | workflow/notify.pyc,,
14 | workflow/update.py,sha256=0n4Yvfiin4AMQuP2orzDZ31SB0ZGJ2l0CA2DISdnt4k,16133
15 | workflow/update.pyc,,
16 | workflow/util.py,sha256=QE3MJOj8Cj7LzB2gHXNZI2HqQ13vivU4wY_FkHpk3sc,18256
17 | workflow/util.pyc,,
18 | workflow/version,sha256=_sWfmyrEjikcQfQVyndvj90fs4KQPeqGIb8e85nzj1c,6
19 | workflow/web.py,sha256=TG_Sv0RJYBlyTRLG9BNjnExg2YeQMnz8JwFteNq2ksU,22093
20 | workflow/web.pyc,,
21 | workflow/workflow.py,sha256=oFxsLKK0E9L6ZM9RRgPa-86zVgBy2HXuPcWZffTM6JY,92565
22 | workflow/workflow.pyc,,
23 | workflow/workflow3.py,sha256=_Gm3IjLDp82YRJiIq1mDnXNT4lWPvAX02_h9wARzJ-8,21854
24 | workflow/workflow3.pyc,,
25 |
--------------------------------------------------------------------------------
/src/Alfred_Workflow-1.40.0.dist-info/REQUESTED:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/src/Alfred_Workflow-1.40.0.dist-info/REQUESTED
--------------------------------------------------------------------------------
/src/Alfred_Workflow-1.40.0.dist-info/WHEEL:
--------------------------------------------------------------------------------
1 | Wheel-Version: 1.0
2 | Generator: bdist_wheel (0.33.1)
3 | Root-Is-Purelib: true
4 | Tag: py2-none-any
5 |
6 |
--------------------------------------------------------------------------------
/src/Alfred_Workflow-1.40.0.dist-info/top_level.txt:
--------------------------------------------------------------------------------
1 | workflow
2 |
--------------------------------------------------------------------------------
/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/src/icon.png
--------------------------------------------------------------------------------
/src/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bundleid
6 |
7 | category
8 | Tools
9 | connections
10 |
11 | 0B3C3531-021F-4008-9760-04C866481BE1
12 |
13 |
14 | destinationuid
15 | 4C7BCA08-E83D-46CD-A019-6B547E9E188D
16 | modifiers
17 | 0
18 | modifiersubtext
19 |
20 | vitoclose
21 |
22 |
23 |
24 | 4C7BCA08-E83D-46CD-A019-6B547E9E188D
25 |
26 |
27 | destinationuid
28 | A3349239-1B3D-4251-B06B-FA08DC93D821
29 | modifiers
30 | 0
31 | modifiersubtext
32 |
33 | sourceoutputuid
34 | BE62F4C5-342A-401E-BC91-755F2084CE3C
35 | vitoclose
36 |
37 |
38 |
39 | destinationuid
40 | E9359454-1A8F-475D-AF7B-66EAB5CBAAB0
41 | modifiers
42 | 0
43 | modifiersubtext
44 |
45 | vitoclose
46 |
47 |
48 |
49 | 5E1FACD5-59E8-4FB2-9969-7DC53462AF90
50 |
51 | E9359454-1A8F-475D-AF7B-66EAB5CBAAB0
52 |
53 |
54 | destinationuid
55 | 5E1FACD5-59E8-4FB2-9969-7DC53462AF90
56 | modifiers
57 | 0
58 | modifiersubtext
59 |
60 | vitoclose
61 |
62 |
63 |
64 |
65 | createdby
66 | SXKDZ
67 | description
68 |
69 | disabled
70 |
71 | name
72 | Linggle
73 | objects
74 |
75 |
76 | config
77 |
78 | browser
79 |
80 | spaces
81 |
82 | url
83 | {query}
84 | utf8
85 |
86 |
87 | type
88 | alfred.workflow.action.openurl
89 | uid
90 | A3349239-1B3D-4251-B06B-FA08DC93D821
91 | version
92 | 1
93 |
94 |
95 | config
96 |
97 | alfredfiltersresults
98 |
99 | alfredfiltersresultsmatchmode
100 | 0
101 | argumenttreatemptyqueryasnil
102 |
103 | argumenttrimmode
104 | 0
105 | argumenttype
106 | 0
107 | escaping
108 | 102
109 | keyword
110 | lin
111 | queuedelaycustom
112 | 3
113 | queuedelayimmediatelyinitially
114 |
115 | queuedelaymode
116 | 0
117 | queuemode
118 | 1
119 | runningsubtext
120 | Searching...
121 | script
122 | python linggle.py "{query}"
123 | scriptargtype
124 | 0
125 | scriptfile
126 |
127 | subtext
128 | * word | _ words | ? necessity | / substitution | n. etc. POS
129 | title
130 | Search Collocations in Linggle 10^12
131 | type
132 | 0
133 | withspace
134 |
135 |
136 | type
137 | alfred.workflow.input.scriptfilter
138 | uid
139 | 0B3C3531-021F-4008-9760-04C866481BE1
140 | version
141 | 3
142 |
143 |
144 | config
145 |
146 | conditions
147 |
148 |
149 | inputstring
150 |
151 | matchcasesensitive
152 |
153 | matchmode
154 | 4
155 | matchstring
156 | ^https:\/\/linggle\.com\/\?q=.+
157 | outputlabel
158 |
159 | uid
160 | BE62F4C5-342A-401E-BC91-755F2084CE3C
161 |
162 |
163 | elselabel
164 | else
165 |
166 | type
167 | alfred.workflow.utility.conditional
168 | uid
169 | 4C7BCA08-E83D-46CD-A019-6B547E9E188D
170 | version
171 | 1
172 |
173 |
174 | config
175 |
176 | lastpathcomponent
177 |
178 | onlyshowifquerypopulated
179 |
180 | removeextension
181 |
182 | text
183 | {query}
184 | title
185 | Copied to Clipboard
186 |
187 | type
188 | alfred.workflow.output.notification
189 | uid
190 | 5E1FACD5-59E8-4FB2-9969-7DC53462AF90
191 | version
192 | 1
193 |
194 |
195 | config
196 |
197 | autopaste
198 |
199 | clipboardtext
200 | {query}
201 | ignoredynamicplaceholders
202 |
203 | transient
204 |
205 |
206 | type
207 | alfred.workflow.output.clipboard
208 | uid
209 | E9359454-1A8F-475D-AF7B-66EAB5CBAAB0
210 | version
211 | 3
212 |
213 |
214 | readme
215 |
216 | uidata
217 |
218 | 0B3C3531-021F-4008-9760-04C866481BE1
219 |
220 | xpos
221 | 35
222 | ypos
223 | 150
224 |
225 | 4C7BCA08-E83D-46CD-A019-6B547E9E188D
226 |
227 | xpos
228 | 240
229 | ypos
230 | 170
231 |
232 | 5E1FACD5-59E8-4FB2-9969-7DC53462AF90
233 |
234 | xpos
235 | 565
236 | ypos
237 | 290
238 |
239 | A3349239-1B3D-4251-B06B-FA08DC93D821
240 |
241 | xpos
242 | 370
243 | ypos
244 | 10
245 |
246 | E9359454-1A8F-475D-AF7B-66EAB5CBAAB0
247 |
248 | xpos
249 | 370
250 | ypos
251 | 290
252 |
253 |
254 | variablesdontexport
255 |
256 | version
257 |
258 | webaddress
259 | sxkdz.github.io
260 |
261 |
262 |
--------------------------------------------------------------------------------
/src/linggle.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import json
3 | import time
4 | import requests
5 | from workflow import Workflow, ICON_WEB
6 |
7 | def main(wf):
8 | s = requests.Session()
9 |
10 | query = wf.args[0]
11 | query_load = requests.utils.quote(query)
12 | try:
13 | answer = s.get('https://search.linggle.com/api/ngram/{}'.format(query_load)).json()
14 | if len(answer['ngrams']) == 0:
15 | wf.add_item(
16 | title='No Results',
17 | subtitle='Modify your search',
18 | valid=False,
19 | icon='icon.png'
20 | )
21 | else:
22 | total = 0
23 | for item in answer['ngrams']:
24 | total += item[1]
25 |
26 | for item in answer['ngrams'][:20]:
27 | phrase = item[0]
28 | subtitle = '{:.2f}% | {}'.format(float(item[1]) * 100 / total, item[1])
29 | wf.add_item(
30 | title=phrase,
31 | subtitle=subtitle,
32 | arg=phrase,
33 | valid=True,
34 | icon='icon.png'
35 | )
36 | except:
37 | wf.add_item(
38 | title='Inquiry Error',
39 | subtitle='Modify your search',
40 | valid=False,
41 | icon='icon.png'
42 | )
43 | wf.add_item(
44 | title='Visit Linggle',
45 | subtitle='Open browser for Linggle',
46 | icon=ICON_WEB,
47 | valid=True,
48 | arg='https://search.linggle.com/?q={}'.format(query_load)
49 | )
50 | wf.send_feedback()
51 |
52 | if __name__ == '__main__':
53 | wf = Workflow()
54 | sys.exit(wf.run(main))
55 |
--------------------------------------------------------------------------------
/src/linggle_example.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 | import requests
4 | from workflow import Workflow, ICON_WEB
5 |
6 |
7 | def strip_html(html):
8 | p = re.compile(r'<.*?>')
9 | return p.sub('', html)
10 |
11 |
12 | def main(wf):
13 | s = requests.Session()
14 |
15 | query = wf.args[0]
16 | query_load = requests.utils.quote(query)
17 | payload = {'q': query, 'maxResults': 20}
18 |
19 | proxies = {
20 | 'http': 'http://127.0.0.1:1087',
21 | 'https': 'http://127.0.0.1:1087',
22 | }
23 |
24 | try:
25 | answer = s.get('https://www.googleapis.com/books/v1/volumes', params=payload, proxies=proxies).json()
26 | if int(answer['totalItems']) == 0:
27 | wf.add_item(
28 | title='No Examples',
29 | subtitle='Try another search',
30 | valid=False,
31 | icon='icon.png'
32 | )
33 | else:
34 | for item in answer['items']:
35 | try:
36 | phrase = item['searchInfo']['textSnippet']
37 | phrase = strip_html(phrase)
38 | wf.add_item(
39 | title=phrase,
40 | arg=phrase,
41 | valid=True,
42 | icon='icon.png'
43 | )
44 | except KeyError:
45 | pass
46 |
47 | except Exception as e:
48 | wf.add_item(
49 | title='Inquiry Error: {}'.format(e),
50 | subtitle='Modify your search',
51 | valid=False,
52 | icon='icon.png'
53 | )
54 | wf.add_item(
55 | title='Visit Linggle',
56 | subtitle='Open browser for Linggle',
57 | icon=ICON_WEB,
58 | valid=True,
59 | arg='https://linggle.com/?q={}'.format(query_load)
60 | )
61 | wf.send_feedback()
62 |
63 | if __name__ == '__main__':
64 | wf = Workflow()
65 | sys.exit(wf.run(main))
66 |
--------------------------------------------------------------------------------
/src/workflow/.alfredversionchecked:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/src/workflow/.alfredversionchecked
--------------------------------------------------------------------------------
/src/workflow/Notify.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SXKDZ/linggle_alfred/8d3ad8664c92c010be39a4b6d2f34c0707318b65/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-2019 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/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 | """This module provides an API to run commands in background processes.
12 |
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 | os.unlink(pidfile)
106 |
107 |
108 | def is_running(name):
109 | """Test whether task ``name`` is currently running.
110 |
111 | :param name: name of task
112 | :type name: unicode
113 | :returns: ``True`` if task with name ``name`` is running, else ``False``
114 | :rtype: bool
115 |
116 | """
117 | if _job_pid(name) is not None:
118 | return True
119 |
120 | return False
121 |
122 |
123 | def _background(pidfile, stdin='/dev/null', stdout='/dev/null',
124 | stderr='/dev/null'): # pragma: no cover
125 | """Fork the current process into a background daemon.
126 |
127 | :param pidfile: file to write PID of daemon process to.
128 | :type pidfile: filepath
129 | :param stdin: where to read input
130 | :type stdin: filepath
131 | :param stdout: where to write stdout output
132 | :type stdout: filepath
133 | :param stderr: where to write stderr output
134 | :type stderr: filepath
135 |
136 | """
137 | def _fork_and_exit_parent(errmsg, wait=False, write=False):
138 | try:
139 | pid = os.fork()
140 | if pid > 0:
141 | if write: # write PID of child process to `pidfile`
142 | tmp = pidfile + '.tmp'
143 | with open(tmp, 'wb') as fp:
144 | fp.write(str(pid))
145 | os.rename(tmp, pidfile)
146 | if wait: # wait for child process to exit
147 | os.waitpid(pid, 0)
148 | os._exit(0)
149 | except OSError as err:
150 | _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror)
151 | raise err
152 |
153 | # Do first fork and wait for second fork to finish.
154 | _fork_and_exit_parent('fork #1 failed', wait=True)
155 |
156 | # Decouple from parent environment.
157 | os.chdir(wf().workflowdir)
158 | os.setsid()
159 |
160 | # Do second fork and write PID to pidfile.
161 | _fork_and_exit_parent('fork #2 failed', write=True)
162 |
163 | # Now I am a daemon!
164 | # Redirect standard file descriptors.
165 | si = open(stdin, 'r', 0)
166 | so = open(stdout, 'a+', 0)
167 | se = open(stderr, 'a+', 0)
168 | if hasattr(sys.stdin, 'fileno'):
169 | os.dup2(si.fileno(), sys.stdin.fileno())
170 | if hasattr(sys.stdout, 'fileno'):
171 | os.dup2(so.fileno(), sys.stdout.fileno())
172 | if hasattr(sys.stderr, 'fileno'):
173 | os.dup2(se.fileno(), sys.stderr.fileno())
174 |
175 |
176 | def kill(name, sig=signal.SIGTERM):
177 | """Send a signal to job ``name`` via :func:`os.kill`.
178 |
179 | .. versionadded:: 1.29
180 |
181 | Args:
182 | name (str): Name of the job
183 | sig (int, optional): Signal to send (default: SIGTERM)
184 |
185 | Returns:
186 | bool: `False` if job isn't running, `True` if signal was sent.
187 | """
188 | pid = _job_pid(name)
189 | if pid is None:
190 | return False
191 |
192 | os.kill(pid, sig)
193 | return True
194 |
195 |
196 | def run_in_background(name, args, **kwargs):
197 | r"""Cache arguments then call this script again via :func:`subprocess.call`.
198 |
199 | :param name: name of job
200 | :type name: unicode
201 | :param args: arguments passed as first argument to :func:`subprocess.call`
202 | :param \**kwargs: keyword arguments to :func:`subprocess.call`
203 | :returns: exit code of sub-process
204 | :rtype: int
205 |
206 | When you call this function, it caches its arguments and then calls
207 | ``background.py`` in a subprocess. The Python subprocess will load the
208 | cached arguments, fork into the background, and then run the command you
209 | specified.
210 |
211 | This function will return as soon as the ``background.py`` subprocess has
212 | forked, returning the exit code of *that* process (i.e. not of the command
213 | you're trying to run).
214 |
215 | If that process fails, an error will be written to the log file.
216 |
217 | If a process is already running under the same name, this function will
218 | return immediately and will not run the specified command.
219 |
220 | """
221 | if is_running(name):
222 | _log().info('[%s] job already running', name)
223 | return
224 |
225 | argcache = _arg_cache(name)
226 |
227 | # Cache arguments
228 | with open(argcache, 'wb') as fp:
229 | pickle.dump({'args': args, 'kwargs': kwargs}, fp)
230 | _log().debug('[%s] command cached: %s', name, argcache)
231 |
232 | # Call this script
233 | cmd = ['/usr/bin/python', __file__, name]
234 | _log().debug('[%s] passing job to background runner: %r', name, cmd)
235 | retcode = subprocess.call(cmd)
236 |
237 | if retcode: # pragma: no cover
238 | _log().error('[%s] background runner failed with %d', name, retcode)
239 | else:
240 | _log().debug('[%s] background job started', name)
241 |
242 | return retcode
243 |
244 |
245 | def main(wf): # pragma: no cover
246 | """Run command in a background process.
247 |
248 | Load cached arguments, fork into background, then call
249 | :meth:`subprocess.call` with cached arguments.
250 |
251 | """
252 | log = wf.logger
253 | name = wf.args[0]
254 | argcache = _arg_cache(name)
255 | if not os.path.exists(argcache):
256 | msg = '[{0}] command cache not found: {1}'.format(name, argcache)
257 | log.critical(msg)
258 | raise IOError(msg)
259 |
260 | # Fork to background and run command
261 | pidfile = _pid_file(name)
262 | _background(pidfile)
263 |
264 | # Load cached arguments
265 | with open(argcache, 'rb') as fp:
266 | data = pickle.load(fp)
267 |
268 | # Cached arguments
269 | args = data['args']
270 | kwargs = data['kwargs']
271 |
272 | # Delete argument cache file
273 | os.unlink(argcache)
274 |
275 | try:
276 | # Run the command
277 | log.debug('[%s] running command: %r', name, args)
278 |
279 | retcode = subprocess.call(args, **kwargs)
280 |
281 | if retcode:
282 | log.error('[%s] command failed with status %d', name, retcode)
283 | finally:
284 | os.unlink(pidfile)
285 |
286 | log.debug('[%s] job complete', name)
287 |
288 |
289 | if __name__ == '__main__': # pragma: no cover
290 | wf().run(main)
291 |
--------------------------------------------------------------------------------
/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.
15 |
16 | This feature is only available on Mountain Lion (10.8) and later.
17 | It will silently fail on older systems.
18 |
19 | The main API is a single function, :func:`~workflow.notify.notify`.
20 |
21 | It works by copying a simple application to your workflow's data
22 | directory. It replaces the application's icon with your workflow's
23 | icon and then calls the application to post notifications.
24 | """
25 |
26 | from __future__ import print_function, unicode_literals
27 |
28 | import os
29 | import plistlib
30 | import shutil
31 | import subprocess
32 | import sys
33 | import tarfile
34 | import tempfile
35 | import uuid
36 |
37 | import workflow
38 |
39 |
40 | _wf = None
41 | _log = None
42 |
43 |
44 | #: Available system sounds from System Preferences > Sound > Sound Effects
45 | SOUNDS = (
46 | 'Basso',
47 | 'Blow',
48 | 'Bottle',
49 | 'Frog',
50 | 'Funk',
51 | 'Glass',
52 | 'Hero',
53 | 'Morse',
54 | 'Ping',
55 | 'Pop',
56 | 'Purr',
57 | 'Sosumi',
58 | 'Submarine',
59 | 'Tink',
60 | )
61 |
62 |
63 | def wf():
64 | """Return Workflow object for this module.
65 |
66 | Returns:
67 | workflow.Workflow: Workflow object for current workflow.
68 | """
69 | global _wf
70 | if _wf is None:
71 | _wf = workflow.Workflow()
72 | return _wf
73 |
74 |
75 | def log():
76 | """Return logger for this module.
77 |
78 | Returns:
79 | logging.Logger: Logger for this module.
80 | """
81 | global _log
82 | if _log is None:
83 | _log = wf().logger
84 | return _log
85 |
86 |
87 | def notifier_program():
88 | """Return path to notifier applet executable.
89 |
90 | Returns:
91 | unicode: Path to Notify.app ``applet`` executable.
92 | """
93 | return wf().datafile('Notify.app/Contents/MacOS/applet')
94 |
95 |
96 | def notifier_icon_path():
97 | """Return path to icon file in installed Notify.app.
98 |
99 | Returns:
100 | unicode: Path to ``applet.icns`` within the app bundle.
101 | """
102 | return wf().datafile('Notify.app/Contents/Resources/applet.icns')
103 |
104 |
105 | def install_notifier():
106 | """Extract ``Notify.app`` from the workflow to data directory.
107 |
108 | Changes the bundle ID of the installed app and gives it the
109 | workflow's icon.
110 | """
111 | archive = os.path.join(os.path.dirname(__file__), 'Notify.tgz')
112 | destdir = wf().datadir
113 | app_path = os.path.join(destdir, 'Notify.app')
114 | n = notifier_program()
115 | log().debug('installing Notify.app to %r ...', destdir)
116 | # z = zipfile.ZipFile(archive, 'r')
117 | # z.extractall(destdir)
118 | tgz = tarfile.open(archive, 'r:gz')
119 | tgz.extractall(destdir)
120 | if not os.path.exists(n): # pragma: nocover
121 | raise RuntimeError('Notify.app could not be installed in ' + destdir)
122 |
123 | # Replace applet icon
124 | icon = notifier_icon_path()
125 | workflow_icon = wf().workflowfile('icon.png')
126 | if os.path.exists(icon):
127 | os.unlink(icon)
128 |
129 | png_to_icns(workflow_icon, icon)
130 |
131 | # Set file icon
132 | # PyObjC isn't available for 2.6, so this is 2.7 only. Actually,
133 | # none of this code will "work" on pre-10.8 systems. Let it run
134 | # until I figure out a better way of excluding this module
135 | # from coverage in py2.6.
136 | if sys.version_info >= (2, 7): # pragma: no cover
137 | from AppKit import NSWorkspace, NSImage
138 |
139 | ws = NSWorkspace.sharedWorkspace()
140 | img = NSImage.alloc().init()
141 | img.initWithContentsOfFile_(icon)
142 | ws.setIcon_forFile_options_(img, app_path, 0)
143 |
144 | # Change bundle ID of installed app
145 | ip_path = os.path.join(app_path, 'Contents/Info.plist')
146 | bundle_id = '{0}.{1}'.format(wf().bundleid, uuid.uuid4().hex)
147 | data = plistlib.readPlist(ip_path)
148 | log().debug('changing bundle ID to %r', bundle_id)
149 | data['CFBundleIdentifier'] = bundle_id
150 | plistlib.writePlist(data, ip_path)
151 |
152 |
153 | def validate_sound(sound):
154 | """Coerce ``sound`` to valid sound name.
155 |
156 | Returns ``None`` for invalid sounds. Sound names can be found
157 | in ``System Preferences > Sound > Sound Effects``.
158 |
159 | Args:
160 | sound (str): Name of system sound.
161 |
162 | Returns:
163 | str: Proper name of sound or ``None``.
164 | """
165 | if not sound:
166 | return None
167 |
168 | # Case-insensitive comparison of `sound`
169 | if sound.lower() in [s.lower() for s in SOUNDS]:
170 | # Title-case is correct for all system sounds as of macOS 10.11
171 | return sound.title()
172 | return None
173 |
174 |
175 | def notify(title='', text='', sound=None):
176 | """Post notification via Notify.app helper.
177 |
178 | Args:
179 | title (str, optional): Notification title.
180 | text (str, optional): Notification body text.
181 | sound (str, optional): Name of sound to play.
182 |
183 | Raises:
184 | ValueError: Raised if both ``title`` and ``text`` are empty.
185 |
186 | Returns:
187 | bool: ``True`` if notification was posted, else ``False``.
188 | """
189 | if title == text == '':
190 | raise ValueError('Empty notification')
191 |
192 | sound = validate_sound(sound) or ''
193 |
194 | n = notifier_program()
195 |
196 | if not os.path.exists(n):
197 | install_notifier()
198 |
199 | env = os.environ.copy()
200 | enc = 'utf-8'
201 | env['NOTIFY_TITLE'] = title.encode(enc)
202 | env['NOTIFY_MESSAGE'] = text.encode(enc)
203 | env['NOTIFY_SOUND'] = sound.encode(enc)
204 | cmd = [n]
205 | retcode = subprocess.call(cmd, env=env)
206 | if retcode == 0:
207 | return True
208 |
209 | log().error('Notify.app exited with status {0}.'.format(retcode))
210 | return False
211 |
212 |
213 | def convert_image(inpath, outpath, size):
214 | """Convert an image file using ``sips``.
215 |
216 | Args:
217 | inpath (str): Path of source file.
218 | outpath (str): Path to destination file.
219 | size (int): Width and height of destination image in pixels.
220 |
221 | Raises:
222 | RuntimeError: Raised if ``sips`` exits with non-zero status.
223 | """
224 | cmd = [
225 | b'sips',
226 | b'-z', str(size), str(size),
227 | inpath,
228 | b'--out', outpath]
229 | # log().debug(cmd)
230 | with open(os.devnull, 'w') as pipe:
231 | retcode = subprocess.call(cmd, stdout=pipe, stderr=subprocess.STDOUT)
232 |
233 | if retcode != 0:
234 | raise RuntimeError('sips exited with %d' % retcode)
235 |
236 |
237 | def png_to_icns(png_path, icns_path):
238 | """Convert PNG file to ICNS using ``iconutil``.
239 |
240 | Create an iconset from the source PNG file. Generate PNG files
241 | in each size required by macOS, then call ``iconutil`` to turn
242 | them into a single ICNS file.
243 |
244 | Args:
245 | png_path (str): Path to source PNG file.
246 | icns_path (str): Path to destination ICNS file.
247 |
248 | Raises:
249 | RuntimeError: Raised if ``iconutil`` or ``sips`` fail.
250 | """
251 | tempdir = tempfile.mkdtemp(prefix='aw-', dir=wf().datadir)
252 |
253 | try:
254 | iconset = os.path.join(tempdir, 'Icon.iconset')
255 |
256 | if os.path.exists(iconset): # pragma: nocover
257 | raise RuntimeError('iconset already exists: ' + iconset)
258 |
259 | os.makedirs(iconset)
260 |
261 | # Copy source icon to icon set and generate all the other
262 | # sizes needed
263 | configs = []
264 | for i in (16, 32, 128, 256, 512):
265 | configs.append(('icon_{0}x{0}.png'.format(i), i))
266 | configs.append((('icon_{0}x{0}@2x.png'.format(i), i * 2)))
267 |
268 | shutil.copy(png_path, os.path.join(iconset, 'icon_256x256.png'))
269 | shutil.copy(png_path, os.path.join(iconset, 'icon_128x128@2x.png'))
270 |
271 | for name, size in configs:
272 | outpath = os.path.join(iconset, name)
273 | if os.path.exists(outpath):
274 | continue
275 | convert_image(png_path, outpath, size)
276 |
277 | cmd = [
278 | b'iconutil',
279 | b'-c', b'icns',
280 | b'-o', icns_path,
281 | iconset]
282 |
283 | retcode = subprocess.call(cmd)
284 | if retcode != 0:
285 | raise RuntimeError('iconset exited with %d' % retcode)
286 |
287 | if not os.path.exists(icns_path): # pragma: nocover
288 | raise ValueError(
289 | 'generated ICNS file not found: ' + repr(icns_path))
290 | finally:
291 | try:
292 | shutil.rmtree(tempdir)
293 | except OSError: # pragma: no cover
294 | pass
295 |
296 |
297 | if __name__ == '__main__': # pragma: nocover
298 | # Simple command-line script to test module with
299 | # This won't work on 2.6, as `argparse` isn't available
300 | # by default.
301 | import argparse
302 |
303 | from unicodedata import normalize
304 |
305 | def ustr(s):
306 | """Coerce `s` to normalised Unicode."""
307 | return normalize('NFD', s.decode('utf-8'))
308 |
309 | p = argparse.ArgumentParser()
310 | p.add_argument('-p', '--png', help="PNG image to convert to ICNS.")
311 | p.add_argument('-l', '--list-sounds', help="Show available sounds.",
312 | action='store_true')
313 | p.add_argument('-t', '--title',
314 | help="Notification title.", type=ustr,
315 | default='')
316 | p.add_argument('-s', '--sound', type=ustr,
317 | help="Optional notification sound.", default='')
318 | p.add_argument('text', type=ustr,
319 | help="Notification body text.", default='', nargs='?')
320 | o = p.parse_args()
321 |
322 | # List available sounds
323 | if o.list_sounds:
324 | for sound in SOUNDS:
325 | print(sound)
326 | sys.exit(0)
327 |
328 | # Convert PNG to ICNS
329 | if o.png:
330 | icns = os.path.join(
331 | os.path.dirname(o.png),
332 | os.path.splitext(os.path.basename(o.png))[0] + '.icns')
333 |
334 | print('converting {0!r} to {1!r} ...'.format(o.png, icns),
335 | file=sys.stderr)
336 |
337 | if os.path.exists(icns):
338 | raise ValueError('destination file already exists: ' + icns)
339 |
340 | png_to_icns(o.png, icns)
341 | sys.exit(0)
342 |
343 | # Post notification
344 | if o.title == o.text == '':
345 | print('ERROR: empty notification.', file=sys.stderr)
346 | sys.exit(1)
347 | else:
348 | notify(o.title, o.text, o.sound)
349 |
--------------------------------------------------------------------------------
/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 | from collections import defaultdict
27 | from functools import total_ordering
28 | import json
29 | import os
30 | import tempfile
31 | import re
32 | import subprocess
33 |
34 | import workflow
35 | import web
36 |
37 | # __all__ = []
38 |
39 |
40 | RELEASES_BASE = 'https://api.github.com/repos/{}/releases'
41 | match_workflow = re.compile(r'\.alfred(\d+)?workflow$').search
42 |
43 | _wf = None
44 |
45 |
46 | def wf():
47 | """Lazy `Workflow` object."""
48 | global _wf
49 | if _wf is None:
50 | _wf = workflow.Workflow()
51 | return _wf
52 |
53 |
54 | @total_ordering
55 | class Download(object):
56 | """A workflow file that is available for download.
57 |
58 | .. versionadded: 1.37
59 |
60 | Attributes:
61 | url (str): URL of workflow file.
62 | filename (str): Filename of workflow file.
63 | version (Version): Semantic version of workflow.
64 | prerelease (bool): Whether version is a pre-release.
65 | alfred_version (Version): Minimum compatible version
66 | of Alfred.
67 |
68 | """
69 |
70 | @classmethod
71 | def from_dict(cls, d):
72 | """Create a `Download` from a `dict`."""
73 | return cls(url=d['url'], filename=d['filename'],
74 | version=Version(d['version']),
75 | prerelease=d['prerelease'])
76 |
77 | @classmethod
78 | def from_releases(cls, js):
79 | """Extract downloads from GitHub releases.
80 |
81 | Searches releases with semantic tags for assets with
82 | file extension .alfredworkflow or .alfredXworkflow where
83 | X is a number.
84 |
85 | Files are returned sorted by latest version first. Any
86 | releases containing multiple files with the same (workflow)
87 | extension are rejected as ambiguous.
88 |
89 | Args:
90 | js (str): JSON response from GitHub's releases endpoint.
91 |
92 | Returns:
93 | list: Sequence of `Download`.
94 | """
95 | releases = json.loads(js)
96 | downloads = []
97 | for release in releases:
98 | tag = release['tag_name']
99 | dupes = defaultdict(int)
100 | try:
101 | version = Version(tag)
102 | except ValueError as err:
103 | wf().logger.debug('ignored release: bad version "%s": %s',
104 | tag, err)
105 | continue
106 |
107 | dls = []
108 | for asset in release.get('assets', []):
109 | url = asset.get('browser_download_url')
110 | filename = os.path.basename(url)
111 | m = match_workflow(filename)
112 | if not m:
113 | wf().logger.debug('unwanted file: %s', filename)
114 | continue
115 |
116 | ext = m.group(0)
117 | dupes[ext] = dupes[ext] + 1
118 | dls.append(Download(url, filename, version,
119 | release['prerelease']))
120 |
121 | valid = True
122 | for ext, n in dupes.items():
123 | if n > 1:
124 | wf().logger.debug('ignored release "%s": multiple assets '
125 | 'with extension "%s"', tag, ext)
126 | valid = False
127 | break
128 |
129 | if valid:
130 | downloads.extend(dls)
131 |
132 | downloads.sort(reverse=True)
133 | return downloads
134 |
135 | def __init__(self, url, filename, version, prerelease=False):
136 | """Create a new Download.
137 |
138 | Args:
139 | url (str): URL of workflow file.
140 | filename (str): Filename of workflow file.
141 | version (Version): Version of workflow.
142 | prerelease (bool, optional): Whether version is
143 | pre-release. Defaults to False.
144 |
145 | """
146 | if isinstance(version, basestring):
147 | version = Version(version)
148 |
149 | self.url = url
150 | self.filename = filename
151 | self.version = version
152 | self.prerelease = prerelease
153 |
154 | @property
155 | def alfred_version(self):
156 | """Minimum Alfred version based on filename extension."""
157 | m = match_workflow(self.filename)
158 | if not m or not m.group(1):
159 | return Version('0')
160 | return Version(m.group(1))
161 |
162 | @property
163 | def dict(self):
164 | """Convert `Download` to `dict`."""
165 | return dict(url=self.url, filename=self.filename,
166 | version=str(self.version), prerelease=self.prerelease)
167 |
168 | def __str__(self):
169 | """Format `Download` for printing."""
170 | u = ('Download(url={dl.url!r}, '
171 | 'filename={dl.filename!r}, '
172 | 'version={dl.version!r}, '
173 | 'prerelease={dl.prerelease!r})'.format(dl=self))
174 |
175 | return u.encode('utf-8')
176 |
177 | def __repr__(self):
178 | """Code-like representation of `Download`."""
179 | return str(self)
180 |
181 | def __eq__(self, other):
182 | """Compare Downloads based on version numbers."""
183 | if self.url != other.url \
184 | or self.filename != other.filename \
185 | or self.version != other.version \
186 | or self.prerelease != other.prerelease:
187 | return False
188 | return True
189 |
190 | def __ne__(self, other):
191 | """Compare Downloads based on version numbers."""
192 | return not self.__eq__(other)
193 |
194 | def __lt__(self, other):
195 | """Compare Downloads based on version numbers."""
196 | if self.version != other.version:
197 | return self.version < other.version
198 | return self.alfred_version < other.alfred_version
199 |
200 |
201 | class Version(object):
202 | """Mostly semantic versioning.
203 |
204 | The main difference to proper :ref:`semantic versioning `
205 | is that this implementation doesn't require a minor or patch version.
206 |
207 | Version strings may also be prefixed with "v", e.g.:
208 |
209 | >>> v = Version('v1.1.1')
210 | >>> v.tuple
211 | (1, 1, 1, '')
212 |
213 | >>> v = Version('2.0')
214 | >>> v.tuple
215 | (2, 0, 0, '')
216 |
217 | >>> Version('3.1-beta').tuple
218 | (3, 1, 0, 'beta')
219 |
220 | >>> Version('1.0.1') > Version('0.0.1')
221 | True
222 | """
223 |
224 | #: Match version and pre-release/build information in version strings
225 | match_version = re.compile(r'([0-9][0-9\.]*)(.+)?').match
226 |
227 | def __init__(self, vstr):
228 | """Create new `Version` object.
229 |
230 | Args:
231 | vstr (basestring): Semantic version string.
232 | """
233 | if not vstr:
234 | raise ValueError('invalid version number: {!r}'.format(vstr))
235 |
236 | self.vstr = vstr
237 | self.major = 0
238 | self.minor = 0
239 | self.patch = 0
240 | self.suffix = ''
241 | self.build = ''
242 | self._parse(vstr)
243 |
244 | def _parse(self, vstr):
245 | if vstr.startswith('v'):
246 | m = self.match_version(vstr[1:])
247 | else:
248 | m = self.match_version(vstr)
249 | if not m:
250 | raise ValueError('invalid version number: ' + vstr)
251 |
252 | version, suffix = m.groups()
253 | parts = self._parse_dotted_string(version)
254 | self.major = parts.pop(0)
255 | if len(parts):
256 | self.minor = parts.pop(0)
257 | if len(parts):
258 | self.patch = parts.pop(0)
259 | if not len(parts) == 0:
260 | raise ValueError('version number too long: ' + vstr)
261 |
262 | if suffix:
263 | # Build info
264 | idx = suffix.find('+')
265 | if idx > -1:
266 | self.build = suffix[idx+1:]
267 | suffix = suffix[:idx]
268 | if suffix:
269 | if not suffix.startswith('-'):
270 | raise ValueError(
271 | 'suffix must start with - : ' + suffix)
272 | self.suffix = suffix[1:]
273 |
274 | def _parse_dotted_string(self, s):
275 | """Parse string ``s`` into list of ints and strings."""
276 | parsed = []
277 | parts = s.split('.')
278 | for p in parts:
279 | if p.isdigit():
280 | p = int(p)
281 | parsed.append(p)
282 | return parsed
283 |
284 | @property
285 | def tuple(self):
286 | """Version number as a tuple of major, minor, patch, pre-release."""
287 | return (self.major, self.minor, self.patch, self.suffix)
288 |
289 | def __lt__(self, other):
290 | """Implement comparison."""
291 | if not isinstance(other, Version):
292 | raise ValueError('not a Version instance: {0!r}'.format(other))
293 | t = self.tuple[:3]
294 | o = other.tuple[:3]
295 | if t < o:
296 | return True
297 | if t == o: # We need to compare suffixes
298 | if self.suffix and not other.suffix:
299 | return True
300 | if other.suffix and not self.suffix:
301 | return False
302 | return self._parse_dotted_string(self.suffix) \
303 | < self._parse_dotted_string(other.suffix)
304 | # t > o
305 | return False
306 |
307 | def __eq__(self, other):
308 | """Implement comparison."""
309 | if not isinstance(other, Version):
310 | raise ValueError('not a Version instance: {0!r}'.format(other))
311 | return self.tuple == other.tuple
312 |
313 | def __ne__(self, other):
314 | """Implement comparison."""
315 | return not self.__eq__(other)
316 |
317 | def __gt__(self, other):
318 | """Implement comparison."""
319 | if not isinstance(other, Version):
320 | raise ValueError('not a Version instance: {0!r}'.format(other))
321 | return other.__lt__(self)
322 |
323 | def __le__(self, other):
324 | """Implement comparison."""
325 | if not isinstance(other, Version):
326 | raise ValueError('not a Version instance: {0!r}'.format(other))
327 | return not other.__lt__(self)
328 |
329 | def __ge__(self, other):
330 | """Implement comparison."""
331 | return not self.__lt__(other)
332 |
333 | def __str__(self):
334 | """Return semantic version string."""
335 | vstr = '{0}.{1}.{2}'.format(self.major, self.minor, self.patch)
336 | if self.suffix:
337 | vstr = '{0}-{1}'.format(vstr, self.suffix)
338 | if self.build:
339 | vstr = '{0}+{1}'.format(vstr, self.build)
340 | return vstr
341 |
342 | def __repr__(self):
343 | """Return 'code' representation of `Version`."""
344 | return "Version('{0}')".format(str(self))
345 |
346 |
347 | def retrieve_download(dl):
348 | """Saves a download to a temporary file and returns path.
349 |
350 | .. versionadded: 1.37
351 |
352 | Args:
353 | url (unicode): URL to .alfredworkflow file in GitHub repo
354 |
355 | Returns:
356 | unicode: path to downloaded file
357 |
358 | """
359 | if not match_workflow(dl.filename):
360 | raise ValueError('attachment not a workflow: ' + dl.filename)
361 |
362 | path = os.path.join(tempfile.gettempdir(), dl.filename)
363 | wf().logger.debug('downloading update from '
364 | '%r to %r ...', dl.url, path)
365 |
366 | r = web.get(dl.url)
367 | r.raise_for_status()
368 |
369 | r.save_to_path(path)
370 |
371 | return path
372 |
373 |
374 | def build_api_url(repo):
375 | """Generate releases URL from GitHub repo.
376 |
377 | Args:
378 | repo (unicode): Repo name in form ``username/repo``
379 |
380 | Returns:
381 | unicode: URL to the API endpoint for the repo's releases
382 |
383 | """
384 | if len(repo.split('/')) != 2:
385 | raise ValueError('invalid GitHub repo: {!r}'.format(repo))
386 |
387 | return RELEASES_BASE.format(repo)
388 |
389 |
390 | def get_downloads(repo):
391 | """Load available ``Download``s for GitHub repo.
392 |
393 | .. versionadded: 1.37
394 |
395 | Args:
396 | repo (unicode): GitHub repo to load releases for.
397 |
398 | Returns:
399 | list: Sequence of `Download` contained in GitHub releases.
400 | """
401 | url = build_api_url(repo)
402 |
403 | def _fetch():
404 | wf().logger.info('retrieving releases for %r ...', repo)
405 | r = web.get(url)
406 | r.raise_for_status()
407 | return r.content
408 |
409 | key = 'github-releases-' + repo.replace('/', '-')
410 | js = wf().cached_data(key, _fetch, max_age=60)
411 |
412 | return Download.from_releases(js)
413 |
414 |
415 | def latest_download(dls, alfred_version=None, prereleases=False):
416 | """Return newest `Download`."""
417 | alfred_version = alfred_version or os.getenv('alfred_version')
418 | version = None
419 | if alfred_version:
420 | version = Version(alfred_version)
421 |
422 | dls.sort(reverse=True)
423 | for dl in dls:
424 | if dl.prerelease and not prereleases:
425 | wf().logger.debug('ignored prerelease: %s', dl.version)
426 | continue
427 | if version and dl.alfred_version > version:
428 | wf().logger.debug('ignored incompatible (%s > %s): %s',
429 | dl.alfred_version, version, dl.filename)
430 | continue
431 |
432 | wf().logger.debug('latest version: %s (%s)', dl.version, dl.filename)
433 | return dl
434 |
435 | return None
436 |
437 |
438 | def check_update(repo, current_version, prereleases=False,
439 | alfred_version=None):
440 | """Check whether a newer release is available on GitHub.
441 |
442 | Args:
443 | repo (unicode): ``username/repo`` for workflow's GitHub repo
444 | current_version (unicode): the currently installed version of the
445 | workflow. :ref:`Semantic versioning ` is required.
446 | prereleases (bool): Whether to include pre-releases.
447 | alfred_version (unicode): version of currently-running Alfred.
448 | if empty, defaults to ``$alfred_version`` environment variable.
449 |
450 | Returns:
451 | bool: ``True`` if an update is available, else ``False``
452 |
453 | If an update is available, its version number and download URL will
454 | be cached.
455 |
456 | """
457 | key = '__workflow_latest_version'
458 | # data stored when no update is available
459 | no_update = {
460 | 'available': False,
461 | 'download': None,
462 | 'version': None,
463 | }
464 | current = Version(current_version)
465 |
466 | dls = get_downloads(repo)
467 | if not len(dls):
468 | wf().logger.warning('no valid downloads for %s', repo)
469 | wf().cache_data(key, no_update)
470 | return False
471 |
472 | wf().logger.info('%d download(s) for %s', len(dls), repo)
473 |
474 | dl = latest_download(dls, alfred_version, prereleases)
475 |
476 | if not dl:
477 | wf().logger.warning('no compatible downloads for %s', repo)
478 | wf().cache_data(key, no_update)
479 | return False
480 |
481 | wf().logger.debug('latest=%r, installed=%r', dl.version, current)
482 |
483 | if dl.version > current:
484 | wf().cache_data(key, {
485 | 'version': str(dl.version),
486 | 'download': dl.dict,
487 | 'available': True,
488 | })
489 | return True
490 |
491 | wf().cache_data(key, no_update)
492 | return False
493 |
494 |
495 | def install_update():
496 | """If a newer release is available, download and install it.
497 |
498 | :returns: ``True`` if an update is installed, else ``False``
499 |
500 | """
501 | key = '__workflow_latest_version'
502 | # data stored when no update is available
503 | no_update = {
504 | 'available': False,
505 | 'download': None,
506 | 'version': None,
507 | }
508 | status = wf().cached_data(key, max_age=0)
509 |
510 | if not status or not status.get('available'):
511 | wf().logger.info('no update available')
512 | return False
513 |
514 | dl = status.get('download')
515 | if not dl:
516 | wf().logger.info('no download information')
517 | return False
518 |
519 | path = retrieve_download(Download.from_dict(dl))
520 |
521 | wf().logger.info('installing updated workflow ...')
522 | subprocess.call(['open', path]) # nosec
523 |
524 | wf().cache_data(key, no_update)
525 | return True
526 |
527 |
528 | if __name__ == '__main__': # pragma: nocover
529 | import sys
530 |
531 | prereleases = False
532 |
533 | def show_help(status=0):
534 | """Print help message."""
535 | print('usage: update.py (check|install) '
536 | '[--prereleases] ')
537 | sys.exit(status)
538 |
539 | argv = sys.argv[:]
540 | if '-h' in argv or '--help' in argv:
541 | show_help()
542 |
543 | if '--prereleases' in argv:
544 | argv.remove('--prereleases')
545 | prereleases = True
546 |
547 | if len(argv) != 4:
548 | show_help(1)
549 |
550 | action = argv[1]
551 | repo = argv[2]
552 | version = argv[3]
553 |
554 | try:
555 |
556 | if action == 'check':
557 | check_update(repo, version, prereleases)
558 | elif action == 'install':
559 | install_update()
560 | else:
561 | show_help(1)
562 |
563 | except Exception as err: # ensure traceback is in log file
564 | wf().logger.exception(err)
565 | raise err
566 |
--------------------------------------------------------------------------------
/src/workflow/util.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | #
4 | # Copyright (c) 2017 Dean Jackson
5 | #
6 | # MIT Licence. See http://opensource.org/licenses/MIT
7 | #
8 | # Created on 2017-12-17
9 | #
10 |
11 | """A selection of helper functions useful for building workflows."""
12 |
13 | from __future__ import print_function, absolute_import
14 |
15 | import atexit
16 | from collections import namedtuple
17 | from contextlib import contextmanager
18 | import errno
19 | import fcntl
20 | import functools
21 | import json
22 | import os
23 | import signal
24 | import subprocess
25 | import sys
26 | from threading import Event
27 | import time
28 |
29 | # JXA scripts to call Alfred's API via the Scripting Bridge
30 | # {app} is automatically replaced with "Alfred 3" or
31 | # "com.runningwithcrayons.Alfred" depending on version.
32 | #
33 | # Open Alfred in search (regular) mode
34 | JXA_SEARCH = 'Application({app}).search({arg});'
35 | # Open Alfred's File Actions on an argument
36 | JXA_ACTION = 'Application({app}).action({arg});'
37 | # Open Alfred's navigation mode at path
38 | JXA_BROWSE = 'Application({app}).browse({arg});'
39 | # Set the specified theme
40 | JXA_SET_THEME = 'Application({app}).setTheme({arg});'
41 | # Call an External Trigger
42 | JXA_TRIGGER = 'Application({app}).runTrigger({arg}, {opts});'
43 | # Save a variable to the workflow configuration sheet/info.plist
44 | JXA_SET_CONFIG = 'Application({app}).setConfiguration({arg}, {opts});'
45 | # Delete a variable from the workflow configuration sheet/info.plist
46 | JXA_UNSET_CONFIG = 'Application({app}).removeConfiguration({arg}, {opts});'
47 | # Tell Alfred to reload a workflow from disk
48 | JXA_RELOAD_WORKFLOW = 'Application({app}).reloadWorkflow({arg});'
49 |
50 |
51 | class AcquisitionError(Exception):
52 | """Raised if a lock cannot be acquired."""
53 |
54 |
55 | AppInfo = namedtuple('AppInfo', ['name', 'path', 'bundleid'])
56 | """Information about an installed application.
57 |
58 | Returned by :func:`appinfo`. All attributes are Unicode.
59 |
60 | .. py:attribute:: name
61 |
62 | Name of the application, e.g. ``u'Safari'``.
63 |
64 | .. py:attribute:: path
65 |
66 | Path to the application bundle, e.g. ``u'/Applications/Safari.app'``.
67 |
68 | .. py:attribute:: bundleid
69 |
70 | Application's bundle ID, e.g. ``u'com.apple.Safari'``.
71 |
72 | """
73 |
74 |
75 | def jxa_app_name():
76 | """Return name of application to call currently running Alfred.
77 |
78 | .. versionadded: 1.37
79 |
80 | Returns 'Alfred 3' or 'com.runningwithcrayons.Alfred' depending
81 | on which version of Alfred is running.
82 |
83 | This name is suitable for use with ``Application(name)`` in JXA.
84 |
85 | Returns:
86 | unicode: Application name or ID.
87 |
88 | """
89 | if os.getenv('alfred_version', '').startswith('3'):
90 | # Alfred 3
91 | return u'Alfred 3'
92 | # Alfred 4+
93 | return u'com.runningwithcrayons.Alfred'
94 |
95 |
96 | def unicodify(s, encoding='utf-8', norm=None):
97 | """Ensure string is Unicode.
98 |
99 | .. versionadded:: 1.31
100 |
101 | Decode encoded strings using ``encoding`` and normalise Unicode
102 | to form ``norm`` if specified.
103 |
104 | Args:
105 | s (str): String to decode. May also be Unicode.
106 | encoding (str, optional): Encoding to use on bytestrings.
107 | norm (None, optional): Normalisation form to apply to Unicode string.
108 |
109 | Returns:
110 | unicode: Decoded, optionally normalised, Unicode string.
111 |
112 | """
113 | if not isinstance(s, unicode):
114 | s = unicode(s, encoding)
115 |
116 | if norm:
117 | from unicodedata import normalize
118 | s = normalize(norm, s)
119 |
120 | return s
121 |
122 |
123 | def utf8ify(s):
124 | """Ensure string is a bytestring.
125 |
126 | .. versionadded:: 1.31
127 |
128 | Returns `str` objects unchanced, encodes `unicode` objects to
129 | UTF-8, and calls :func:`str` on anything else.
130 |
131 | Args:
132 | s (object): A Python object
133 |
134 | Returns:
135 | str: UTF-8 string or string representation of s.
136 |
137 | """
138 | if isinstance(s, str):
139 | return s
140 |
141 | if isinstance(s, unicode):
142 | return s.encode('utf-8')
143 |
144 | return str(s)
145 |
146 |
147 | def applescriptify(s):
148 | """Escape string for insertion into an AppleScript string.
149 |
150 | .. versionadded:: 1.31
151 |
152 | Replaces ``"`` with `"& quote &"`. Use this function if you want
153 | to insert a string into an AppleScript script:
154 |
155 | >>> applescriptify('g "python" test')
156 | 'g " & quote & "python" & quote & "test'
157 |
158 | Args:
159 | s (unicode): Unicode string to escape.
160 |
161 | Returns:
162 | unicode: Escaped string.
163 |
164 | """
165 | return s.replace(u'"', u'" & quote & "')
166 |
167 |
168 | def run_command(cmd, **kwargs):
169 | """Run a command and return the output.
170 |
171 | .. versionadded:: 1.31
172 |
173 | A thin wrapper around :func:`subprocess.check_output` that ensures
174 | all arguments are encoded to UTF-8 first.
175 |
176 | Args:
177 | cmd (list): Command arguments to pass to :func:`~subprocess.check_output`.
178 | **kwargs: Keyword arguments to pass to :func:`~subprocess.check_output`.
179 |
180 | Returns:
181 | str: Output returned by :func:`~subprocess.check_output`.
182 |
183 | """
184 | cmd = [utf8ify(s) for s in cmd]
185 | return subprocess.check_output(cmd, **kwargs)
186 |
187 |
188 | def run_applescript(script, *args, **kwargs):
189 | """Execute an AppleScript script and return its output.
190 |
191 | .. versionadded:: 1.31
192 |
193 | Run AppleScript either by filepath or code. If ``script`` is a valid
194 | filepath, that script will be run, otherwise ``script`` is treated
195 | as code.
196 |
197 | Args:
198 | script (str, optional): Filepath of script or code to run.
199 | *args: Optional command-line arguments to pass to the script.
200 | **kwargs: Pass ``lang`` to run a language other than AppleScript.
201 | Any other keyword arguments are passed to :func:`run_command`.
202 |
203 | Returns:
204 | str: Output of run command.
205 |
206 | """
207 | lang = 'AppleScript'
208 | if 'lang' in kwargs:
209 | lang = kwargs['lang']
210 | del kwargs['lang']
211 |
212 | cmd = ['/usr/bin/osascript', '-l', lang]
213 |
214 | if os.path.exists(script):
215 | cmd += [script]
216 | else:
217 | cmd += ['-e', script]
218 |
219 | cmd.extend(args)
220 |
221 | return run_command(cmd, **kwargs)
222 |
223 |
224 | def run_jxa(script, *args):
225 | """Execute a JXA script and return its output.
226 |
227 | .. versionadded:: 1.31
228 |
229 | Wrapper around :func:`run_applescript` that passes ``lang=JavaScript``.
230 |
231 | Args:
232 | script (str): Filepath of script or code to run.
233 | *args: Optional command-line arguments to pass to script.
234 |
235 | Returns:
236 | str: Output of script.
237 |
238 | """
239 | return run_applescript(script, *args, lang='JavaScript')
240 |
241 |
242 | def run_trigger(name, bundleid=None, arg=None):
243 | """Call an Alfred External Trigger.
244 |
245 | .. versionadded:: 1.31
246 |
247 | If ``bundleid`` is not specified, the bundle ID of the calling
248 | workflow is used.
249 |
250 | Args:
251 | name (str): Name of External Trigger to call.
252 | bundleid (str, optional): Bundle ID of workflow trigger belongs to.
253 | arg (str, optional): Argument to pass to trigger.
254 |
255 | """
256 | bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
257 | appname = jxa_app_name()
258 | opts = {'inWorkflow': bundleid}
259 | if arg:
260 | opts['withArgument'] = arg
261 |
262 | script = JXA_TRIGGER.format(app=json.dumps(appname),
263 | arg=json.dumps(name),
264 | opts=json.dumps(opts, sort_keys=True))
265 |
266 | run_applescript(script, lang='JavaScript')
267 |
268 |
269 | def set_theme(theme_name):
270 | """Change Alfred's theme.
271 |
272 | .. versionadded:: 1.39.0
273 |
274 | Args:
275 | theme_name (unicode): Name of theme Alfred should use.
276 |
277 | """
278 | appname = jxa_app_name()
279 | script = JXA_SET_THEME.format(app=json.dumps(appname),
280 | arg=json.dumps(theme_name))
281 | run_applescript(script, lang='JavaScript')
282 |
283 |
284 | def set_config(name, value, bundleid=None, exportable=False):
285 | """Set a workflow variable in ``info.plist``.
286 |
287 | .. versionadded:: 1.33
288 |
289 | If ``bundleid`` is not specified, the bundle ID of the calling
290 | workflow is used.
291 |
292 | Args:
293 | name (str): Name of variable to set.
294 | value (str): Value to set variable to.
295 | bundleid (str, optional): Bundle ID of workflow variable belongs to.
296 | exportable (bool, optional): Whether variable should be marked
297 | as exportable (Don't Export checkbox).
298 |
299 | """
300 | bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
301 | appname = jxa_app_name()
302 | opts = {
303 | 'toValue': value,
304 | 'inWorkflow': bundleid,
305 | 'exportable': exportable,
306 | }
307 |
308 | script = JXA_SET_CONFIG.format(app=json.dumps(appname),
309 | arg=json.dumps(name),
310 | opts=json.dumps(opts, sort_keys=True))
311 |
312 | run_applescript(script, lang='JavaScript')
313 |
314 |
315 | def unset_config(name, bundleid=None):
316 | """Delete a workflow variable from ``info.plist``.
317 |
318 | .. versionadded:: 1.33
319 |
320 | If ``bundleid`` is not specified, the bundle ID of the calling
321 | workflow is used.
322 |
323 | Args:
324 | name (str): Name of variable to delete.
325 | bundleid (str, optional): Bundle ID of workflow variable belongs to.
326 |
327 | """
328 | bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
329 | appname = jxa_app_name()
330 | opts = {'inWorkflow': bundleid}
331 |
332 | script = JXA_UNSET_CONFIG.format(app=json.dumps(appname),
333 | arg=json.dumps(name),
334 | opts=json.dumps(opts, sort_keys=True))
335 |
336 | run_applescript(script, lang='JavaScript')
337 |
338 |
339 | def search_in_alfred(query=None):
340 | """Open Alfred with given search query.
341 |
342 | .. versionadded:: 1.39.0
343 |
344 | Omit ``query`` to simply open Alfred's main window.
345 |
346 | Args:
347 | query (unicode, optional): Search query.
348 |
349 | """
350 | query = query or u''
351 | appname = jxa_app_name()
352 | script = JXA_SEARCH.format(app=json.dumps(appname), arg=json.dumps(query))
353 | run_applescript(script, lang='JavaScript')
354 |
355 |
356 | def browse_in_alfred(path):
357 | """Open Alfred's filesystem navigation mode at ``path``.
358 |
359 | .. versionadded:: 1.39.0
360 |
361 | Args:
362 | path (unicode): File or directory path.
363 |
364 | """
365 | appname = jxa_app_name()
366 | script = JXA_BROWSE.format(app=json.dumps(appname), arg=json.dumps(path))
367 | run_applescript(script, lang='JavaScript')
368 |
369 |
370 | def action_in_alfred(paths):
371 | """Action the give filepaths in Alfred.
372 |
373 | .. versionadded:: 1.39.0
374 |
375 | Args:
376 | paths (list): Unicode paths to files/directories to action.
377 |
378 | """
379 | appname = jxa_app_name()
380 | script = JXA_ACTION.format(app=json.dumps(appname), arg=json.dumps(paths))
381 | run_applescript(script, lang='JavaScript')
382 |
383 |
384 | def reload_workflow(bundleid=None):
385 | """Tell Alfred to reload a workflow from disk.
386 |
387 | .. versionadded:: 1.39.0
388 |
389 | If ``bundleid`` is not specified, the bundle ID of the calling
390 | workflow is used.
391 |
392 | Args:
393 | bundleid (unicode, optional): Bundle ID of workflow to reload.
394 |
395 | """
396 | bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
397 | appname = jxa_app_name()
398 | script = JXA_RELOAD_WORKFLOW.format(app=json.dumps(appname),
399 | arg=json.dumps(bundleid))
400 |
401 | run_applescript(script, lang='JavaScript')
402 |
403 |
404 | def appinfo(name):
405 | """Get information about an installed application.
406 |
407 | .. versionadded:: 1.31
408 |
409 | Args:
410 | name (str): Name of application to look up.
411 |
412 | Returns:
413 | AppInfo: :class:`AppInfo` tuple or ``None`` if app isn't found.
414 |
415 | """
416 | cmd = [
417 | 'mdfind',
418 | '-onlyin', '/Applications',
419 | '-onlyin', '/System/Applications',
420 | '-onlyin', os.path.expanduser('~/Applications'),
421 | '(kMDItemContentTypeTree == com.apple.application &&'
422 | '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))'
423 | .format(name)
424 | ]
425 |
426 | output = run_command(cmd).strip()
427 | if not output:
428 | return None
429 |
430 | path = output.split('\n')[0]
431 |
432 | cmd = ['mdls', '-raw', '-name', 'kMDItemCFBundleIdentifier', path]
433 | bid = run_command(cmd).strip()
434 | if not bid: # pragma: no cover
435 | return None
436 |
437 | return AppInfo(unicodify(name), unicodify(path), unicodify(bid))
438 |
439 |
440 | @contextmanager
441 | def atomic_writer(fpath, mode):
442 | """Atomic file writer.
443 |
444 | .. versionadded:: 1.12
445 |
446 | Context manager that ensures the file is only written if the write
447 | succeeds. The data is first written to a temporary file.
448 |
449 | :param fpath: path of file to write to.
450 | :type fpath: ``unicode``
451 | :param mode: sames as for :func:`open`
452 | :type mode: string
453 |
454 | """
455 | suffix = '.{}.tmp'.format(os.getpid())
456 | temppath = fpath + suffix
457 | with open(temppath, mode) as fp:
458 | try:
459 | yield fp
460 | os.rename(temppath, fpath)
461 | finally:
462 | try:
463 | os.remove(temppath)
464 | except (OSError, IOError):
465 | pass
466 |
467 |
468 | class LockFile(object):
469 | """Context manager to protect filepaths with lockfiles.
470 |
471 | .. versionadded:: 1.13
472 |
473 | Creates a lockfile alongside ``protected_path``. Other ``LockFile``
474 | instances will refuse to lock the same path.
475 |
476 | >>> path = '/path/to/file'
477 | >>> with LockFile(path):
478 | >>> with open(path, 'wb') as fp:
479 | >>> fp.write(data)
480 |
481 | Args:
482 | protected_path (unicode): File to protect with a lockfile
483 | timeout (float, optional): Raises an :class:`AcquisitionError`
484 | if lock cannot be acquired within this number of seconds.
485 | If ``timeout`` is 0 (the default), wait forever.
486 | delay (float, optional): How often to check (in seconds) if
487 | lock has been released.
488 |
489 | Attributes:
490 | delay (float): How often to check (in seconds) whether the lock
491 | can be acquired.
492 | lockfile (unicode): Path of the lockfile.
493 | timeout (float): How long to wait to acquire the lock.
494 |
495 | """
496 |
497 | def __init__(self, protected_path, timeout=0.0, delay=0.05):
498 | """Create new :class:`LockFile` object."""
499 | self.lockfile = protected_path + '.lock'
500 | self._lockfile = None
501 | self.timeout = timeout
502 | self.delay = delay
503 | self._lock = Event()
504 | atexit.register(self.release)
505 |
506 | @property
507 | def locked(self):
508 | """``True`` if file is locked by this instance."""
509 | return self._lock.is_set()
510 |
511 | def acquire(self, blocking=True):
512 | """Acquire the lock if possible.
513 |
514 | If the lock is in use and ``blocking`` is ``False``, return
515 | ``False``.
516 |
517 | Otherwise, check every :attr:`delay` seconds until it acquires
518 | lock or exceeds attr:`timeout` and raises an :class:`AcquisitionError`.
519 |
520 | """
521 | if self.locked and not blocking:
522 | return False
523 |
524 | start = time.time()
525 | while True:
526 | # Raise error if we've been waiting too long to acquire the lock
527 | if self.timeout and (time.time() - start) >= self.timeout:
528 | raise AcquisitionError('lock acquisition timed out')
529 |
530 | # If already locked, wait then try again
531 | if self.locked:
532 | time.sleep(self.delay)
533 | continue
534 |
535 | # Create in append mode so we don't lose any contents
536 | if self._lockfile is None:
537 | self._lockfile = open(self.lockfile, 'a')
538 |
539 | # Try to acquire the lock
540 | try:
541 | fcntl.lockf(self._lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
542 | self._lock.set()
543 | break
544 | except IOError as err: # pragma: no cover
545 | if err.errno not in (errno.EACCES, errno.EAGAIN):
546 | raise
547 |
548 | # Don't try again
549 | if not blocking: # pragma: no cover
550 | return False
551 |
552 | # Wait, then try again
553 | time.sleep(self.delay)
554 |
555 | return True
556 |
557 | def release(self):
558 | """Release the lock by deleting `self.lockfile`."""
559 | if not self._lock.is_set():
560 | return False
561 |
562 | try:
563 | fcntl.lockf(self._lockfile, fcntl.LOCK_UN)
564 | except IOError: # pragma: no cover
565 | pass
566 | finally:
567 | self._lock.clear()
568 | self._lockfile = None
569 | try:
570 | os.unlink(self.lockfile)
571 | except (IOError, OSError): # pragma: no cover
572 | pass
573 |
574 | return True
575 |
576 | def __enter__(self):
577 | """Acquire lock."""
578 | self.acquire()
579 | return self
580 |
581 | def __exit__(self, typ, value, traceback):
582 | """Release lock."""
583 | self.release()
584 |
585 | def __del__(self):
586 | """Clear up `self.lockfile`."""
587 | self.release() # pragma: no cover
588 |
589 |
590 | class uninterruptible(object):
591 | """Decorator that postpones SIGTERM until wrapped function returns.
592 |
593 | .. versionadded:: 1.12
594 |
595 | .. important:: This decorator is NOT thread-safe.
596 |
597 | As of version 2.7, Alfred allows Script Filters to be killed. If
598 | your workflow is killed in the middle of critical code (e.g.
599 | writing data to disk), this may corrupt your workflow's data.
600 |
601 | Use this decorator to wrap critical functions that *must* complete.
602 | If the script is killed while a wrapped function is executing,
603 | the SIGTERM will be caught and handled after your function has
604 | finished executing.
605 |
606 | Alfred-Workflow uses this internally to ensure its settings, data
607 | and cache writes complete.
608 |
609 | """
610 |
611 | def __init__(self, func, class_name=''):
612 | """Decorate `func`."""
613 | self.func = func
614 | functools.update_wrapper(self, func)
615 | self._caught_signal = None
616 |
617 | def signal_handler(self, signum, frame):
618 | """Called when process receives SIGTERM."""
619 | self._caught_signal = (signum, frame)
620 |
621 | def __call__(self, *args, **kwargs):
622 | """Trap ``SIGTERM`` and call wrapped function."""
623 | self._caught_signal = None
624 | # Register handler for SIGTERM, then call `self.func`
625 | self.old_signal_handler = signal.getsignal(signal.SIGTERM)
626 | signal.signal(signal.SIGTERM, self.signal_handler)
627 |
628 | self.func(*args, **kwargs)
629 |
630 | # Restore old signal handler
631 | signal.signal(signal.SIGTERM, self.old_signal_handler)
632 |
633 | # Handle any signal caught during execution
634 | if self._caught_signal is not None:
635 | signum, frame = self._caught_signal
636 | if callable(self.old_signal_handler):
637 | self.old_signal_handler(signum, frame)
638 | elif self.old_signal_handler == signal.SIG_DFL:
639 | sys.exit(0)
640 |
641 | def __get__(self, obj=None, klass=None):
642 | """Decorator API."""
643 | return self.__class__(self.func.__get__(obj, klass),
644 | klass.__name__)
645 |
--------------------------------------------------------------------------------
/src/workflow/version:
--------------------------------------------------------------------------------
1 | 1.40.0
--------------------------------------------------------------------------------
/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 | from __future__ import absolute_import, print_function
13 |
14 | import codecs
15 | import json
16 | import mimetypes
17 | import os
18 | import random
19 | import re
20 | import socket
21 | import string
22 | import unicodedata
23 | import urllib
24 | import urllib2
25 | import urlparse
26 | import zlib
27 |
28 | __version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
29 |
30 | USER_AGENT = (u'Alfred-Workflow/' + __version__ +
31 | ' (+http://www.deanishe.net/alfred-workflow)')
32 |
33 | # Valid characters for multipart form data boundaries
34 | BOUNDARY_CHARS = string.digits + string.ascii_letters
35 |
36 | # HTTP response codes
37 | RESPONSES = {
38 | 100: 'Continue',
39 | 101: 'Switching Protocols',
40 | 200: 'OK',
41 | 201: 'Created',
42 | 202: 'Accepted',
43 | 203: 'Non-Authoritative Information',
44 | 204: 'No Content',
45 | 205: 'Reset Content',
46 | 206: 'Partial Content',
47 | 300: 'Multiple Choices',
48 | 301: 'Moved Permanently',
49 | 302: 'Found',
50 | 303: 'See Other',
51 | 304: 'Not Modified',
52 | 305: 'Use Proxy',
53 | 307: 'Temporary Redirect',
54 | 400: 'Bad Request',
55 | 401: 'Unauthorized',
56 | 402: 'Payment Required',
57 | 403: 'Forbidden',
58 | 404: 'Not Found',
59 | 405: 'Method Not Allowed',
60 | 406: 'Not Acceptable',
61 | 407: 'Proxy Authentication Required',
62 | 408: 'Request Timeout',
63 | 409: 'Conflict',
64 | 410: 'Gone',
65 | 411: 'Length Required',
66 | 412: 'Precondition Failed',
67 | 413: 'Request Entity Too Large',
68 | 414: 'Request-URI Too Long',
69 | 415: 'Unsupported Media Type',
70 | 416: 'Requested Range Not Satisfiable',
71 | 417: 'Expectation Failed',
72 | 500: 'Internal Server Error',
73 | 501: 'Not Implemented',
74 | 502: 'Bad Gateway',
75 | 503: 'Service Unavailable',
76 | 504: 'Gateway Timeout',
77 | 505: 'HTTP Version Not Supported'
78 | }
79 |
80 |
81 | def str_dict(dic):
82 | """Convert keys and values in ``dic`` into UTF-8-encoded :class:`str`.
83 |
84 | :param dic: Mapping of Unicode strings
85 | :type dic: dict
86 | :returns: Dictionary containing only UTF-8 strings
87 | :rtype: dict
88 |
89 | """
90 | if isinstance(dic, CaseInsensitiveDictionary):
91 | dic2 = CaseInsensitiveDictionary()
92 | else:
93 | dic2 = {}
94 | for k, v in dic.items():
95 | if isinstance(k, unicode):
96 | k = k.encode('utf-8')
97 | if isinstance(v, unicode):
98 | v = v.encode('utf-8')
99 | dic2[k] = v
100 | return dic2
101 |
102 |
103 | class NoRedirectHandler(urllib2.HTTPRedirectHandler):
104 | """Prevent redirections."""
105 |
106 | def redirect_request(self, *args):
107 | """Ignore redirect."""
108 | return None
109 |
110 |
111 | # Adapted from https://gist.github.com/babakness/3901174
112 | class CaseInsensitiveDictionary(dict):
113 | """Dictionary with caseless key search.
114 |
115 | Enables case insensitive searching while preserving case sensitivity
116 | when keys are listed, ie, via keys() or items() methods.
117 |
118 | Works by storing a lowercase version of the key as the new key and
119 | stores the original key-value pair as the key's value
120 | (values become dictionaries).
121 |
122 | """
123 |
124 | def __init__(self, initval=None):
125 | """Create new case-insensitive dictionary."""
126 | if isinstance(initval, dict):
127 | for key, value in initval.iteritems():
128 | self.__setitem__(key, value)
129 |
130 | elif isinstance(initval, list):
131 | for (key, value) in initval:
132 | self.__setitem__(key, value)
133 |
134 | def __contains__(self, key):
135 | return dict.__contains__(self, key.lower())
136 |
137 | def __getitem__(self, key):
138 | return dict.__getitem__(self, key.lower())['val']
139 |
140 | def __setitem__(self, key, value):
141 | return dict.__setitem__(self, key.lower(), {'key': key, 'val': value})
142 |
143 | def get(self, key, default=None):
144 | """Return value for case-insensitive key or default."""
145 | try:
146 | v = dict.__getitem__(self, key.lower())
147 | except KeyError:
148 | return default
149 | else:
150 | return v['val']
151 |
152 | def update(self, other):
153 | """Update values from other ``dict``."""
154 | for k, v in other.items():
155 | self[k] = v
156 |
157 | def items(self):
158 | """Return ``(key, value)`` pairs."""
159 | return [(v['key'], v['val']) for v in dict.itervalues(self)]
160 |
161 | def keys(self):
162 | """Return original keys."""
163 | return [v['key'] for v in dict.itervalues(self)]
164 |
165 | def values(self):
166 | """Return all values."""
167 | return [v['val'] for v in dict.itervalues(self)]
168 |
169 | def iteritems(self):
170 | """Iterate over ``(key, value)`` pairs."""
171 | for v in dict.itervalues(self):
172 | yield v['key'], v['val']
173 |
174 | def iterkeys(self):
175 | """Iterate over original keys."""
176 | for v in dict.itervalues(self):
177 | yield v['key']
178 |
179 | def itervalues(self):
180 | """Interate over values."""
181 | for v in dict.itervalues(self):
182 | yield v['val']
183 |
184 |
185 | class Request(urllib2.Request):
186 | """Subclass of :class:`urllib2.Request` that supports custom methods."""
187 |
188 | def __init__(self, *args, **kwargs):
189 | """Create a new :class:`Request`."""
190 | self._method = kwargs.pop('method', None)
191 | urllib2.Request.__init__(self, *args, **kwargs)
192 |
193 | def get_method(self):
194 | return self._method.upper()
195 |
196 |
197 | class Response(object):
198 | """
199 | Returned by :func:`request` / :func:`get` / :func:`post` functions.
200 |
201 | Simplified version of the ``Response`` object in the ``requests`` library.
202 |
203 | >>> r = request('http://www.google.com')
204 | >>> r.status_code
205 | 200
206 | >>> r.encoding
207 | ISO-8859-1
208 | >>> r.content # bytes
209 | ...
210 | >>> r.text # unicode, decoded according to charset in HTTP header/meta tag
211 | u' ...'
212 | >>> r.json() # content parsed as JSON
213 |
214 | """
215 |
216 | def __init__(self, request, stream=False):
217 | """Call `request` with :mod:`urllib2` and process results.
218 |
219 | :param request: :class:`Request` instance
220 | :param stream: Whether to stream response or retrieve it all at once
221 | :type stream: bool
222 |
223 | """
224 | self.request = request
225 | self._stream = stream
226 | self.url = None
227 | self.raw = None
228 | self._encoding = None
229 | self.error = None
230 | self.status_code = None
231 | self.reason = None
232 | self.headers = CaseInsensitiveDictionary()
233 | self._content = None
234 | self._content_loaded = False
235 | self._gzipped = False
236 |
237 | # Execute query
238 | try:
239 | self.raw = urllib2.urlopen(request)
240 | except urllib2.HTTPError as err:
241 | self.error = err
242 | try:
243 | self.url = err.geturl()
244 | # sometimes (e.g. when authentication fails)
245 | # urllib can't get a URL from an HTTPError
246 | # This behaviour changes across Python versions,
247 | # so no test cover (it isn't important).
248 | except AttributeError: # pragma: no cover
249 | pass
250 | self.status_code = err.code
251 | else:
252 | self.status_code = self.raw.getcode()
253 | self.url = self.raw.geturl()
254 | self.reason = RESPONSES.get(self.status_code)
255 |
256 | # Parse additional info if request succeeded
257 | if not self.error:
258 | headers = self.raw.info()
259 | self.transfer_encoding = headers.getencoding()
260 | self.mimetype = headers.gettype()
261 | for key in headers.keys():
262 | self.headers[key.lower()] = headers.get(key)
263 |
264 | # Is content gzipped?
265 | # Transfer-Encoding appears to not be used in the wild
266 | # (contrary to the HTTP standard), but no harm in testing
267 | # for it
268 | if 'gzip' in headers.get('content-encoding', '') or \
269 | 'gzip' in headers.get('transfer-encoding', ''):
270 | self._gzipped = True
271 |
272 | @property
273 | def stream(self):
274 | """Whether response is streamed.
275 |
276 | Returns:
277 | bool: `True` if response is streamed.
278 |
279 | """
280 | return self._stream
281 |
282 | @stream.setter
283 | def stream(self, value):
284 | if self._content_loaded:
285 | raise RuntimeError("`content` has already been read from "
286 | "this Response.")
287 |
288 | self._stream = value
289 |
290 | def json(self):
291 | """Decode response contents as JSON.
292 |
293 | :returns: object decoded from JSON
294 | :rtype: list, dict or unicode
295 |
296 | """
297 | return json.loads(self.content, self.encoding or 'utf-8')
298 |
299 | @property
300 | def encoding(self):
301 | """Text encoding of document or ``None``.
302 |
303 | :returns: Text encoding if found.
304 | :rtype: str or ``None``
305 |
306 | """
307 | if not self._encoding:
308 | self._encoding = self._get_encoding()
309 |
310 | return self._encoding
311 |
312 | @property
313 | def content(self):
314 | """Raw content of response (i.e. bytes).
315 |
316 | :returns: Body of HTTP response
317 | :rtype: str
318 |
319 | """
320 | if not self._content:
321 |
322 | # Decompress gzipped content
323 | if self._gzipped:
324 | decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
325 | self._content = decoder.decompress(self.raw.read())
326 |
327 | else:
328 | self._content = self.raw.read()
329 |
330 | self._content_loaded = True
331 |
332 | return self._content
333 |
334 | @property
335 | def text(self):
336 | """Unicode-decoded content of response body.
337 |
338 | If no encoding can be determined from HTTP headers or the content
339 | itself, the encoded response body will be returned instead.
340 |
341 | :returns: Body of HTTP response
342 | :rtype: unicode or str
343 |
344 | """
345 | if self.encoding:
346 | return unicodedata.normalize('NFC', unicode(self.content,
347 | self.encoding))
348 | return self.content
349 |
350 | def iter_content(self, chunk_size=4096, decode_unicode=False):
351 | """Iterate over response data.
352 |
353 | .. versionadded:: 1.6
354 |
355 | :param chunk_size: Number of bytes to read into memory
356 | :type chunk_size: int
357 | :param decode_unicode: Decode to Unicode using detected encoding
358 | :type decode_unicode: bool
359 | :returns: iterator
360 |
361 | """
362 | if not self.stream:
363 | raise RuntimeError("You cannot call `iter_content` on a "
364 | "Response unless you passed `stream=True`"
365 | " to `get()`/`post()`/`request()`.")
366 |
367 | if self._content_loaded:
368 | raise RuntimeError(
369 | "`content` has already been read from this Response.")
370 |
371 | def decode_stream(iterator, r):
372 | dec = codecs.getincrementaldecoder(r.encoding)(errors='replace')
373 |
374 | for chunk in iterator:
375 | data = dec.decode(chunk)
376 | if data:
377 | yield data
378 |
379 | data = dec.decode(b'', final=True)
380 | if data: # pragma: no cover
381 | yield data
382 |
383 | def generate():
384 | if self._gzipped:
385 | decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
386 |
387 | while True:
388 | chunk = self.raw.read(chunk_size)
389 | if not chunk:
390 | break
391 |
392 | if self._gzipped:
393 | chunk = decoder.decompress(chunk)
394 |
395 | yield chunk
396 |
397 | chunks = generate()
398 |
399 | if decode_unicode and self.encoding:
400 | chunks = decode_stream(chunks, self)
401 |
402 | return chunks
403 |
404 | def save_to_path(self, filepath):
405 | """Save retrieved data to file at ``filepath``.
406 |
407 | .. versionadded: 1.9.6
408 |
409 | :param filepath: Path to save retrieved data.
410 |
411 | """
412 | filepath = os.path.abspath(filepath)
413 | dirname = os.path.dirname(filepath)
414 | if not os.path.exists(dirname):
415 | os.makedirs(dirname)
416 |
417 | self.stream = True
418 |
419 | with open(filepath, 'wb') as fileobj:
420 | for data in self.iter_content():
421 | fileobj.write(data)
422 |
423 | def raise_for_status(self):
424 | """Raise stored error if one occurred.
425 |
426 | error will be instance of :class:`urllib2.HTTPError`
427 | """
428 | if self.error is not None:
429 | raise self.error
430 | return
431 |
432 | def _get_encoding(self):
433 | """Get encoding from HTTP headers or content.
434 |
435 | :returns: encoding or `None`
436 | :rtype: unicode or ``None``
437 |
438 | """
439 | headers = self.raw.info()
440 | encoding = None
441 |
442 | if headers.getparam('charset'):
443 | encoding = headers.getparam('charset')
444 |
445 | # HTTP Content-Type header
446 | for param in headers.getplist():
447 | if param.startswith('charset='):
448 | encoding = param[8:]
449 | break
450 |
451 | if not self.stream: # Try sniffing response content
452 | # Encoding declared in document should override HTTP headers
453 | if self.mimetype == 'text/html': # sniff HTML headers
454 | m = re.search(r"""""",
455 | self.content)
456 | if m:
457 | encoding = m.group(1)
458 |
459 | elif ((self.mimetype.startswith('application/')
460 | or self.mimetype.startswith('text/'))
461 | and 'xml' in self.mimetype):
462 | m = re.search(r"""]*\?>""",
463 | self.content)
464 | if m:
465 | encoding = m.group(1)
466 |
467 | # Format defaults
468 | if self.mimetype == 'application/json' and not encoding:
469 | # The default encoding for JSON
470 | encoding = 'utf-8'
471 |
472 | elif self.mimetype == 'application/xml' and not encoding:
473 | # The default for 'application/xml'
474 | encoding = 'utf-8'
475 |
476 | if encoding:
477 | encoding = encoding.lower()
478 |
479 | return encoding
480 |
481 |
482 | def request(method, url, params=None, data=None, headers=None, cookies=None,
483 | files=None, auth=None, timeout=60, allow_redirects=False,
484 | stream=False):
485 | """Initiate an HTTP(S) request. Returns :class:`Response` object.
486 |
487 | :param method: 'GET' or 'POST'
488 | :type method: unicode
489 | :param url: URL to open
490 | :type url: unicode
491 | :param params: mapping of URL parameters
492 | :type params: dict
493 | :param data: mapping of form data ``{'field_name': 'value'}`` or
494 | :class:`str`
495 | :type data: dict or str
496 | :param headers: HTTP headers
497 | :type headers: dict
498 | :param cookies: cookies to send to server
499 | :type cookies: dict
500 | :param files: files to upload (see below).
501 | :type files: dict
502 | :param auth: username, password
503 | :type auth: tuple
504 | :param timeout: connection timeout limit in seconds
505 | :type timeout: int
506 | :param allow_redirects: follow redirections
507 | :type allow_redirects: bool
508 | :param stream: Stream content instead of fetching it all at once.
509 | :type stream: bool
510 | :returns: Response object
511 | :rtype: :class:`Response`
512 |
513 |
514 | The ``files`` argument is a dictionary::
515 |
516 | {'fieldname' : { 'filename': 'blah.txt',
517 | 'content': '',
518 | 'mimetype': 'text/plain'}
519 | }
520 |
521 | * ``fieldname`` is the name of the field in the HTML form.
522 | * ``mimetype`` is optional. If not provided, :mod:`mimetypes` will
523 | be used to guess the mimetype, or ``application/octet-stream``
524 | will be used.
525 |
526 | """
527 | # TODO: cookies
528 | socket.setdefaulttimeout(timeout)
529 |
530 | # Default handlers
531 | openers = [urllib2.ProxyHandler(urllib2.getproxies())]
532 |
533 | if not allow_redirects:
534 | openers.append(NoRedirectHandler())
535 |
536 | if auth is not None: # Add authorisation handler
537 | username, password = auth
538 | password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
539 | password_manager.add_password(None, url, username, password)
540 | auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
541 | openers.append(auth_manager)
542 |
543 | # Install our custom chain of openers
544 | opener = urllib2.build_opener(*openers)
545 | urllib2.install_opener(opener)
546 |
547 | if not headers:
548 | headers = CaseInsensitiveDictionary()
549 | else:
550 | headers = CaseInsensitiveDictionary(headers)
551 |
552 | if 'user-agent' not in headers:
553 | headers['user-agent'] = USER_AGENT
554 |
555 | # Accept gzip-encoded content
556 | encodings = [s.strip() for s in
557 | headers.get('accept-encoding', '').split(',')]
558 | if 'gzip' not in encodings:
559 | encodings.append('gzip')
560 |
561 | headers['accept-encoding'] = ', '.join(encodings)
562 |
563 | if files:
564 | if not data:
565 | data = {}
566 | new_headers, data = encode_multipart_formdata(data, files)
567 | headers.update(new_headers)
568 | elif data and isinstance(data, dict):
569 | data = urllib.urlencode(str_dict(data))
570 |
571 | # Make sure everything is encoded text
572 | headers = str_dict(headers)
573 |
574 | if isinstance(url, unicode):
575 | url = url.encode('utf-8')
576 |
577 | if params: # GET args (POST args are handled in encode_multipart_formdata)
578 |
579 | scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
580 |
581 | if query: # Combine query string and `params`
582 | url_params = urlparse.parse_qs(query)
583 | # `params` take precedence over URL query string
584 | url_params.update(params)
585 | params = url_params
586 |
587 | query = urllib.urlencode(str_dict(params), doseq=True)
588 | url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
589 |
590 | req = Request(url, data, headers, method=method)
591 | return Response(req, stream)
592 |
593 |
594 | def get(url, params=None, headers=None, cookies=None, auth=None,
595 | timeout=60, allow_redirects=True, stream=False):
596 | """Initiate a GET request. Arguments as for :func:`request`.
597 |
598 | :returns: :class:`Response` instance
599 |
600 | """
601 | return request('GET', url, params, headers=headers, cookies=cookies,
602 | auth=auth, timeout=timeout, allow_redirects=allow_redirects,
603 | stream=stream)
604 |
605 |
606 | def delete(url, params=None, data=None, headers=None, cookies=None, auth=None,
607 | timeout=60, allow_redirects=True, stream=False):
608 | """Initiate a DELETE request. Arguments as for :func:`request`.
609 |
610 | :returns: :class:`Response` instance
611 |
612 | """
613 | return request('DELETE', url, params, data, headers=headers,
614 | cookies=cookies, auth=auth, timeout=timeout,
615 | allow_redirects=allow_redirects, stream=stream)
616 |
617 |
618 | def post(url, params=None, data=None, headers=None, cookies=None, files=None,
619 | auth=None, timeout=60, allow_redirects=False, stream=False):
620 | """Initiate a POST request. Arguments as for :func:`request`.
621 |
622 | :returns: :class:`Response` instance
623 |
624 | """
625 | return request('POST', url, params, data, headers, cookies, files, auth,
626 | timeout, allow_redirects, stream)
627 |
628 |
629 | def put(url, params=None, data=None, headers=None, cookies=None, files=None,
630 | auth=None, timeout=60, allow_redirects=False, stream=False):
631 | """Initiate a PUT request. Arguments as for :func:`request`.
632 |
633 | :returns: :class:`Response` instance
634 |
635 | """
636 | return request('PUT', url, params, data, headers, cookies, files, auth,
637 | timeout, allow_redirects, stream)
638 |
639 |
640 | def encode_multipart_formdata(fields, files):
641 | """Encode form data (``fields``) and ``files`` for POST request.
642 |
643 | :param fields: mapping of ``{name : value}`` pairs for normal form fields.
644 | :type fields: dict
645 | :param files: dictionary of fieldnames/files elements for file data.
646 | See below for details.
647 | :type files: dict of :class:`dict`
648 | :returns: ``(headers, body)`` ``headers`` is a
649 | :class:`dict` of HTTP headers
650 | :rtype: 2-tuple ``(dict, str)``
651 |
652 | The ``files`` argument is a dictionary::
653 |
654 | {'fieldname' : { 'filename': 'blah.txt',
655 | 'content': '',
656 | 'mimetype': 'text/plain'}
657 | }
658 |
659 | - ``fieldname`` is the name of the field in the HTML form.
660 | - ``mimetype`` is optional. If not provided, :mod:`mimetypes` will
661 | be used to guess the mimetype, or ``application/octet-stream``
662 | will be used.
663 |
664 | """
665 | def get_content_type(filename):
666 | """Return or guess mimetype of ``filename``.
667 |
668 | :param filename: filename of file
669 | :type filename: unicode/str
670 | :returns: mime-type, e.g. ``text/html``
671 | :rtype: str
672 |
673 | """
674 | return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
675 |
676 | boundary = '-----' + ''.join(random.choice(BOUNDARY_CHARS)
677 | for i in range(30))
678 | CRLF = '\r\n'
679 | output = []
680 |
681 | # Normal form fields
682 | for (name, value) in fields.items():
683 | if isinstance(name, unicode):
684 | name = name.encode('utf-8')
685 | if isinstance(value, unicode):
686 | value = value.encode('utf-8')
687 | output.append('--' + boundary)
688 | output.append('Content-Disposition: form-data; name="%s"' % name)
689 | output.append('')
690 | output.append(value)
691 |
692 | # Files to upload
693 | for name, d in files.items():
694 | filename = d[u'filename']
695 | content = d[u'content']
696 | if u'mimetype' in d:
697 | mimetype = d[u'mimetype']
698 | else:
699 | mimetype = get_content_type(filename)
700 | if isinstance(name, unicode):
701 | name = name.encode('utf-8')
702 | if isinstance(filename, unicode):
703 | filename = filename.encode('utf-8')
704 | if isinstance(mimetype, unicode):
705 | mimetype = mimetype.encode('utf-8')
706 | output.append('--' + boundary)
707 | output.append('Content-Disposition: form-data; '
708 | 'name="%s"; filename="%s"' % (name, filename))
709 | output.append('Content-Type: %s' % mimetype)
710 | output.append('')
711 | output.append(content)
712 |
713 | output.append('--' + boundary + '--')
714 | output.append('')
715 | body = CRLF.join(output)
716 | headers = {
717 | 'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
718 | 'Content-Length': str(len(body)),
719 | }
720 | return (headers, body)
721 |
--------------------------------------------------------------------------------
/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+ version of :class:`~workflow.Workflow`.
11 |
12 | :class:`~workflow.Workflow3` supports 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 ICON_WARNING, 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 or list, optional): Main output/``{query}``.
54 | **variables: Workflow variables to set.
55 |
56 | In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a
57 | :class:`list` or :class:`tuple`.
58 |
59 | Attributes:
60 | arg (unicode or list): Output value (``{query}``).
61 | In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a
62 | :class:`list` or :class:`tuple`.
63 | config (dict): Configuration for downstream workflow element.
64 |
65 | """
66 |
67 | def __init__(self, arg=None, **variables):
68 | """Create a new `Variables` object."""
69 | self.arg = arg
70 | self.config = {}
71 | super(Variables, self).__init__(**variables)
72 |
73 | @property
74 | def obj(self):
75 | """``alfredworkflow`` :class:`dict`."""
76 | o = {}
77 | if self:
78 | d2 = {}
79 | for k, v in self.items():
80 | d2[k] = v
81 | o['variables'] = d2
82 |
83 | if self.config:
84 | o['config'] = self.config
85 |
86 | if self.arg is not None:
87 | o['arg'] = self.arg
88 |
89 | return {'alfredworkflow': o}
90 |
91 | def __unicode__(self):
92 | """Convert to ``alfredworkflow`` JSON object.
93 |
94 | Returns:
95 | unicode: ``alfredworkflow`` JSON object
96 |
97 | """
98 | if not self and not self.config:
99 | if not self.arg:
100 | return u''
101 | if isinstance(self.arg, unicode):
102 | return self.arg
103 |
104 | return json.dumps(self.obj)
105 |
106 | def __str__(self):
107 | """Convert to ``alfredworkflow`` JSON object.
108 |
109 | Returns:
110 | str: UTF-8 encoded ``alfredworkflow`` JSON object
111 |
112 | """
113 | return unicode(self).encode('utf-8')
114 |
115 |
116 | class Modifier(object):
117 | """Modify :class:`Item3` arg/icon/variables when modifier key is pressed.
118 |
119 | Don't use this class directly (as it won't be associated with any
120 | :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
121 | to add modifiers to results.
122 |
123 | >>> it = wf.add_item('Title', 'Subtitle', valid=True)
124 | >>> it.setvar('name', 'default')
125 | >>> m = it.add_modifier('cmd')
126 | >>> m.setvar('name', 'alternate')
127 |
128 | See :ref:`workflow-variables` in the User Guide for more information
129 | and :ref:`example usage `.
130 |
131 | Args:
132 | key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
133 | subtitle (unicode, optional): Override default subtitle.
134 | arg (unicode, optional): Argument to pass for this modifier.
135 | valid (bool, optional): Override item's validity.
136 | icon (unicode, optional): Filepath/UTI of icon to use
137 | icontype (unicode, optional): Type of icon. See
138 | :meth:`Workflow.add_item() `
139 | for valid values.
140 |
141 | Attributes:
142 | arg (unicode): Arg to pass to following action.
143 | config (dict): Configuration for a downstream element, such as
144 | a File Filter.
145 | icon (unicode): Filepath/UTI of icon.
146 | icontype (unicode): Type of icon. See
147 | :meth:`Workflow.add_item() `
148 | for valid values.
149 | key (unicode): Modifier key (see above).
150 | subtitle (unicode): Override item subtitle.
151 | valid (bool): Override item validity.
152 | variables (dict): Workflow variables set by this modifier.
153 |
154 | """
155 |
156 | def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None,
157 | icontype=None):
158 | """Create a new :class:`Modifier`.
159 |
160 | Don't use this class directly (as it won't be associated with any
161 | :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
162 | to add modifiers to results.
163 |
164 | Args:
165 | key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
166 | subtitle (unicode, optional): Override default subtitle.
167 | arg (unicode, optional): Argument to pass for this modifier.
168 | valid (bool, optional): Override item's validity.
169 | icon (unicode, optional): Filepath/UTI of icon to use
170 | icontype (unicode, optional): Type of icon. See
171 | :meth:`Workflow.add_item() `
172 | for valid values.
173 |
174 | """
175 | self.key = key
176 | self.subtitle = subtitle
177 | self.arg = arg
178 | self.valid = valid
179 | self.icon = icon
180 | self.icontype = icontype
181 |
182 | self.config = {}
183 | self.variables = {}
184 |
185 | def setvar(self, name, value):
186 | """Set a workflow variable for this Item.
187 |
188 | Args:
189 | name (unicode): Name of variable.
190 | value (unicode): Value of variable.
191 |
192 | """
193 | self.variables[name] = value
194 |
195 | def getvar(self, name, default=None):
196 | """Return value of workflow variable for ``name`` or ``default``.
197 |
198 | Args:
199 | name (unicode): Variable name.
200 | default (None, optional): Value to return if variable is unset.
201 |
202 | Returns:
203 | unicode or ``default``: Value of variable if set or ``default``.
204 |
205 | """
206 | return self.variables.get(name, default)
207 |
208 | @property
209 | def obj(self):
210 | """Modifier formatted for JSON serialization for Alfred 3.
211 |
212 | Returns:
213 | dict: Modifier for serializing to JSON.
214 |
215 | """
216 | o = {}
217 |
218 | if self.subtitle is not None:
219 | o['subtitle'] = self.subtitle
220 |
221 | if self.arg is not None:
222 | o['arg'] = self.arg
223 |
224 | if self.valid is not None:
225 | o['valid'] = self.valid
226 |
227 | if self.variables:
228 | o['variables'] = self.variables
229 |
230 | if self.config:
231 | o['config'] = self.config
232 |
233 | icon = self._icon()
234 | if icon:
235 | o['icon'] = icon
236 |
237 | return o
238 |
239 | def _icon(self):
240 | """Return `icon` object for item.
241 |
242 | Returns:
243 | dict: Mapping for item `icon` (may be empty).
244 |
245 | """
246 | icon = {}
247 | if self.icon is not None:
248 | icon['path'] = self.icon
249 |
250 | if self.icontype is not None:
251 | icon['type'] = self.icontype
252 |
253 | return icon
254 |
255 |
256 | class Item3(object):
257 | """Represents a feedback item for Alfred 3+.
258 |
259 | Generates Alfred-compliant JSON for a single item.
260 |
261 | Don't use this class directly (as it then won't be associated with
262 | any :class:`Workflow3 ` object), but rather use
263 | :meth:`Workflow3.add_item() `.
264 | See :meth:`~workflow.Workflow3.add_item` for details of arguments.
265 |
266 | """
267 |
268 | def __init__(self, title, subtitle='', arg=None, autocomplete=None,
269 | match=None, valid=False, uid=None, icon=None, icontype=None,
270 | type=None, largetext=None, copytext=None, quicklookurl=None):
271 | """Create a new :class:`Item3` object.
272 |
273 | Use same arguments as for
274 | :class:`Workflow.Item `.
275 |
276 | Argument ``subtitle_modifiers`` is not supported.
277 |
278 | """
279 | self.title = title
280 | self.subtitle = subtitle
281 | self.arg = arg
282 | self.autocomplete = autocomplete
283 | self.match = match
284 | self.valid = valid
285 | self.uid = uid
286 | self.icon = icon
287 | self.icontype = icontype
288 | self.type = type
289 | self.quicklookurl = quicklookurl
290 | self.largetext = largetext
291 | self.copytext = copytext
292 |
293 | self.modifiers = {}
294 |
295 | self.config = {}
296 | self.variables = {}
297 |
298 | def setvar(self, name, value):
299 | """Set a workflow variable for this Item.
300 |
301 | Args:
302 | name (unicode): Name of variable.
303 | value (unicode): Value of variable.
304 |
305 | """
306 | self.variables[name] = value
307 |
308 | def getvar(self, name, default=None):
309 | """Return value of workflow variable for ``name`` or ``default``.
310 |
311 | Args:
312 | name (unicode): Variable name.
313 | default (None, optional): Value to return if variable is unset.
314 |
315 | Returns:
316 | unicode or ``default``: Value of variable if set or ``default``.
317 |
318 | """
319 | return self.variables.get(name, default)
320 |
321 | def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None,
322 | icontype=None):
323 | """Add alternative values for a modifier key.
324 |
325 | Args:
326 | key (unicode): Modifier key, e.g. ``"cmd"`` or ``"alt"``
327 | subtitle (unicode, optional): Override item subtitle.
328 | arg (unicode, optional): Input for following action.
329 | valid (bool, optional): Override item validity.
330 | icon (unicode, optional): Filepath/UTI of icon.
331 | icontype (unicode, optional): Type of icon. See
332 | :meth:`Workflow.add_item() `
333 | for valid values.
334 |
335 | In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a
336 | :class:`list` or :class:`tuple`.
337 |
338 | Returns:
339 | Modifier: Configured :class:`Modifier`.
340 |
341 | """
342 | mod = Modifier(key, subtitle, arg, valid, icon, icontype)
343 |
344 | # Add Item variables to Modifier
345 | mod.variables.update(self.variables)
346 |
347 | self.modifiers[key] = mod
348 |
349 | return mod
350 |
351 | @property
352 | def obj(self):
353 | """Item formatted for JSON serialization.
354 |
355 | Returns:
356 | dict: Data suitable for Alfred 3 feedback.
357 |
358 | """
359 | # Required values
360 | o = {
361 | 'title': self.title,
362 | 'subtitle': self.subtitle,
363 | 'valid': self.valid,
364 | }
365 |
366 | # Optional values
367 | if self.arg is not None:
368 | o['arg'] = self.arg
369 |
370 | if self.autocomplete is not None:
371 | o['autocomplete'] = self.autocomplete
372 |
373 | if self.match is not None:
374 | o['match'] = self.match
375 |
376 | if self.uid is not None:
377 | o['uid'] = self.uid
378 |
379 | if self.type is not None:
380 | o['type'] = self.type
381 |
382 | if self.quicklookurl is not None:
383 | o['quicklookurl'] = self.quicklookurl
384 |
385 | if self.variables:
386 | o['variables'] = self.variables
387 |
388 | if self.config:
389 | o['config'] = self.config
390 |
391 | # Largetype and copytext
392 | text = self._text()
393 | if text:
394 | o['text'] = text
395 |
396 | icon = self._icon()
397 | if icon:
398 | o['icon'] = icon
399 |
400 | # Modifiers
401 | mods = self._modifiers()
402 | if mods:
403 | o['mods'] = mods
404 |
405 | return o
406 |
407 | def _icon(self):
408 | """Return `icon` object for item.
409 |
410 | Returns:
411 | dict: Mapping for item `icon` (may be empty).
412 |
413 | """
414 | icon = {}
415 | if self.icon is not None:
416 | icon['path'] = self.icon
417 |
418 | if self.icontype is not None:
419 | icon['type'] = self.icontype
420 |
421 | return icon
422 |
423 | def _text(self):
424 | """Return `largetext` and `copytext` object for item.
425 |
426 | Returns:
427 | dict: `text` mapping (may be empty)
428 |
429 | """
430 | text = {}
431 | if self.largetext is not None:
432 | text['largetype'] = self.largetext
433 |
434 | if self.copytext is not None:
435 | text['copy'] = self.copytext
436 |
437 | return text
438 |
439 | def _modifiers(self):
440 | """Build `mods` dictionary for JSON feedback.
441 |
442 | Returns:
443 | dict: Modifier mapping or `None`.
444 |
445 | """
446 | if self.modifiers:
447 | mods = {}
448 | for k, mod in self.modifiers.items():
449 | mods[k] = mod.obj
450 |
451 | return mods
452 |
453 | return None
454 |
455 |
456 | class Workflow3(Workflow):
457 | """Workflow class that generates Alfred 3+ feedback.
458 |
459 | It is a subclass of :class:`~workflow.Workflow` and most of its
460 | methods are documented there.
461 |
462 | Attributes:
463 | item_class (class): Class used to generate feedback items.
464 | variables (dict): Top level workflow variables.
465 |
466 | """
467 |
468 | item_class = Item3
469 |
470 | def __init__(self, **kwargs):
471 | """Create a new :class:`Workflow3` object.
472 |
473 | See :class:`~workflow.Workflow` for documentation.
474 |
475 | """
476 | Workflow.__init__(self, **kwargs)
477 | self.variables = {}
478 | self._rerun = 0
479 | # Get session ID from environment if present
480 | self._session_id = os.getenv('_WF_SESSION_ID') or None
481 | if self._session_id:
482 | self.setvar('_WF_SESSION_ID', self._session_id)
483 |
484 | @property
485 | def _default_cachedir(self):
486 | """Alfred 4's default cache directory."""
487 | return os.path.join(
488 | os.path.expanduser(
489 | '~/Library/Caches/com.runningwithcrayons.Alfred/'
490 | 'Workflow Data/'),
491 | self.bundleid)
492 |
493 | @property
494 | def _default_datadir(self):
495 | """Alfred 4's default data directory."""
496 | return os.path.join(os.path.expanduser(
497 | '~/Library/Application Support/Alfred/Workflow Data/'),
498 | self.bundleid)
499 |
500 | @property
501 | def rerun(self):
502 | """How often (in seconds) Alfred should re-run the Script Filter."""
503 | return self._rerun
504 |
505 | @rerun.setter
506 | def rerun(self, seconds):
507 | """Interval at which Alfred should re-run the Script Filter.
508 |
509 | Args:
510 | seconds (int): Interval between runs.
511 | """
512 | self._rerun = seconds
513 |
514 | @property
515 | def session_id(self):
516 | """A unique session ID every time the user uses the workflow.
517 |
518 | .. versionadded:: 1.25
519 |
520 | The session ID persists while the user is using this workflow.
521 | It expires when the user runs a different workflow or closes
522 | Alfred.
523 |
524 | """
525 | if not self._session_id:
526 | from uuid import uuid4
527 | self._session_id = uuid4().hex
528 | self.setvar('_WF_SESSION_ID', self._session_id)
529 |
530 | return self._session_id
531 |
532 | def setvar(self, name, value, persist=False):
533 | """Set a "global" workflow variable.
534 |
535 | .. versionchanged:: 1.33
536 |
537 | These variables are always passed to downstream workflow objects.
538 |
539 | If you have set :attr:`rerun`, these variables are also passed
540 | back to the script when Alfred runs it again.
541 |
542 | Args:
543 | name (unicode): Name of variable.
544 | value (unicode): Value of variable.
545 | persist (bool, optional): Also save variable to ``info.plist``?
546 |
547 | """
548 | self.variables[name] = value
549 | if persist:
550 | from .util import set_config
551 | set_config(name, value, self.bundleid)
552 | self.logger.debug('saved variable %r with value %r to info.plist',
553 | name, value)
554 |
555 | def getvar(self, name, default=None):
556 | """Return value of workflow variable for ``name`` or ``default``.
557 |
558 | Args:
559 | name (unicode): Variable name.
560 | default (None, optional): Value to return if variable is unset.
561 |
562 | Returns:
563 | unicode or ``default``: Value of variable if set or ``default``.
564 |
565 | """
566 | return self.variables.get(name, default)
567 |
568 | def add_item(self, title, subtitle='', arg=None, autocomplete=None,
569 | valid=False, uid=None, icon=None, icontype=None, type=None,
570 | largetext=None, copytext=None, quicklookurl=None, match=None):
571 | """Add an item to be output to Alfred.
572 |
573 | Args:
574 | match (unicode, optional): If you have "Alfred filters results"
575 | turned on for your Script Filter, Alfred (version 3.5 and
576 | above) will filter against this field, not ``title``.
577 |
578 | In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a
579 | :class:`list` or :class:`tuple`.
580 |
581 | See :meth:`Workflow.add_item() ` for
582 | the main documentation and other parameters.
583 |
584 | The key difference is that this method does not support the
585 | ``modifier_subtitles`` argument. Use the :meth:`~Item3.add_modifier()`
586 | method instead on the returned item instead.
587 |
588 | Returns:
589 | Item3: Alfred feedback item.
590 |
591 | """
592 | item = self.item_class(title, subtitle, arg, autocomplete,
593 | match, valid, uid, icon, icontype, type,
594 | largetext, copytext, quicklookurl)
595 |
596 | # Add variables to child item
597 | item.variables.update(self.variables)
598 |
599 | self._items.append(item)
600 | return item
601 |
602 | @property
603 | def _session_prefix(self):
604 | """Filename prefix for current session."""
605 | return '_wfsess-{0}-'.format(self.session_id)
606 |
607 | def _mk_session_name(self, name):
608 | """New cache name/key based on session ID."""
609 | return self._session_prefix + name
610 |
611 | def cache_data(self, name, data, session=False):
612 | """Cache API with session-scoped expiry.
613 |
614 | .. versionadded:: 1.25
615 |
616 | Args:
617 | name (str): Cache key
618 | data (object): Data to cache
619 | session (bool, optional): Whether to scope the cache
620 | to the current session.
621 |
622 | ``name`` and ``data`` are the same as for the
623 | :meth:`~workflow.Workflow.cache_data` method on
624 | :class:`~workflow.Workflow`.
625 |
626 | If ``session`` is ``True``, then ``name`` is prefixed
627 | with :attr:`session_id`.
628 |
629 | """
630 | if session:
631 | name = self._mk_session_name(name)
632 |
633 | return super(Workflow3, self).cache_data(name, data)
634 |
635 | def cached_data(self, name, data_func=None, max_age=60, session=False):
636 | """Cache API with session-scoped expiry.
637 |
638 | .. versionadded:: 1.25
639 |
640 | Args:
641 | name (str): Cache key
642 | data_func (callable): Callable that returns fresh data. It
643 | is called if the cache has expired or doesn't exist.
644 | max_age (int): Maximum allowable age of cache in seconds.
645 | session (bool, optional): Whether to scope the cache
646 | to the current session.
647 |
648 | ``name``, ``data_func`` and ``max_age`` are the same as for the
649 | :meth:`~workflow.Workflow.cached_data` method on
650 | :class:`~workflow.Workflow`.
651 |
652 | If ``session`` is ``True``, then ``name`` is prefixed
653 | with :attr:`session_id`.
654 |
655 | """
656 | if session:
657 | name = self._mk_session_name(name)
658 |
659 | return super(Workflow3, self).cached_data(name, data_func, max_age)
660 |
661 | def clear_session_cache(self, current=False):
662 | """Remove session data from the cache.
663 |
664 | .. versionadded:: 1.25
665 | .. versionchanged:: 1.27
666 |
667 | By default, data belonging to the current session won't be
668 | deleted. Set ``current=True`` to also clear current session.
669 |
670 | Args:
671 | current (bool, optional): If ``True``, also remove data for
672 | current session.
673 |
674 | """
675 | def _is_session_file(filename):
676 | if current:
677 | return filename.startswith('_wfsess-')
678 | return filename.startswith('_wfsess-') \
679 | and not filename.startswith(self._session_prefix)
680 |
681 | self.clear_cache(_is_session_file)
682 |
683 | @property
684 | def obj(self):
685 | """Feedback formatted for JSON serialization.
686 |
687 | Returns:
688 | dict: Data suitable for Alfred 3 feedback.
689 |
690 | """
691 | items = []
692 | for item in self._items:
693 | items.append(item.obj)
694 |
695 | o = {'items': items}
696 | if self.variables:
697 | o['variables'] = self.variables
698 | if self.rerun:
699 | o['rerun'] = self.rerun
700 | return o
701 |
702 | def warn_empty(self, title, subtitle=u'', icon=None):
703 | """Add a warning to feedback if there are no items.
704 |
705 | .. versionadded:: 1.31
706 |
707 | Add a "warning" item to Alfred feedback if no other items
708 | have been added. This is a handy shortcut to prevent Alfred
709 | from showing its fallback searches, which is does if no
710 | items are returned.
711 |
712 | Args:
713 | title (unicode): Title of feedback item.
714 | subtitle (unicode, optional): Subtitle of feedback item.
715 | icon (str, optional): Icon for feedback item. If not
716 | specified, ``ICON_WARNING`` is used.
717 |
718 | Returns:
719 | Item3: Newly-created item.
720 |
721 | """
722 | if len(self._items):
723 | return
724 |
725 | icon = icon or ICON_WARNING
726 | return self.add_item(title, subtitle, icon=icon)
727 |
728 | def send_feedback(self):
729 | """Print stored items to console/Alfred as JSON."""
730 | if self.debugging:
731 | json.dump(self.obj, sys.stdout, indent=2, separators=(',', ': '))
732 | else:
733 | json.dump(self.obj, sys.stdout)
734 | sys.stdout.flush()
735 |
--------------------------------------------------------------------------------