├── .bandit ├── .coveragerc ├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── .gitmodules ├── .travis.yml ├── Alfred-Workflow 2.0.ft ├── LICENCE.txt ├── MANIFEST.in ├── README.md ├── README_PYPI.rst ├── TODO ├── alfred-workflow-1.40.0.zip ├── bin ├── README.md ├── build-dash-docset.sh ├── build-docs.sh ├── build-workflow.sh ├── publish-cheeseshop.sh ├── publish-docs.sh └── testone ├── docs ├── .gitignore ├── Alfred-Workflow.docset.zip ├── Makefile ├── README.md ├── _static │ ├── ICON_ACCOUNT.png │ ├── ICON_BURN.png │ ├── ICON_CLOCK.png │ ├── ICON_COLOR.png │ ├── ICON_COLOUR.png │ ├── ICON_EJECT.png │ ├── ICON_ERROR.png │ ├── ICON_FAVORITE.png │ ├── ICON_FAVOURITE.png │ ├── ICON_GROUP.png │ ├── ICON_HELP.png │ ├── ICON_HOME.png │ ├── ICON_INFO.png │ ├── ICON_NETWORK.png │ ├── ICON_NOTE.png │ ├── ICON_SETTINGS.png │ ├── ICON_SWIRL.png │ ├── ICON_SWITCH.png │ ├── ICON_SYNC.png │ ├── ICON_TRASH.png │ ├── ICON_USER.png │ ├── ICON_WARNING.png │ ├── ICON_WEB.png │ ├── custom.css │ ├── favicon.ico │ ├── gh-fork-ribbon.css │ ├── gh-fork-ribbon.ie.css │ ├── icon_256.png │ ├── screen10_workflow_error.png │ ├── screen11_add_open_url.png │ ├── screen12_5_nub.png │ ├── screen12_url_query.png │ ├── screen13_connection.png │ ├── screen14_script_filter_details.png │ ├── screen15_no_api_key.png │ ├── screen16_keyword.png │ ├── screen17_set_apikey_keyword.png │ ├── screen18_pbsetkey.png │ ├── screen19_runscript.png │ ├── screen1_blank_workflow.png │ ├── screen20_runscript_settings.png │ ├── screen21_connection.png │ ├── screen22_add_notification.png │ ├── screen22_notification_settings.png │ ├── screen23_three_way.png │ ├── screen24_keychain.png │ ├── screen25_magic.png │ ├── screen26_keyword2.png │ ├── screen27_1_keyword_2_actions.png │ ├── screen28_open_url.png │ ├── screen29_link.png │ ├── screen2_workflow_info1.png │ ├── screen30_UTI.png │ ├── screen3_add_script_filter.png │ ├── screen4_alfred_list.png │ ├── screen5_script_filter_details.png │ ├── screen6_show_in_finder.png │ ├── screen7_finder.png │ ├── screen8_finder_with_workflow.png │ └── screen9_workflow_results.png ├── _templates │ ├── about.html │ └── page.html ├── _themes │ └── alabaster ├── api │ ├── background.rst.inc │ ├── index.rst │ ├── notify.rst.inc │ ├── updates.rst.inc │ ├── util.rst.inc │ ├── web.rst.inc │ ├── workflow.rst.inc │ └── workflow3.rst.inc ├── aw-workflows.rst ├── badges.rst.inc ├── conf.py ├── contributing.rst ├── dash.rst ├── guide │ ├── background.rst │ ├── filtering.rst │ ├── icons.rst │ ├── index.rst │ ├── magic-arguments.rst │ ├── notifications.rst │ ├── persistent-data.rst │ ├── rerun.rst │ ├── serialization.rst │ ├── setup.rst │ ├── text-encoding.rst │ ├── third-party.rst │ ├── update.rst │ ├── variables.rst │ ├── versioning.rst │ └── web.rst ├── index.rst ├── indices.rst ├── installation.rst ├── licence.rst ├── quickindex.rst ├── supported-versions.rst ├── toc.rst.inc ├── tutorial.rst ├── tutorial_1.rst └── tutorial_2.rst ├── extras ├── Notify.app │ └── Contents │ │ ├── Info.plist │ │ ├── MacOS │ │ └── applet │ │ ├── PkgInfo │ │ └── Resources │ │ ├── Scripts │ │ └── main.scpt │ │ ├── applet.icns │ │ ├── applet.rsrc │ │ └── description.rtfd │ │ └── TXT.rtf ├── Notify.tgz ├── README.md ├── benchmark.py ├── benchmarks │ ├── 00-python-interpreter-only │ │ ├── info.plist │ │ └── run.sh │ ├── 01-read-info-plist │ │ ├── info.plist │ │ ├── run.sh │ │ └── script.py │ ├── 02-large-info-plist │ │ ├── info.plist │ │ ├── run.sh │ │ └── script.py │ └── 03-read-envvars │ │ ├── info.plist │ │ ├── run.sh │ │ └── script.py ├── gen_icon_table.py ├── generate_workflow_list.py ├── icons │ ├── Alfred-Workflow.icns │ ├── Alfred-Workflow.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── Alfred-Workflow.iconsproj │ ├── Alfred-Workflow.png │ └── Alfred-Workflow.sketch └── library_workflows.tsv ├── icon.png ├── modd.conf ├── requirements-ci.txt ├── requirements-docs.txt ├── requirements-test.txt ├── run-tests.sh ├── setup.cfg ├── setup.py ├── tests ├── README.md ├── __init__.py ├── conftest.py ├── data │ ├── Dummy-6.0.alfredworkflow │ ├── baidu.html │ ├── baidu.html.gz │ ├── cönfüsed.gif │ ├── cönfüsed.gif.gz │ ├── cönfüsed.gif │ ├── cönfüsed.gif.gz │ ├── fubar.txt │ ├── gh-releases-4plus.json │ ├── gh-releases-4plus.json.gz │ ├── gh-releases.json │ ├── gh-releases.json.gz │ ├── info.plist.alfred2 │ ├── info.plist.alfred3 │ ├── no-encoding.xml │ ├── no-encoding.xml.gz │ ├── us-ascii.xml │ ├── us-ascii.xml.gz │ ├── utf8.html │ ├── utf8.json │ ├── utf8.json.gz │ ├── utf8.xml │ └── utf8.xml.gz ├── lib │ └── youcanimportme.py ├── test_background.py ├── test_notify.py ├── test_update.py ├── test_update_versions.py ├── test_util.py ├── test_util_atomic.py ├── test_util_lockfile.py ├── test_util_uninterruptible.py ├── test_web.py ├── test_web_http_encoding.py ├── test_workflow.py ├── test_workflow3.py ├── test_workflow_encoding.py ├── test_workflow_env.py ├── test_workflow_files.py ├── test_workflow_filter.py ├── test_workflow_import.py ├── test_workflow_keychain.py ├── test_workflow_magic.py ├── test_workflow_magic_alfred2.py ├── test_workflow_run.py ├── test_workflow_serializers.py ├── test_workflow_settings.py ├── test_workflow_update.py ├── test_workflow_versions.py ├── test_workflow_xml.py └── util.py ├── tox.ini └── workflow ├── Notify.tgz ├── __init__.py ├── background.py ├── notify.py ├── update.py ├── util.py ├── version ├── web.py ├── workflow.py └── workflow3.py /.bandit: -------------------------------------------------------------------------------- 1 | [bandit] 2 | skips: B301,B403,B404,B405,B603 3 | exclude: /tests 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | ; .coveragerc to control coverage.py 2 | ; [run] 3 | ; branch = True 4 | 5 | [report] 6 | include = 7 | workflow/* 8 | ; fail_under = 100 9 | show_missing = 1 10 | ; Regexes for lines to exclude from consideration 11 | exclude_lines = 12 | ; Have to re-enable the standard pragma 13 | pragma: no cover 14 | pragma: nocover 15 | 16 | ; Don't complain about missing debug-only code: 17 | def __repr__ 18 | if self\.debug 19 | 20 | ; Don't complain if tests don't hit defensive assertion code: 21 | raise AssertionError 22 | raise NotImplementedError 23 | 24 | ; Don't complain if non-runnable code isn't run: 25 | if 0: 26 | if False: 27 | if __name__ == .__main__.: 28 | 29 | ignore_errors = True 30 | 31 | [html] 32 | directory = coverage_report 33 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | run: 11 | name: "tests & coverage" 12 | runs-on: macos-latest 13 | env: 14 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 15 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 2.7 23 | 24 | - name: Install test dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements-test.txt 28 | 29 | - name: Lint 30 | run: ./run-tests.sh -l 31 | 32 | - name: Unit tests 33 | run: ./run-tests.sh 34 | 35 | - name: Install coverage dependencies 36 | run: pip install -r requirements-ci.txt 37 | 38 | - name: Codacy 39 | run: python-codacy-coverage -r coverage.xml -c ${{ github.sha }} || exit 0 40 | 41 | - name: Coveralls 42 | run: coveralls || exit 0 43 | 44 | - name: Codecov 45 | run: bash <(curl -s https://codecov.io/bash) || exit 0 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | *.egg-info 4 | /dist 5 | 6 | # virtual env 7 | /venv 8 | 9 | # tests 10 | /coverage_report 11 | /coverage.xml 12 | .coverage 13 | .coverage.* 14 | .tox 15 | 16 | temp_*.py 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extras/alabaster"] 2 | path = extras/alabaster 3 | url = https://github.com/deanishe/alabaster.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | 3 | osx_image: xcode8.3 4 | 5 | env: 6 | matrix: 7 | - VERSION=2.7.10 SOURCE=macpython 8 | # Turn off 2.6 as it's no longer supported 9 | # - VERSION=2.6.9 SOURCE=macports 10 | 11 | before_install: 12 | # - brew update 13 | - brew install duti 14 | - sudo duti -s com.apple.TextEdit alfredworkflow all 15 | 16 | install: 17 | - git clone https://github.com/MacPython/terryfy 18 | - source terryfy/travis_tools.sh 19 | - get_python_environment $SOURCE $VERSION venv 20 | - pip install -r requirements-test.txt 21 | - pip install -r requirements-ci.txt 22 | 23 | script: 24 | - ./run-tests.sh -l 25 | - ./run-tests.sh 26 | 27 | after_success: 28 | - python-codacy-coverage -r coverage.xml 29 | - coveralls 30 | - bash <(curl -s https://codecov.io/bash) 31 | -------------------------------------------------------------------------------- /Alfred-Workflow 2.0.ft: -------------------------------------------------------------------------------- 1 | # Alfred-Workflow 2.0 2 | 3 | ## New features 4 | - `sqlite FTS`-based filtering (search) 5 | - Last-version-run. Workflow can ask for the last version of itself to run. Should enable the migration of old data/settings to the newer version. 6 | - Turn automatic update checks on/off 7 | - Add `kill_process()` to `background.py` 8 | - Add locking around data and cache files 9 | - Generalise update functionality via plugin system 10 | - `filter()` should warn when comparing Unicode to non-Unicode 11 | - `filter()` should replace "smart" puncutation with "dumb" punctuation 12 | - `max_age` arg of `cached_data()` should default to `0` This is currently the only API-breaking feature @api_breaking 13 | - Add logging to `web.py` 14 | 15 | ### Utility features 16 | - `run_alfred(query)` — Call Alfred via AppleScript 17 | - `applescriptify(text)` — Escape `text` for inclusion in AppleScript. 18 | - `get_clipboard()` — Return clipboard text as `Unicode` 19 | - `set_clipboard(text)` — Set clipboard text 20 | 21 | ## Refactoring of existing code 22 | Move code from `workflow.py` to other modules. 23 | 24 | ### Aims 25 | - Make `Workflow` a composed class, whereby core functionality can be overriden fairly easily (as with serialisation) 26 | - Make `workflow.py` easier to read and reason about 27 | - Make writing unit tests easier. Currently, it's a horror show, as so much depends on creating a `Workflow` instance and therefore on `info.plist` file being present. 28 | 29 | ### Requirements to meet refactoring aims.todo 30 | - Allow most individual test modules to be run without the test environment 31 | - Put tests requiring special conditions (presence of `info.plist`, non-ASCII characters in path to `workflow/`) in separate modules 32 | 33 | ### Modules 34 | The following modules are envisaged after the refactoring. 35 | 36 | #### `__init__.py` 37 | `Workflow` class and imports to form the main API. 38 | 39 | #### `constants.py` 40 | All constants from other modules to make them more readable (`ASCII_REPLACEMENTS` in particular is distractingly long) 41 | 42 | #### `core.py` 43 | Move functionality required by multiple modules to this module, so it's not necessary for `update.py`/`background.py` to instantiate `Workflow`. This includes: 44 | 45 | - Alfred environmental variables 46 | - `info.plist` parsing 47 | - logging 48 | 49 | Should have functions for the most commonly-required functionality (getting a bundle identifier, data/cache paths, values from `info.plist`). These functions should be memoised for performance (in most cases, the info *should* come from environmental variables, but that isn't ensured). 50 | 51 | #### `index.py` 52 | The home of the suggested `sqlite`-based search module. Possibly combine with `search.py`? 53 | 54 | #### `persistence.py` 55 | Data storage/caching API and Keychain access. `Settings` should probably go in here, too. 56 | 57 | #### `search.py` 58 | The `filter()` and `_filter_item()` methods of `Workflow` are excessively long. Put 'em in here for cleanliness. Possibly combine with `index.py`? 59 | 60 | #### `util.py` 61 | - `run_alfred()` 62 | - `applescriptify()` 63 | - `get_clipboard()` 64 | - `set_clipboard()` 65 | 66 | 67 | #### `update.py` 68 | Mostly as before (some default values may be moved to `constants.py`. (This doesn't take into account the proposed generalisation of the update mechanism.) 69 | 70 | #### `web.py` 71 | As before 72 | 73 | 74 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | All Python source code is under the MIT Licence. 2 | 3 | The documentation, in particular the tutorials, are under the 4 | Creative Commons Attribution-NonCommercial (CC BY-NC) licence. 5 | 6 | --------------------------------------------------------------------- 7 | 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2014 Dean Jackson 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in 20 | all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | THE SOFTWARE. 29 | 30 | --------------------------------------------------------------------- 31 | 32 | Creative Commons Attribution-NonCommercial (CC BY-NC) licence 33 | 34 | https://creativecommons.org/licenses/by-nc/4.0/legalcode 35 | 36 | (This one's quite long.) 37 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include workflow/version 2 | include workflow/Notify.tgz 3 | include README_PYPI.rst 4 | -------------------------------------------------------------------------------- /README_PYPI.rst: -------------------------------------------------------------------------------- 1 | 2 | A helper library for writing `Alfred 2, 3 and 4`_ workflows. 3 | 4 | Supports macOS 10.7+ and Python 2.7 (Alfred 3 is 10.9+/2.7 only). 5 | 6 | Alfred-Workflow is designed to take the grunt work out of writing a workflow. 7 | 8 | It gives you the tools to create a fast and featureful Alfred workflow from an 9 | API, application or library in minutes. 10 | 11 | http://www.deanishe.net/alfred-workflow/ 12 | 13 | 14 | Features 15 | ======== 16 | 17 | * Catches and logs workflow errors for easier development and support 18 | * "Magic" arguments to help development/debugging 19 | * Auto-saves settings 20 | * Super-simple data caching 21 | * Fuzzy, Alfred-like search/filtering with diacritic folding 22 | * Keychain support for secure storage (and syncing) of passwords, API keys etc. 23 | * Simple generation of Alfred feedback (XML output) 24 | * Input/output decoding for handling non-ASCII text 25 | * Lightweight web API with modelled on `requests`_ 26 | * Pre-configured logging 27 | * Painlessly add directories to ``sys.path`` 28 | * Easily launch background tasks (daemons) to keep your workflow responsive 29 | * Check for new versions and update workflows hosted on GitHub. 30 | * Post notifications via Notification Center. 31 | 32 | 33 | Alfred 3-only features 34 | ---------------------- 35 | 36 | * Set `workflow variables`_ from code 37 | * Advanced modifiers 38 | * Alfred 3-only updates (won't break Alfred 2 installs) 39 | * Re-running Script Filters 40 | 41 | 42 | Quick Example 43 | ============= 44 | 45 | Here's how to show recent `Pinboard.in `_ posts 46 | in Alfred. 47 | 48 | Create a new workflow in Alfred's preferences. Add a **Script Filter** with 49 | Language ``/usr/bin/python`` and paste the following into the **Script** 50 | field (changing ``API_KEY``): 51 | 52 | 53 | .. code-block:: python 54 | 55 | import sys 56 | from workflow import Workflow, ICON_WEB, web 57 | 58 | API_KEY = 'your-pinboard-api-key' 59 | 60 | def main(wf): 61 | url = 'https://api.pinboard.in/v1/posts/recent' 62 | params = dict(auth_token=API_KEY, count=20, format='json') 63 | r = web.get(url, params) 64 | r.raise_for_status() 65 | for post in r.json()['posts']: 66 | wf.add_item(post['description'], post['href'], arg=post['href'], 67 | uid=post['hash'], valid=True, icon=ICON_WEB) 68 | wf.send_feedback() 69 | 70 | 71 | if __name__ == u"__main__": 72 | wf = Workflow() 73 | sys.exit(wf.run(main)) 74 | 75 | 76 | Add an **Open URL** action to your workflow with ``{query}`` as the **URL**, 77 | connect your **Script Filter** to it, and you can now hit **ENTER** on a 78 | Pinboard item in Alfred to open it in your browser. 79 | 80 | 81 | Installation 82 | ============ 83 | 84 | **Note**: If you intend to distribute your workflow to other users, you 85 | should include Alfred-Workflow (and other Python libraries your workflow 86 | requires) within your workflow's directory as described below. **Do not** 87 | ask users to install anything into their system Python. Python installations 88 | cannot support multiple versions of the same library, so if you rely on 89 | globally-installed libraries, the chances are very good that your workflow 90 | will sooner or later break—or be broken by—some other software doing the 91 | same naughty thing. 92 | 93 | 94 | With pip 95 | -------- 96 | 97 | You can install Alfred-Workflow directly into your workflow with:: 98 | 99 | # from within your workflow directory 100 | pip install --target=. Alfred-Workflow 101 | 102 | You can install any other library available on the `Cheese Shop`_ the 103 | same way. See the `pip documentation`_ for more information. 104 | 105 | 106 | From source 107 | ----------- 108 | 109 | Download the ``alfred-workflow-X.X.X.zip`` file from the `GitHub releases`_ 110 | page and extract the ZIP to the root directory of your workflow (where 111 | ``info.plist`` is). 112 | 113 | Alternatively, you can download `the source code`_ from the 114 | `GitHub repository`_ and copy the ``workflow`` subfolder to the root 115 | directory of your workflow. 116 | 117 | Your workflow directory should look something like this (where 118 | ``yourscript.py`` contains your workflow code and ``info.plist`` is 119 | the workflow information file generated by Alfred):: 120 | 121 | Your Workflow/ 122 | info.plist 123 | icon.png 124 | workflow/ 125 | __init__.py 126 | background.py 127 | notify.py 128 | Notify.tgz 129 | update.py 130 | version 131 | web.py 132 | workflow.py 133 | yourscript.py 134 | etc. 135 | 136 | 137 | Documentation 138 | ============= 139 | 140 | Detailed documentation, including a tutorial, is available at 141 | http://www.deanishe.net/alfred-workflow/. 142 | 143 | .. _v2 branch: https://github.com/deanishe/alfred-workflow/tree/v2 144 | .. _requests: http://docs.python-requests.org/en/latest/ 145 | .. _Alfred 2, 3 and 4: http://www.alfredapp.com/ 146 | .. _GitHub releases: https://github.com/deanishe/alfred-workflow/releases 147 | .. _the source code: https://github.com/deanishe/alfred-workflow/archive/master.zip 148 | .. _GitHub repository: https://github.com/deanishe/alfred-workflow 149 | .. _Cheese Shop: https://pypi.python.org/pypi 150 | .. _pip documentation: https://pip.pypa.io/en/latest/ 151 | .. _workflow variables: http://www.deanishe.net/alfred-workflow/user-manual/workflow-variables.html 152 | -------------------------------------------------------------------------------- /alfred-workflow-1.40.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/alfred-workflow-1.40.0.zip -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | 2 | bin 3 | === 4 | 5 | Scripts for building and publishing Alfred-Workflow. 6 | 7 | - `build-workflow.sh` — Build the distributable workflow as `workflow-1.n.n.zip` in the project root directory. 8 | - `build-dash-docset.sh` — Build a docset for [Dash][dash] in `docs/` from HTML docs. 9 | - `build-docs.sh` — Generate HTML docs in `docs/_build/html/`. You must run this before running `build-dash-docset.sh`. 10 | - `publish-cheeseshop.sh` — Build the distribution and publish on PyPi. 11 | - `publish-docs.sh` — Build the docs and push them to `gh-pages` / http://www.deanishe.net/alfred-workflow/ 12 | - `testone` — Run test script(s) with coverage for one module/package. 13 | 14 | [dash]: https://kapeli.com/dash 15 | -------------------------------------------------------------------------------- /bin/build-dash-docset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | basedir=$(cd $(dirname $0)/../; pwd) 6 | docdir="${basedir}/docs" 7 | docset="Alfred-Workflow.docset" 8 | zipfile="${docset}.zip" 9 | icon="${basedir}/icon.png" 10 | 11 | 12 | echo "======================= Building Dash docset =======================" 13 | 14 | cd "$docdir" 15 | 16 | if [[ -d "$docset" ]]; then 17 | command rm -rf "$docset" 18 | fi 19 | 20 | if [[ -f "$zipfile" ]]; then 21 | command rm -f "$zipfile" 22 | fi 23 | 24 | doc2dash -f -n 'Alfred-Workflow' \ 25 | -i "$icon" \ 26 | -I "quickindex.html" \ 27 | -u "http://www.deanishe.net/alfred-workflow/" \ 28 | _build/html 29 | 30 | zip -rq "$zipfile" "$docset" 31 | # command rm -rf "$docset" 32 | 33 | cd - &>/dev/null 34 | 35 | echo "Saved Dash docset to $zipfile" 36 | -------------------------------------------------------------------------------- /bin/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | basedir=$(cd $(dirname $0)/../; pwd) 4 | docdir="${basedir}/docs" 5 | 6 | 7 | echo "========================= Building docs ==============================" 8 | 9 | cd "${docdir}" 10 | if [[ -d _build ]]; then 11 | rm -rf _build/* 12 | fi 13 | make html 14 | cd - 15 | -------------------------------------------------------------------------------- /bin/build-workflow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # Create a useable, zipped version of the workflow in the repo root. 3 | # The file will be named `alfred-workflow-X.X.X.zip`, or the like. 4 | 5 | set -e 6 | 7 | # Files to include in the zipped file 8 | patterns=('workflow/*.py') 9 | patterns+=('workflow/version') 10 | patterns+=('workflow/Notify.tgz') 11 | 12 | # Compute required paths relative to this script's location 13 | # (i.e. Don't move this script without editing it) 14 | rootdir=$(cd $(dirname $0)/../; pwd) 15 | version=$(cat "${rootdir}/workflow/version") 16 | zipfile="alfred-workflow-${version}.zip" 17 | sourcedir="workflow" 18 | 19 | log() { 20 | echo "$@" > /dev/stderr 21 | } 22 | 23 | help() { 24 | cat > /dev/stderr << EOS 25 | build-workflow.sh 26 | 27 | Create distributable, useable ZIP archive of workflow in repo root. 28 | 29 | Resulting file will be called workflow-1.n.n.zip 30 | 31 | Usage: 32 | build-workflow.sh [-n] 33 | build-workflow.sh (-h|--help) 34 | 35 | Options: 36 | -n, --nothing Only show what would be done. 37 | -h, --help Show this message and exit. 38 | 39 | EOS 40 | } 41 | 42 | dryrun=0 43 | 44 | case "$1" in 45 | -h|--help) 46 | help 47 | exit 0 48 | ;; 49 | -n|--nothing) 50 | dryrun=1 51 | ;; 52 | *) 53 | ;; 54 | esac 55 | 56 | pushd "${rootdir}" &> /dev/null 57 | 58 | if [[ -f "${zipfile}" ]] && [[ $dryrun -eq 0 ]]; then 59 | log "Deleting existing zip archive" 60 | rm -f "${zipfile}" 61 | fi 62 | 63 | mode= 64 | if [[ $dryrun -eq 1 ]]; then 65 | mode="-sf" 66 | fi 67 | 68 | zip $mode -r ${zipfile} ${sourcedir} -i $patterns 69 | 70 | log "Created ${zipfile}" 71 | popd &> /dev/null 72 | 73 | exit 0 74 | -------------------------------------------------------------------------------- /bin/publish-cheeseshop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | set -e 4 | 5 | rootdir=$(cd $(dirname $0)/../; pwd) 6 | 7 | cd "${rootdir}" 8 | version=$( cat workflow/version ) 9 | 10 | /usr/bin/python setup.py sdist 11 | twine upload dist/Alfred-Workflow-$version.tar.gz 12 | 13 | cd - 14 | -------------------------------------------------------------------------------- /bin/publish-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Publish documentation to gh-pages branch using ghp-import 6 | 7 | if [[ -z "$1" ]]; then 8 | echo "You must specify a commit message." 9 | exit 1 10 | fi 11 | 12 | # Test that ghp-import is installed 13 | command -v ghp-import > /dev/null 2>&1 || { 14 | echo "ghp-import not found." 15 | echo "'pip install ghp-import' to install" 16 | exit 1 17 | } 18 | 19 | basedir=$(cd $(dirname $0)/../; pwd) 20 | docdir="${basedir}/docs" 21 | builddir="${docdir}/_build/html" 22 | buildscript="${basedir}/bin/build-docs.sh" 23 | 24 | echo "\$basedir : ${basedir}" 25 | echo "\$docdir : ${docdir}" 26 | echo "\$builddir : ${builddir}" 27 | 28 | # cd "${docdir}" 29 | # if [[ -d _build/html ]]; then 30 | # rm -rf _build/html 31 | # fi 32 | # make html 33 | # cd - 34 | 35 | /bin/bash "${buildscript}" 36 | 37 | echo "######################################################################" 38 | echo "Publishing docs to gh-pages branch" 39 | echo "######################################################################" 40 | 41 | # Publish $builddir to gh-pages branch 42 | ghp-import -n -p -m "$1" "${builddir}" 43 | 44 | -------------------------------------------------------------------------------- /bin/testone: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rootdir="$( cd "$( dirname "$0" )/../"; pwd )" 4 | 5 | 6 | usage() { 7 | cat > /dev/stderr < ... 9 | 10 | Run test script(s) with coverage for one package. 11 | 12 | Usage: 13 | testone [-v|-V] ... 14 | testone -h 15 | 16 | Options: 17 | -v verbose output 18 | -V very verbose output 19 | -h show this message and exit 20 | 21 | Example: 22 | testone workflow.notify tests/test_notify.py 23 | 24 | EOS 25 | } 26 | 27 | if [ -t 1 ]; then 28 | red='\033[0;31m' 29 | green='\033[0;32m' 30 | nc='\033[0m' 31 | else 32 | red= 33 | green= 34 | nc= 35 | fi 36 | 37 | function log() { 38 | echo "$@" 39 | } 40 | 41 | function fail() { 42 | printf "${red}$@${nc}\n" 43 | } 44 | 45 | function success() { 46 | printf "${green}$@${nc}\n" 47 | } 48 | 49 | vopts= 50 | while getopts ":hvV" opt; do 51 | case $opt in 52 | h) 53 | usage 54 | exit 0 55 | ;; 56 | v) 57 | vopts="-v" 58 | ;; 59 | V) 60 | vopts="-vv" 61 | ;; 62 | \?) 63 | log "Invalid option: -$OPTARG" 64 | exit 1 65 | ;; 66 | esac 67 | done 68 | shift $((OPTIND-1)) 69 | 70 | 71 | if [[ "$1" = "" ]] || [[ "$2" = "" ]]; then 72 | fail "missing argument(s)" 73 | log "" 74 | usage 75 | exit 1 76 | fi 77 | 78 | package="$1" 79 | shift 80 | 81 | # Run tests 82 | pytest $vopts --cov="${package}" "$@" 83 | # ret1=$? 84 | ret1=${PIPESTATUS[0]} 85 | 86 | echo 87 | 88 | case "$ret1" in 89 | 0) success "TESTS OK";; 90 | *) fail "TESTS FAILED";; 91 | esac 92 | 93 | log "" 94 | 95 | # Test coverage 96 | coverage report --fail-under 100 --show-missing 97 | ret2=${PIPESTATUS[0]} 98 | 99 | echo 100 | 101 | case "$ret2" in 102 | 0) success "COVERAGE OK" ;; 103 | *) fail "COVERAGE FAILED" ;; 104 | esac 105 | 106 | coverage erase 107 | 108 | if [[ "$ret1" -ne 0 ]]; then 109 | exit $ret1 110 | fi 111 | 112 | exit $ret2 113 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /Alfred-Workflow.docset 2 | /_build 3 | -------------------------------------------------------------------------------- /docs/Alfred-Workflow.docset.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/Alfred-Workflow.docset.zip -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | This directory contains the *source* files for Alfred-Workflow's 3 | documentation. 4 | 5 | Read the *built* docs at http://www.deanishe.net/alfred-workflow/ 6 | -------------------------------------------------------------------------------- /docs/_static/ICON_ACCOUNT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_ACCOUNT.png -------------------------------------------------------------------------------- /docs/_static/ICON_BURN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_BURN.png -------------------------------------------------------------------------------- /docs/_static/ICON_CLOCK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_CLOCK.png -------------------------------------------------------------------------------- /docs/_static/ICON_COLOR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_COLOR.png -------------------------------------------------------------------------------- /docs/_static/ICON_COLOUR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_COLOUR.png -------------------------------------------------------------------------------- /docs/_static/ICON_EJECT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_EJECT.png -------------------------------------------------------------------------------- /docs/_static/ICON_ERROR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_ERROR.png -------------------------------------------------------------------------------- /docs/_static/ICON_FAVORITE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_FAVORITE.png -------------------------------------------------------------------------------- /docs/_static/ICON_FAVOURITE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_FAVOURITE.png -------------------------------------------------------------------------------- /docs/_static/ICON_GROUP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_GROUP.png -------------------------------------------------------------------------------- /docs/_static/ICON_HELP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_HELP.png -------------------------------------------------------------------------------- /docs/_static/ICON_HOME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_HOME.png -------------------------------------------------------------------------------- /docs/_static/ICON_INFO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_INFO.png -------------------------------------------------------------------------------- /docs/_static/ICON_NETWORK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_NETWORK.png -------------------------------------------------------------------------------- /docs/_static/ICON_NOTE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_NOTE.png -------------------------------------------------------------------------------- /docs/_static/ICON_SETTINGS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_SETTINGS.png -------------------------------------------------------------------------------- /docs/_static/ICON_SWIRL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_SWIRL.png -------------------------------------------------------------------------------- /docs/_static/ICON_SWITCH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_SWITCH.png -------------------------------------------------------------------------------- /docs/_static/ICON_SYNC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_SYNC.png -------------------------------------------------------------------------------- /docs/_static/ICON_TRASH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_TRASH.png -------------------------------------------------------------------------------- /docs/_static/ICON_USER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_USER.png -------------------------------------------------------------------------------- /docs/_static/ICON_WARNING.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_WARNING.png -------------------------------------------------------------------------------- /docs/_static/ICON_WEB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/ICON_WEB.png -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* GitHub "fork me" ribbon */ 2 | /* 3 | .github-fork-ribbon { 4 | background-color: #333; 5 | } 6 | 7 | .versionmodified { 8 | font-style: italic; 9 | } 10 | */ 11 | 12 | div.sphinxsidebar p.shield a, 13 | div.sphinxsidebar p.shield a:link, 14 | div.sphinxsidebar p.shield a:hover { 15 | border-bottom: 0; 16 | text-decoration: none; 17 | } 18 | 19 | /* Top navigation bar */ 20 | div.related ul { 21 | margin: 0.25em 1em; 22 | } 23 | 24 | /*div.section > div.topic,*/ 25 | div#contents { 26 | border-left: none; 27 | border-right: none; 28 | background-color: inherit; 29 | } 30 | 31 | a.reference.image-reference, 32 | a.reference.image-reference:hover, 33 | a.reference.image-reference:active, 34 | a.reference.image-reference:link { 35 | text-decoration: none; 36 | border-bottom: none; 37 | } 38 | 39 | div[class^='highlight'], 40 | div.highlight, 41 | div.highlight-python { 42 | box-sizing: border-box; 43 | -webkit-box-sizing: border-box; 44 | -moz-box-sizing: border-box; 45 | overflow-x: auto; 46 | /*background-color: inherit;*/ 47 | background-color: #fff; 48 | } 49 | 50 | div[class^='highlight'] { 51 | border: 1px solid #ccc; 52 | } 53 | 54 | div[class^='highlight'] div.highlight { 55 | border: none; 56 | } 57 | 58 | div.linenodiv { 59 | /*background-color: #fff;*/ 60 | padding-right: 0.25em; 61 | padding-left: 0.25em; 62 | } 63 | 64 | div.highlight pre { 65 | /*overflow-x: auto;*/ 66 | padding-left: 1em; 67 | padding-right: 1em; 68 | background-color: #fff; 69 | /*border-left: 1px solid #ccc;*/ 70 | } 71 | 72 | pre { 73 | white-space: pre; 74 | } 75 | 76 | table.highlighttable { 77 | border-collapse: collapse; 78 | border-spacing: 0; 79 | display: table; 80 | max-width: 100%; 81 | margin-left: 0; 82 | /*background-color: #fff;*/ 83 | } 84 | 85 | td.code, 86 | td.linenos { 87 | vertical-align: top; 88 | } 89 | 90 | td.linenos { 91 | border-right: 1px solid #ccc; 92 | } 93 | 94 | table.highlighttable td { 95 | padding-left: 0; 96 | padding-right: 0; 97 | } 98 | 99 | /* Grey */ 100 | div.admonition, div.note, div.seealso { 101 | background-color: #eee; 102 | border: 1px solid #ccc; 103 | } 104 | 105 | /* Blue */ 106 | div.tip, div.note, div.seealso { 107 | background-color: #e7f2fa; 108 | border: 1px solid #6ab0de; 109 | } 110 | 111 | /* Yellow */ 112 | div.warning, div.important { 113 | background-color: #fef9e9; 114 | border: 1px solid #fbe091; 115 | } 116 | 117 | /* Red */ 118 | div.danger, div.error { 119 | background-color: #FCC; 120 | border: 1px solid #FAA; 121 | } 122 | 123 | 124 | @media screen and (max-width: 875px) { 125 | ul ul { 126 | margin-left: 30px; 127 | } 128 | } -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/gh-fork-ribbon.css: -------------------------------------------------------------------------------- 1 | /* Left will inherit from right (so we don't need to duplicate code) */ 2 | .github-fork-ribbon { 3 | /* The right and left classes determine the side we attach our banner to */ 4 | position: absolute; 5 | 6 | /* Add a bit of padding to give some substance outside the "stitching" */ 7 | padding: 2px 0; 8 | 9 | /* Set the base colour */ 10 | background-color: #a00; 11 | 12 | /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */ 13 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0.15))); 14 | background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 15 | background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 16 | background-image: -ms-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 17 | background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 18 | background-image: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 19 | 20 | /* Add a drop shadow */ 21 | -webkit-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); 22 | -moz-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); 23 | box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); 24 | 25 | z-index: 9999; 26 | pointer-events: auto; 27 | } 28 | 29 | .github-fork-ribbon a, 30 | .github-fork-ribbon a:hover { 31 | /* Set the font */ 32 | font: 700 13px "Helvetica Neue", Helvetica, Arial, sans-serif; 33 | color: #fff; 34 | 35 | /* Set the text properties */ 36 | text-decoration: none; 37 | text-shadow: 0 -1px rgba(0, 0, 0, 0.5); 38 | text-align: center; 39 | 40 | /* Set the geometry. If you fiddle with these you'll also need 41 | to tweak the top and right values in .github-fork-ribbon. */ 42 | width: 200px; 43 | line-height: 20px; 44 | 45 | /* Set the layout properties */ 46 | display: inline-block; 47 | padding: 2px 0; 48 | 49 | /* Add "stitching" effect */ 50 | border-width: 1px 0; 51 | border-style: dotted; 52 | border-color: #fff; 53 | border-color: rgba(255, 255, 255, 0.7); 54 | } 55 | 56 | .github-fork-ribbon-wrapper { 57 | width: 150px; 58 | height: 150px; 59 | position: absolute; 60 | overflow: hidden; 61 | top: 0; 62 | z-index: 9999; 63 | pointer-events: none; 64 | } 65 | 66 | .github-fork-ribbon-wrapper.fixed { 67 | position: fixed; 68 | } 69 | 70 | .github-fork-ribbon-wrapper.left { 71 | left: 0; 72 | } 73 | 74 | .github-fork-ribbon-wrapper.right { 75 | right: 0; 76 | } 77 | 78 | .github-fork-ribbon-wrapper.left-bottom { 79 | position: fixed; 80 | top: inherit; 81 | bottom: 0; 82 | left: 0; 83 | } 84 | 85 | .github-fork-ribbon-wrapper.right-bottom { 86 | position: fixed; 87 | top: inherit; 88 | bottom: 0; 89 | right: 0; 90 | } 91 | 92 | .github-fork-ribbon-wrapper.right .github-fork-ribbon { 93 | top: 42px; 94 | right: -43px; 95 | 96 | -webkit-transform: rotate(45deg); 97 | -moz-transform: rotate(45deg); 98 | -ms-transform: rotate(45deg); 99 | -o-transform: rotate(45deg); 100 | transform: rotate(45deg); 101 | } 102 | 103 | .github-fork-ribbon-wrapper.left .github-fork-ribbon { 104 | top: 42px; 105 | left: -43px; 106 | 107 | -webkit-transform: rotate(-45deg); 108 | -moz-transform: rotate(-45deg); 109 | -ms-transform: rotate(-45deg); 110 | -o-transform: rotate(-45deg); 111 | transform: rotate(-45deg); 112 | } 113 | 114 | 115 | .github-fork-ribbon-wrapper.left-bottom .github-fork-ribbon { 116 | top: 80px; 117 | left: -43px; 118 | 119 | -webkit-transform: rotate(45deg); 120 | -moz-transform: rotate(45deg); 121 | -ms-transform: rotate(45deg); 122 | -o-transform: rotate(45deg); 123 | transform: rotate(45deg); 124 | } 125 | 126 | .github-fork-ribbon-wrapper.right-bottom .github-fork-ribbon { 127 | top: 80px; 128 | right: -43px; 129 | 130 | -webkit-transform: rotate(-45deg); 131 | -moz-transform: rotate(-45deg); 132 | -ms-transform: rotate(-45deg); 133 | -o-transform: rotate(-45deg); 134 | transform: rotate(-45deg); 135 | } 136 | -------------------------------------------------------------------------------- /docs/_static/gh-fork-ribbon.ie.css: -------------------------------------------------------------------------------- 1 | /* IE voodoo courtesy of http://stackoverflow.com/a/4617511/263871 and 2 | * http://www.useragentman.com/IETransformsTranslator */ 3 | 4 | .github-fork-ribbon { 5 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#000000', EndColorStr='#000000'); 6 | } 7 | 8 | .github-fork-ribbon-wrapper.right .github-fork-ribbon { 9 | /* IE positioning hack (couldn't find a transform-origin alternative for IE) */ 10 | top: -22px; 11 | right: -62px; 12 | 13 | /* IE8+ */ 14 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865474, M12=-0.7071067811865477, M21=0.7071067811865477, M22=0.7071067811865474, SizingMethod='auto expand')"; 15 | /* IE6 and 7 */ 16 | filter: progid:DXImageTransform.Microsoft.Matrix( 17 | M11=0.7071067811865474, 18 | M12=-0.7071067811865477, 19 | M21=0.7071067811865477, 20 | M22=0.7071067811865474, 21 | SizingMethod='auto expand' 22 | ); 23 | } 24 | 25 | .github-fork-ribbon-wrapper.left .github-fork-ribbon { 26 | top: -22px; 27 | left: -22px; 28 | 29 | /* IE8+ */ 30 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865483, M12=0.7071067811865467, M21=-0.7071067811865467, M22=0.7071067811865483, SizingMethod='auto expand')"; 31 | /* IE6 and 7 */ 32 | filter: progid:DXImageTransform.Microsoft.Matrix( 33 | M11=0.7071067811865483, 34 | M12=0.7071067811865467, 35 | M21=-0.7071067811865467, 36 | M22=0.7071067811865483, 37 | SizingMethod='auto expand' 38 | ); 39 | } 40 | 41 | .github-fork-ribbon-wrapper.left-bottom .github-fork-ribbon { 42 | /* IE positioning hack (couldn't find a transform-origin alternative for IE) */ 43 | top: 12px; 44 | left: -22px; 45 | 46 | 47 | /* IE8+ */ 48 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865474, M12=-0.7071067811865477, M21=0.7071067811865477, M22=0.7071067811865474, SizingMethod='auto expand')"; 49 | /* IE6 and 7 */ 50 | /* filter: progid:DXImageTransform.Microsoft.Matrix( 51 | M11=0.7071067811865474, 52 | M12=-0.7071067811865477, 53 | M21=0.7071067811865477, 54 | M22=0.7071067811865474, 55 | SizingMethod='auto expand' 56 | ); 57 | */} 58 | 59 | .github-fork-ribbon-wrapper.right-bottom .github-fork-ribbon { 60 | top: 12px; 61 | right: -62px; 62 | 63 | /* IE8+ */ 64 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865483, M12=0.7071067811865467, M21=-0.7071067811865467, M22=0.7071067811865483, SizingMethod='auto expand')"; 65 | /* IE6 and 7 */ 66 | filter: progid:DXImageTransform.Microsoft.Matrix( 67 | M11=0.7071067811865483, 68 | M12=0.7071067811865467, 69 | M21=-0.7071067811865467, 70 | M22=0.7071067811865483, 71 | SizingMethod='auto expand' 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /docs/_static/icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/icon_256.png -------------------------------------------------------------------------------- /docs/_static/screen10_workflow_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen10_workflow_error.png -------------------------------------------------------------------------------- /docs/_static/screen11_add_open_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen11_add_open_url.png -------------------------------------------------------------------------------- /docs/_static/screen12_5_nub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen12_5_nub.png -------------------------------------------------------------------------------- /docs/_static/screen12_url_query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen12_url_query.png -------------------------------------------------------------------------------- /docs/_static/screen13_connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen13_connection.png -------------------------------------------------------------------------------- /docs/_static/screen14_script_filter_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen14_script_filter_details.png -------------------------------------------------------------------------------- /docs/_static/screen15_no_api_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen15_no_api_key.png -------------------------------------------------------------------------------- /docs/_static/screen16_keyword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen16_keyword.png -------------------------------------------------------------------------------- /docs/_static/screen17_set_apikey_keyword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen17_set_apikey_keyword.png -------------------------------------------------------------------------------- /docs/_static/screen18_pbsetkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen18_pbsetkey.png -------------------------------------------------------------------------------- /docs/_static/screen19_runscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen19_runscript.png -------------------------------------------------------------------------------- /docs/_static/screen1_blank_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen1_blank_workflow.png -------------------------------------------------------------------------------- /docs/_static/screen20_runscript_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen20_runscript_settings.png -------------------------------------------------------------------------------- /docs/_static/screen21_connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen21_connection.png -------------------------------------------------------------------------------- /docs/_static/screen22_add_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen22_add_notification.png -------------------------------------------------------------------------------- /docs/_static/screen22_notification_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen22_notification_settings.png -------------------------------------------------------------------------------- /docs/_static/screen23_three_way.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen23_three_way.png -------------------------------------------------------------------------------- /docs/_static/screen24_keychain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen24_keychain.png -------------------------------------------------------------------------------- /docs/_static/screen25_magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen25_magic.png -------------------------------------------------------------------------------- /docs/_static/screen26_keyword2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen26_keyword2.png -------------------------------------------------------------------------------- /docs/_static/screen27_1_keyword_2_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen27_1_keyword_2_actions.png -------------------------------------------------------------------------------- /docs/_static/screen28_open_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen28_open_url.png -------------------------------------------------------------------------------- /docs/_static/screen29_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen29_link.png -------------------------------------------------------------------------------- /docs/_static/screen2_workflow_info1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen2_workflow_info1.png -------------------------------------------------------------------------------- /docs/_static/screen30_UTI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen30_UTI.png -------------------------------------------------------------------------------- /docs/_static/screen3_add_script_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen3_add_script_filter.png -------------------------------------------------------------------------------- /docs/_static/screen4_alfred_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen4_alfred_list.png -------------------------------------------------------------------------------- /docs/_static/screen5_script_filter_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen5_script_filter_details.png -------------------------------------------------------------------------------- /docs/_static/screen6_show_in_finder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen6_show_in_finder.png -------------------------------------------------------------------------------- /docs/_static/screen7_finder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen7_finder.png -------------------------------------------------------------------------------- /docs/_static/screen8_finder_with_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen8_finder_with_workflow.png -------------------------------------------------------------------------------- /docs/_static/screen9_workflow_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/docs/_static/screen9_workflow_results.png -------------------------------------------------------------------------------- /docs/_templates/about.html: -------------------------------------------------------------------------------- 1 | {% if theme_logo %} 2 |

{{ project }}

7 | {% endif %} 8 | 9 |

10 | {% else %} 11 |

{{ project }}

12 | {% endif %} 13 | 14 | {% if theme_description %} 15 |

{{ theme_description }}

16 | {% endif %} 17 | 18 | {% if theme_github_user and theme_github_repo %} 19 | {% if theme_github_button|lower == 'true' %} 20 |

21 | 23 |

24 | {% endif %} 25 | {% endif %} 26 | 27 | {% if theme_travis_button|lower != 'false' %} 28 | {% if theme_travis_button|lower == 'true' %} 29 | {% set path = theme_github_user + '/' + theme_github_repo %} 30 | {% else %} 31 | {% set path = theme_travis_button %} 32 | {% endif %} 33 |

34 | 35 | https://secure.travis-ci.org/{{ path }}.svg?branch={{ theme_badge_branch }} 39 | 40 |

41 | {% endif %} 42 | 43 | {% if theme_coveralls_button|lower != 'false' %} 44 | {% if theme_coveralls_button|lower == 'true' %} 45 | {% set path = theme_github_user + '/' + theme_github_repo %} 46 | {% else %} 47 | {% set path = theme_coveralls_button %} 48 | {% endif %} 49 |

50 | 51 | coverage icon 55 | 56 | {% endif %} 57 | {# src="https://img.shields.io/coveralls/{{ path }}/{{ theme_badge_branch }}.svg?style=flat" #} 58 | 59 | 60 | {% if theme_codecov_button|lower != 'false' %} 61 | {% if theme_codecov_button|lower == 'true' %} 62 | {% set path = theme_github_user + '/' + theme_github_repo %} 63 | {% else %} 64 | {% set path = theme_codecov_button %} 65 | {% endif %} 66 |

67 | 68 | https://codecov.io/github/{{ path }}/coverage.svg?branch={{ theme_badge_branch }} 72 | 73 |

74 | {% endif %} 75 | 76 | {% if theme_pypi_button|lower != 'false' %} 77 | {% if theme_pypi_button|lower == 'true' %} 78 | {% set path = project %} 79 | {% else %} 80 | {% set path = theme_pypi_button %} 81 | {% endif %} 82 |

83 | 84 | https://img.shields.io/pypi/v/{{ path }}.svg?style=flat 88 | 89 | {% endif %} -------------------------------------------------------------------------------- /docs/_templates/page.html: -------------------------------------------------------------------------------- 1 | {# Import the theme's layout. #} 2 | {%- set favicon = 'favicon.ico' %} 3 | {%- set display_github = False %} 4 | {%- set show_source = False %} 5 | {% extends "!page.html" %} 6 | 7 | {% block extrahead %} 8 | {{ super() }} 9 | {# #} 10 | 13 | 14 | {% endblock %} 15 | 16 | {# 17 | {% if not dash_docset %} 18 | {% block relbar1 %}{% endblock %} 19 | {% endif %} 20 | #} 21 | 22 | {% block footer %} 23 | {# 24 |

25 |
26 | Fork me on GitHub 27 |
28 |
29 | #} 30 | {{ super() }} 31 | 32 | 33 | 45 | 46 | 47 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /docs/_themes/alabaster: -------------------------------------------------------------------------------- 1 | ../../extras/alabaster/alabaster -------------------------------------------------------------------------------- /docs/api/background.rst.inc: -------------------------------------------------------------------------------- 1 | 2 | .. _api-background: 3 | 4 | Background processes 5 | -------------------- 6 | 7 | .. module:: workflow.background 8 | 9 | .. versionadded:: 1.4 10 | 11 | As Alfred may try to run your workflow on every keypress, it's a good idea 12 | to execute longer-running tasks (e.g. fetching data from the a webservice 13 | or application) in a background process. 14 | 15 | .. automodule:: workflow.background 16 | :noindex: 17 | 18 | .. autofunction:: run_in_background 19 | .. autofunction:: is_running 20 | .. autofunction:: kill 21 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _api: 3 | 4 | ================= 5 | API documentation 6 | ================= 7 | 8 | Documentation for Alfred-Workflow's classes and methods. 9 | 10 | See :ref:`user-manual` for documentation focussed on performing specific 11 | tasks. 12 | 13 | See the :ref:`main index ` for a list of all classes and methods. 14 | 15 | 16 | .. contents:: 17 | :local: 18 | 19 | .. include:: workflow.rst.inc 20 | 21 | .. include:: workflow3.rst.inc 22 | 23 | .. include:: background.rst.inc 24 | 25 | .. include:: web.rst.inc 26 | 27 | .. include:: updates.rst.inc 28 | 29 | .. include:: notify.rst.inc 30 | 31 | .. include:: util.rst.inc 32 | -------------------------------------------------------------------------------- /docs/api/notify.rst.inc: -------------------------------------------------------------------------------- 1 | 2 | .. _api-notify: 3 | 4 | Notifications 5 | ------------- 6 | 7 | .. module:: workflow.notify 8 | 9 | .. versionadded:: 1.15 10 | 11 | .. note:: 12 | 13 | Notifications aren't available in version of macOS older than 14 | 10.8/Mountain Lion. Calling :func:`notify` on these systems will 15 | silently do nothing. 16 | 17 | :mod:`~workflow.notify` allows you to post notifications via macOS's 18 | Notification Center at any time, not just at the end of your script. 19 | Furthermore, it uses your workflow's icon, not Alfred's. 20 | 21 | The main API is a single function, :func:`notify`. 22 | 23 | It works by copying a simple application to your workflow's data 24 | directory. It replaces the application's icon with your workflow's 25 | icon and then calls the application to post notifications. 26 | 27 | .. autofunction:: notify 28 | 29 | -------------------------------------------------------------------------------- /docs/api/updates.rst.inc: -------------------------------------------------------------------------------- 1 | 2 | .. _api-updates: 3 | 4 | Workflow updates 5 | ---------------- 6 | 7 | .. module:: workflow.update 8 | 9 | .. versionadded:: 1.9 10 | 11 | Add self-updating capabilities to your workflow. It regularly (every day 12 | by default) fetches the latest releases from the specified GitHub repository. 13 | 14 | Currently, only updates from `GitHub releases`_ are supported. 15 | 16 | .. important:: 17 | 18 | Alfred-Workflow will check for updates, but will neither install them 19 | nor notify the user that an update is available. 20 | 21 | 22 | Normally, you would not use :mod:`workflow.update` directly, but use the 23 | corresponding methods on :class:`~workflow.Workflow` and 24 | :class:`~workflow.Workflow3`. 25 | 26 | Please see :ref:`guide-updates` in the :ref:`user-manual` for information 27 | on how to enable automatic updates in your workflow. 28 | 29 | 30 | .. autofunction:: check_update 31 | .. autofunction:: install_update 32 | 33 | 34 | Helpers 35 | ^^^^^^^ 36 | 37 | .. autoclass:: Download 38 | :members: 39 | 40 | .. autoclass:: Version 41 | :members: 42 | 43 | .. autofunction:: retrieve_download 44 | 45 | 46 | .. _GitHub releases: https://help.github.com/categories/85/articles 47 | -------------------------------------------------------------------------------- /docs/api/util.rst.inc: -------------------------------------------------------------------------------- 1 | 2 | .. _api-util: 3 | 4 | Utility & helper functions 5 | -------------------------- 6 | 7 | 8 | .. currentmodule:: workflow.util 9 | 10 | A collection of functions and classes for common workflow-related tasks, such 11 | as running AppleScript or JXA code, or calling an External Trigger. 12 | 13 | 14 | Scripting 15 | ^^^^^^^^^ 16 | 17 | Functions to simplify running scripts, programs and applications. 18 | 19 | .. autofunction:: run_command 20 | 21 | .. autofunction:: run_applescript 22 | 23 | .. autofunction:: run_jxa 24 | 25 | .. autofunction:: run_trigger 26 | 27 | .. autoclass:: AppInfo 28 | 29 | .. autofunction:: appinfo 30 | 31 | 32 | Text 33 | ^^^^ 34 | 35 | Text encoding and formatting. 36 | 37 | .. autofunction:: unicodify 38 | 39 | .. autofunction:: utf8ify 40 | 41 | .. autofunction:: applescriptify 42 | 43 | 44 | Alfred's API 45 | ^^^^^^^^^^^^ 46 | 47 | Alfred-Workflow provides functions that enable you to call Alfred's AppleScript API directly from Python. 48 | 49 | 50 | Workflow stuff 51 | """""""""""""" 52 | 53 | Manipulate the values of workflow variables in the `workflow configuration sheet`_/``info.plist``. 54 | 55 | .. autofunction:: set_config 56 | 57 | .. autofunction:: unset_config 58 | 59 | Tell Alfred to reload a workflow from disk if it has changed. Normally, Alfred will notice when a workflow changes, but it won't if the workflow's directory is a symlink. 60 | 61 | .. autofunction:: reload_workflow 62 | 63 | 64 | Alfred stuff 65 | """""""""""" 66 | 67 | You can open Alfred in normal or file navigation mode: 68 | 69 | .. autofunction:: search_in_alfred 70 | 71 | .. autofunction:: browse_in_alfred 72 | 73 | Or tell Alfred to action one or more files/directories: 74 | 75 | .. autofunction:: action_in_alfred 76 | 77 | Finally, you can tell Alfred to use a specific theme: 78 | 79 | .. autofunction:: set_theme 80 | 81 | 82 | Miscellaneous 83 | ^^^^^^^^^^^^^ 84 | 85 | These utility classes and functions are used internally by Alfred-Workflow, 86 | but may also be useful in your workflow. 87 | 88 | 89 | Writing files 90 | """"""""""""" 91 | 92 | 93 | .. autoclass:: LockFile 94 | :members: 95 | 96 | .. autoclass:: uninterruptible 97 | :members: 98 | 99 | .. autofunction:: atomic_writer 100 | 101 | 102 | Images & sounds 103 | """"""""""""""" 104 | 105 | .. autofunction:: workflow.notify.convert_image 106 | 107 | .. autofunction:: workflow.notify.png_to_icns 108 | 109 | .. autofunction:: workflow.notify.validate_sound 110 | 111 | 112 | .. _api-util-exceptions: 113 | 114 | 115 | Exceptions 116 | ^^^^^^^^^^ 117 | 118 | The following exceptions, may be raised by utility functions. 119 | 120 | .. autoexception:: AcquisitionError 121 | 122 | .. autoexception:: subprocess.CalledProcessError 123 | 124 | 125 | .. _workflow configuration sheet: https://www.alfredapp.com/help/workflows/advanced/variables/#environment 126 | -------------------------------------------------------------------------------- /docs/api/web.rst.inc: -------------------------------------------------------------------------------- 1 | 2 | .. _api-web: 3 | 4 | HTTP requests 5 | ------------- 6 | 7 | .. module:: workflow.web 8 | 9 | :mod:`workflow.web` provides a simple API for retrieving data from the Web 10 | modelled on the excellent `requests`_ library. 11 | 12 | The purpose of :mod:`workflow.web` is to cover trivial cases at just 0.5% of 13 | the size of `requests`_. 14 | 15 | .. danger:: 16 | 17 | As :mod:`workflow.web` is based on Python 2's standard HTTP libraries, 18 | there are two important problems with SSL connections. 19 | 20 | Python versions older than 2.7.9 (i.e. pre-Yosemite) **do not** 21 | verify SSL certificates when establishing HTTPS connections. 22 | 23 | As a result, you **must not** use this module for sensitive 24 | connections unless you're certain it will only run on 2.7.9/Yosemite 25 | and later. If your workflow is Alfred 3-only, this requirement is met. 26 | 27 | Secondly, versions of macOS older than High Sierra (10.13) have an 28 | extremely outdated version of OpenSSL, which is incompatible with 29 | many servers' SSL configuration. 30 | 31 | Consequently, :mod:`workflow.web` cannot connect to such servers. 32 | As this includes GitHub's SSL configuration, the 33 | :ref:`update mechanism ` only works on High Sierra 34 | and later. 35 | 36 | 37 | .. autofunction:: get 38 | .. autofunction:: post 39 | .. autofunction:: request 40 | 41 | 42 | Response objects 43 | ^^^^^^^^^^^^^^^^ 44 | 45 | .. autoclass:: Response 46 | :members: 47 | 48 | 49 | .. _requests: http://docs.python-requests.org/en/latest/ 50 | -------------------------------------------------------------------------------- /docs/api/workflow.rst.inc: -------------------------------------------------------------------------------- 1 | 2 | .. _api-workflow: 3 | 4 | Workflow objects 5 | ---------------- 6 | 7 | .. module:: workflow 8 | 9 | The :class:`Workflow` object is the main interface to this library. 10 | 11 | :class:`Workflow` is targeted at Alfred 2. Use :class:`Workflow3` if 12 | you want to use Alfred 3's new features, such as 13 | :ref:`workflow variables ` or 14 | :ref:`more powerful modifiers `. 15 | 16 | See :ref:`setup` in the :ref:`user-manual` for an example of how to set 17 | up your Python script to best utilise the :class:`Workflow` object. 18 | 19 | 20 | .. autoclass:: Workflow 21 | :members: 22 | 23 | 24 | .. _api-settings: 25 | 26 | Settings 27 | ^^^^^^^^ 28 | 29 | .. currentmodule:: workflow 30 | 31 | :class:`Workflow` and :class:`Workflow3` objects each have a 32 | :attr:`~Workflow.settings` attribute that provides a simple API for storing 33 | workflow settings. 34 | 35 | .. autoclass:: workflow.workflow.Settings 36 | :members: 37 | 38 | .. automethod:: save(self) 39 | 40 | 41 | .. _api-serializers: 42 | 43 | Serializers 44 | ^^^^^^^^^^^ 45 | 46 | .. currentmodule:: workflow.workflow 47 | 48 | The serialization API provided by :class:`SerializerManager` is used by 49 | :class:`~workflow.Workflow` to store saved & cached data and settings. 50 | You can register your own serializers on a manager. 51 | 52 | The default manager (which supports JSON, pickle and cPickle) is at 53 | :data:`workflow.manager`. 54 | 55 | .. autoclass:: SerializerManager 56 | :members: 57 | 58 | .. autoclass:: JSONSerializer 59 | :members: 60 | 61 | .. autoclass:: CPickleSerializer 62 | :members: 63 | 64 | .. autoclass:: PickleSerializer 65 | :members: 66 | 67 | 68 | .. _api-exceptions: 69 | 70 | Exceptions 71 | ^^^^^^^^^^ 72 | 73 | .. currentmodule:: workflow 74 | 75 | Alfred-Workflow defines the following exceptions, which may be raised 76 | by the Keychain API. 77 | 78 | .. autoexception:: KeychainError 79 | 80 | .. autoexception:: PasswordNotFound 81 | 82 | .. autoexception:: workflow.workflow.PasswordExists 83 | -------------------------------------------------------------------------------- /docs/api/workflow3.rst.inc: -------------------------------------------------------------------------------- 1 | .. _api-workflow3: 2 | 3 | Workflow3 objects 4 | ----------------- 5 | 6 | .. module:: workflow.workflow3 7 | 8 | .. versionadded:: 1.18 9 | 10 | :class:`~workflow.Workflow3` is an Alfred 3-only version of 11 | :class:`~workflow.Workflow` that supports Alfred 3's new features. 12 | 13 | It supports 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:`~workflow.Workflow3.add_item` and :meth:`Item3.add_modifier()` methods 19 | respectively. If you instantiate :class:`Item3` or :class:`Modifier` objects 20 | directly, the current :class:`~workflow.Workflow3` object won't be aware of 21 | them, and they won't be sent to Alfred when you call 22 | :meth:`~workflow.Workflow3.send_feedback`. 23 | 24 | .. autoclass:: workflow.Workflow3 25 | :members: 26 | 27 | 28 | .. _api-variables: 29 | 30 | Workflow variables 31 | ^^^^^^^^^^^^^^^^^^ 32 | 33 | .. versionadded:: 1.18 34 | 35 | Alfred 3 allows you to pass around variables in your workflows. It passes 36 | these to your scripts as environment variables (access them with 37 | :func:`os.getenv`), and you can pass variables back to Alfred via JSON. 38 | 39 | In Script Filters, use the ``setvar()`` methods on :class:`~workflow.Workflow3`, 40 | :class:`Item3` and :class:`Modifier`. 41 | 42 | In Run Script actions, use the :class:`~workflow.Variables`. 43 | 44 | See :ref:`workflow-variables` in the User Guide for more information. 45 | 46 | :class:`Item3` and :class:`Modifier` are documented below, in the 47 | :ref:`api-modifiers` section. 48 | 49 | .. autoclass:: workflow.Variables 50 | :members: 51 | 52 | 53 | .. _api-modifiers: 54 | 55 | Advanced modifiers 56 | ^^^^^^^^^^^^^^^^^^ 57 | 58 | .. versionadded:: 1.18 59 | .. versionchanged:: 1.27 60 | 61 | You can specify alternate ``arg``, ``subtitle`` and variables if a 62 | Script Filter result is actioned while holding down a modifier key. 63 | 64 | As of Alfred-Workflow version 1.27 and Alfred version 3.4.1, you can also 65 | specify an alternative icon for a modifier. 66 | 67 | Add a modifier to an item using :meth:`Item3.add_modifier`, and configure 68 | the :class:`Modifier` via its attributes and :meth:`~Modifier.setvar` 69 | method. 70 | 71 | .. autoclass:: Item3 72 | :members: 73 | 74 | .. autoclass:: Modifier 75 | :members: 76 | -------------------------------------------------------------------------------- /docs/badges.rst.inc: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/deanishe/alfred-workflow/workflows/build/badge.svg 2 | :target: https://github.com/deanishe/alfred-workflow/actions?query=workflow%3Abuild 3 | .. image:: https://coveralls.io/repos/github/deanishe/alfred-workflow/badge.svg?branch=master 4 | :target: https://coveralls.io/github/deanishe/alfred-workflow 5 | .. image:: https://img.shields.io/pypi/status/Alfred-Workflow.svg?style=flat 6 | :target: https://pypi.python.org/pypi/Alfred-Workflow/ 7 | .. image:: https://img.shields.io/pypi/v/Alfred-Workflow.svg?style=flat 8 | :target: https://pypi.python.org/pypi/Alfred-Workflow/ 9 | .. image:: https://img.shields.io/pypi/pyversions/Alfred-Workflow.svg?style=flat 10 | :target: https://pypi.python.org/pypi/Alfred-Workflow/ 11 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _contributing: 3 | 4 | ============ 5 | Contributing 6 | ============ 7 | 8 | Alfred-Workflow is an open-source project and contributions are welcome. 9 | 10 | .. important:: 11 | 12 | **Do not submit yet another feature request for Python 3 support** 13 | 14 | I am aware of the existence of Python 3. There will be a rewrite of the library that removes all the crufy and only supports Python 3 when I get around to it. 15 | 16 | 17 | .. _bugs: 18 | 19 | Feature requests and bugs 20 | ========================= 21 | 22 | If you have a bug report or a feature request, please create a new 23 | `issue on GitHub`_. 24 | 25 | 26 | .. _pull-requests: 27 | 28 | Pull requests 29 | ============= 30 | 31 | If you'd like to submit a pull request, please observe the following: 32 | 33 | - Alfred-Workflow has very close to 100% test coverage. "Proof-of-concept" 34 | pull requests without tests are welcome. However, please be prepared 35 | to add the appropriate tests if you want your pull request to be ultimately 36 | accepted. 37 | - Complete coverage is *only a proxy* for decent tests. Tests should also 38 | cover a decent variety of valid/invalid input. For example, if the code 39 | *could potentially* be handed non-ASCII input, it should be tested with 40 | non-ASCII input. 41 | - Code should be `PEP8`_-compliant as far as is reasonable. Any decent code 42 | editor has a PEP8 plugin that will warn you of potential transgressions. 43 | - Please choose your function, method and argument names carefully, with an 44 | eye to the existing names. Obviousness is more important than brevity. 45 | - Document your code using the `Sphinx ReST format`_. Even if your 46 | function/method isn't user-facing, some other developer will be looking at 47 | it. Even if it's only a one-liner, the developer may be looking at 48 | :ref:`the API docs ` in a browser, not at the source code. 49 | If you don't feel comfortable writing English, I'd be happy to write the 50 | docs for you, but please ensure the code is easily understandable (i.e. comment the code if it's not totally obvious). 51 | - Performance counts. By default, Alfred will try to run a workflow anew on 52 | every keypress. As a rule, 0.3 seconds execution time is decent, 0.2 53 | seconds or less is smooth. Alfred-Workflow should do its utmost to 54 | consume as little of that time as possible. 55 | 56 | The main entry point for unit testing is the ``run-tests.sh`` script in the root directory. This will fail *if code coverage is < 100%*. Travis-CI and GitHub Actions also use this script. Add ``# pragma: no cover`` with care. 57 | 58 | 59 | .. _unit-tests: 60 | 61 | Unit tests 62 | ========== 63 | 64 | Alfred-Workflow includes a full suite of unit tests. Please use the 65 | ``run-tests.sh`` script in the root directory of the repo to run the unit tests: it creates the necessary test environment to run the unit tests. 66 | ``test_workflow.py`` *will* fail if not run via ``run-scripts.sh``, but the test suites for the other modules may also be run directly. 67 | 68 | Moreover, ``run-tests.sh`` checks the coverage of the unit tests and will fail if it is below 100%. 69 | 70 | 71 | .. _questions: 72 | 73 | Questions and help 74 | ================== 75 | 76 | If you have feedback or a question regarding Alfred-Workflow, please post in 77 | the `Alfred forum thread`_. 78 | 79 | 80 | 81 | .. _Alfred forum thread: http://www.alfredforum.com/topic/4031-workflow-library-for-python/ 82 | .. _GitHub: https://github.com/deanishe/alfred-workflow/ 83 | .. _Python Package Index: https://pypi.python.org/pypi/Alfred-Workflow 84 | .. _issue on GitHub: https://github.com/deanishe/alfred-workflow/issues 85 | .. _pip: https://pypi.python.org/pypi/pip 86 | .. _PEP8: http://legacy.python.org/dev/peps/pep-0008/ 87 | .. _Sphinx ReST format: http://sphinx-doc.org/ 88 | -------------------------------------------------------------------------------- /docs/dash.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _dash-docset: 3 | 4 | =========== 5 | Dash docset 6 | =========== 7 | 8 | This documentation is also available as a docset for `Dash`_, an amazing 9 | documentation app for macOS with native support for Alfred. 10 | 11 | Grab the `docset from the GitHub repo`_. 12 | 13 | 14 | .. _Dash: https://kapeli.com/dash 15 | .. _docset from the GitHub repo: https://github.com/deanishe/alfred-workflow/raw/master/docs/Alfred-Workflow.docset.zip 16 | -------------------------------------------------------------------------------- /docs/guide/background.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _background-processes: 3 | 4 | ==================== 5 | Background processes 6 | ==================== 7 | 8 | .. currentmodule:: workflow.background 9 | 10 | Many workflows provide a convenient interface to applications and/or web services. 11 | 12 | For performance reasons, it's common for workflows to cache data locally, but 13 | updating this cache typically takes a few seconds, making your workflow 14 | unresponsive while an update is occurring, which is very un-Alfred-like. 15 | 16 | To avoid such delays, Alfred-Workflow provides the :mod:`~workflow.background` 17 | module to allow you to easily run scripts in the background. 18 | 19 | There are two functions, :func:`run_in_background` and :func:`is_running`, 20 | that provide the main interface. The processes started are full daemon 21 | processes, so you can start real servers as easily as simple scripts. 22 | 23 | Here's an example of a common usage pattern (updating cached data in the 24 | background). What we're doing is: 25 | 26 | 1. Checking the age of the cached data and running the update script via 27 | :func:`run_in_background` if the cached data are too old or don't exist. 28 | 2. (Optionally) informing the user that data are being updated. 29 | 3. Loading the cached data regardless of age. 30 | 4. Displaying the cached data (if any). 31 | 32 | .. code-block:: python 33 | :linenos: 34 | 35 | from workflow import Workflow3, ICON_INFO 36 | from workflow.background import run_in_background, is_running 37 | 38 | def main(wf): 39 | # Is cache over 1 hour old or non-existent? 40 | if not wf.cached_data_fresh('exchange-rates', 3600): 41 | run_in_background('update', 42 | ['/usr/bin/python', 43 | wf.workflowfile('update_exchange_rates.py')]) 44 | 45 | if is_running('update'): 46 | # Tell Alfred to run the script again every 0.5 seconds 47 | # until the `update` job is complete (and Alfred is 48 | # showing results based on the newly-retrieved data) 49 | wf.rerun = 0.5 50 | # Add a notification if the script is running 51 | wf.add_item('Updating exchange rates...', icon=ICON_INFO) 52 | 53 | # max_age=0 will load any cached data regardless of age 54 | exchange_rates = wf.cached_data('exchage-rates', max_age=0) 55 | 56 | # Display (possibly stale) cache data 57 | if exchange_rates: 58 | for rate in exchange_rates: 59 | wf.add_item(rate) 60 | 61 | # Send results to Alfred 62 | wf.send_feedback() 63 | 64 | if __name__ == '__main__': 65 | # Use Workflow3 so we can use Alfred 3's awesome `rerun` feature 66 | wf = Workflow3() 67 | wf.run(main) 68 | 69 | 70 | For a working example, see 71 | :ref:`Part 2 of the Tutorial ` or the 72 | `source code `_ 73 | of my `Git Repos `_ workflow, 74 | which is a bit smarter about showing the user update information. 75 | -------------------------------------------------------------------------------- /docs/guide/icons.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _icons: 3 | 4 | ============ 5 | System icons 6 | ============ 7 | 8 | The :mod:`~workflow.workflow` module provides access to a number of default 9 | macOS icons via ``ICON_*`` constants for use when generating Alfred feedback: 10 | 11 | .. code-block:: python 12 | :linenos: 13 | 14 | from workflow import Workflow, ICON_INFO 15 | 16 | wf = Workflow() 17 | wf.add_item('For your information', icon=ICON_INFO) 18 | wf.send_feedback() 19 | 20 | 21 | .. _icon-list: 22 | 23 | List of icons 24 | ============= 25 | 26 | These are all the icons accessible in :mod:`~workflow.workflow`. They (and 27 | more) can be found in 28 | ``/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/``. 29 | 30 | +-------------------+----------------------------------------+ 31 | | Name | Preview | 32 | +===================+========================================+ 33 | |``ICON_ACCOUNT`` |.. image:: ../_static/ICON_ACCOUNT.png | 34 | +-------------------+----------------------------------------+ 35 | |``ICON_BURN`` |.. image:: ../_static/ICON_BURN.png | 36 | +-------------------+----------------------------------------+ 37 | |``ICON_CLOCK`` |.. image:: ../_static/ICON_CLOCK.png | 38 | +-------------------+----------------------------------------+ 39 | |``ICON_COLOR`` |.. image:: ../_static/ICON_COLOR.png | 40 | +-------------------+----------------------------------------+ 41 | |``ICON_COLOUR`` |.. image:: ../_static/ICON_COLOUR.png | 42 | +-------------------+----------------------------------------+ 43 | |``ICON_EJECT`` |.. image:: ../_static/ICON_EJECT.png | 44 | +-------------------+----------------------------------------+ 45 | |``ICON_ERROR`` |.. image:: ../_static/ICON_ERROR.png | 46 | +-------------------+----------------------------------------+ 47 | |``ICON_FAVORITE`` |.. image:: ../_static/ICON_FAVORITE.png | 48 | +-------------------+----------------------------------------+ 49 | |``ICON_FAVOURITE`` |.. image:: ../_static/ICON_FAVOURITE.png| 50 | +-------------------+----------------------------------------+ 51 | |``ICON_GROUP`` |.. image:: ../_static/ICON_GROUP.png | 52 | +-------------------+----------------------------------------+ 53 | |``ICON_HELP`` |.. image:: ../_static/ICON_HELP.png | 54 | +-------------------+----------------------------------------+ 55 | |``ICON_HOME`` |.. image:: ../_static/ICON_HOME.png | 56 | +-------------------+----------------------------------------+ 57 | |``ICON_INFO`` |.. image:: ../_static/ICON_INFO.png | 58 | +-------------------+----------------------------------------+ 59 | |``ICON_NETWORK`` |.. image:: ../_static/ICON_NETWORK.png | 60 | +-------------------+----------------------------------------+ 61 | |``ICON_NOTE`` |.. image:: ../_static/ICON_NOTE.png | 62 | +-------------------+----------------------------------------+ 63 | |``ICON_SETTINGS`` |.. image:: ../_static/ICON_SETTINGS.png | 64 | +-------------------+----------------------------------------+ 65 | |``ICON_SWIRL`` |.. image:: ../_static/ICON_SWIRL.png | 66 | +-------------------+----------------------------------------+ 67 | |``ICON_SWITCH`` |.. image:: ../_static/ICON_SWITCH.png | 68 | +-------------------+----------------------------------------+ 69 | |``ICON_SYNC`` |.. image:: ../_static/ICON_SYNC.png | 70 | +-------------------+----------------------------------------+ 71 | |``ICON_TRASH`` |.. image:: ../_static/ICON_TRASH.png | 72 | +-------------------+----------------------------------------+ 73 | |``ICON_USER`` |.. image:: ../_static/ICON_USER.png | 74 | +-------------------+----------------------------------------+ 75 | |``ICON_WARNING`` |.. image:: ../_static/ICON_WARNING.png | 76 | +-------------------+----------------------------------------+ 77 | |``ICON_WEB`` |.. image:: ../_static/ICON_WEB.png | 78 | +-------------------+----------------------------------------+ 79 | 80 | If you'd like other standard macOS icons to be added, please 81 | `add an issue on GitHub`_. 82 | 83 | .. _add an issue on GitHub: https://github.com/deanishe/alfred-workflow/issues 84 | -------------------------------------------------------------------------------- /docs/guide/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _user-manual: 3 | 4 | ========== 5 | User Guide 6 | ========== 7 | 8 | This section describes how to use the features of Alfred-Workflow. 9 | 10 | If you're new to writing workflows or coding in general, start with the 11 | :ref:`Tutorial `. 12 | 13 | .. tip:: 14 | 15 | If you're writing a workflow that uses data from the system 16 | (e.g. from files/the filesystem or via command-line programs 17 | called via :mod:`subprocess`), please read :ref:`text-encoding`, 18 | which describes how to handle data from sources other than 19 | Alfred-Workflow's APIs. 20 | 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | setup 26 | third-party 27 | persistent-data 28 | filtering 29 | web 30 | notifications 31 | background 32 | update 33 | versioning 34 | icons 35 | magic-arguments 36 | variables 37 | serialization 38 | rerun 39 | text-encoding 40 | -------------------------------------------------------------------------------- /docs/guide/magic-arguments.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _magic-arguments: 3 | 4 | ================= 5 | "Magic" arguments 6 | ================= 7 | 8 | .. contents:: 9 | :local: 10 | 11 | 12 | If your Script Filter (or script) accepts a query (or command line arguments), 13 | you can pass it so-called magic arguments that instruct 14 | :class:`~workflow.workflow.Workflow` to perform certain actions, such as 15 | opening the log file or clearing the cache/settings. 16 | 17 | These can be a big help while developing and debugging and especially when 18 | debugging problems your Workflow's users may be having. 19 | 20 | The :meth:`Workflow.run() <~workflow.workflow.Workflow.run>` method 21 | (which you should "wrap" your Workflow's entry functions in) will catch any 22 | raised exceptions, log them and display them in Alfred. You can call your 23 | Workflow with ``workflow:openlog`` as an Alfred query/command line argument 24 | and :class:`~workflow.workflow.Workflow` will open the Workflow's log file 25 | in the default app (usually **Console.app**). 26 | 27 | This makes it easy for you to get at the log file and data and cache directories 28 | (hidden away in ``~/Library``), and for your users to send you their logs 29 | for debugging. 30 | 31 | .. note:: 32 | 33 | Magic arguments will only work with scripts that accept arguments *and* use 34 | the :attr:`~workflow.workflow.Workflow.args` property (where magic 35 | arguments are parsed). 36 | 37 | :class:`~workflow.workflow.Workflow` supports the following magic arguments by default: 38 | 39 | - ``workflow:magic`` — List available magic arguments. 40 | - ``workflow:help`` — Open workflow's help URL in default web browser. This URL is specified in the ``help_url`` argument to :class:`~workflow.workflow.Workflow`. 41 | - ``workflow:version`` — Display the installed version of the workflow (if one is set). 42 | - ``workflow:delcache`` — Delete the Workflow's cache. 43 | - ``workflow:deldata`` — Delete the Workflow's saved data. 44 | - ``workflow:delsettings`` — Delete the Workflow's settings file (which contains the data stored using :attr:`Workflow.settings `). 45 | - ``workflow:foldingdefault`` — Reset diacritic folding to workflow default 46 | - ``workflow:foldingoff`` — Never fold diacritics in search keys 47 | - ``workflow:foldingon`` — Force diacritic folding in search keys (e.g. convert *ü* to *ue*) 48 | - ``workflow:opencache`` — Open the Workflow's cache directory. 49 | - ``workflow:opendata`` — Open the Workflow's data directory. 50 | - ``workflow:openlog`` — Open the Workflow's log file in the default app. 51 | - ``workflow:openterm`` — Open a Terminal window in the Workflow's root directory. 52 | - ``workflow:openworkflow`` — Open the Workflow's root directory (where ``info.plist`` is). 53 | - ``workflow:reset`` — Delete the Workflow's settings, cache and saved data. 54 | - ``workflow:update`` — Check for a newer version of the workflow using GitHub releases and install the newer version if one is available. 55 | - ``workflow:noautoupdate`` — Turn off automatic checks for updates. 56 | - ``workflow:autoupdate`` — Turn automatic checks for updates on. 57 | - ``workflow:prereleases`` — Enable updating the workflow to pre-release versions. 58 | - ``workflow:noprereleases`` — Disable updating the workflow to pre-release versions (default). 59 | 60 | The three ``workflow:folding…`` settings allow users to override the diacritic 61 | folding set by a workflow's author. This may be useful if the author's choice 62 | does not correspond with a user's usage pattern. 63 | 64 | You can turn off magic arguments by passing ``capture_args=False`` to 65 | :class:`~workflow.workflow.Workflow` on instantiation, or call the corresponding methods of :class:`~workflow.workflow.Workflow` directly, 66 | perhaps assigning your own keywords within your Workflow: 67 | 68 | - :meth:`~workflow.workflow.Workflow.open_help` 69 | - :meth:`~workflow.workflow.Workflow.open_log` 70 | - :meth:`~workflow.workflow.Workflow.open_cachedir` 71 | - :meth:`~workflow.workflow.Workflow.open_datadir` 72 | - :meth:`~workflow.workflow.Workflow.open_workflowdir` 73 | - :meth:`~workflow.workflow.Workflow.open_terminal` 74 | - :meth:`~workflow.workflow.Workflow.clear_cache` 75 | - :meth:`~workflow.workflow.Workflow.clear_data` 76 | - :meth:`~workflow.workflow.Workflow.clear_settings` 77 | - :meth:`~workflow.workflow.Workflow.reset` (a shortcut to call the three previous ``clear_*`` methods) 78 | - :meth:`~workflow.workflow.Workflow.check_update` 79 | - :meth:`~workflow.workflow.Workflow.start_update` 80 | 81 | .. _custom-magic: 82 | 83 | Customising magic arguments 84 | =========================== 85 | 86 | The default prefix for magic arguments (``workflow:``) is contained in the 87 | :attr:`~workflow.workflow.Workflow.magic_prefix` attribute of 88 | :class:`~workflow.workflow.Workflow`. If you want to change it to, say, 89 | ``wf:`` (which will become the default in v2 of Alfred-Workflow), simply 90 | reassign it:: 91 | 92 | wf.magic_prefix = 'wf:' 93 | 94 | The magic arguments are defined in the :attr:`Workflow.magic_arguments ` dictionary. 95 | The dictionary keys are the keywords for the arguments (without the 96 | prefix) and the values are functions that should be called when the magic 97 | argument is entered. You can show a message in Alfred by returning a 98 | ``unicode`` string from the function. 99 | 100 | To add a new magic argument that opens the workflow's settings file, you 101 | could do: 102 | 103 | .. code-block:: python 104 | :linenos: 105 | 106 | wf = Workflow() 107 | wf.magic_prefix = 'wf:' # Change prefix to `wf:` 108 | 109 | def opensettings(): 110 | subprocess.call(['open', wf.settings_path]) 111 | return 'Opening workflow settings...' 112 | 113 | wf.magic_arguments['settings'] = opensettings 114 | 115 | Now entering ``wf:settings`` as your workflow's query in Alfred will 116 | open ``settings.json`` in the default application. 117 | -------------------------------------------------------------------------------- /docs/guide/notifications.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _notifications: 3 | 4 | ============= 5 | Notifications 6 | ============= 7 | 8 | .. versionadded:: 1.15 9 | 10 | .. note:: 11 | 12 | Notifications are only available on Mountain Lion (10.8) and later. Calling 13 | :func:`~workflow.notify.notify` on earlier systems will silently fail. 14 | 15 | Alfred 2 allows you to post notifications, but only at the end of 16 | a workflow, and only with its own icon. 17 | 18 | Alfred-Workflow's :mod:`~workflow.notify` module lets you post notifications 19 | whenever you want, and with your workflow's icon. 20 | 21 | 22 | Usage 23 | ===== 24 | 25 | .. code-block:: python 26 | :linenos: 27 | 28 | from workflow.notify import notify 29 | 30 | notify('My Title', 'My Text') 31 | 32 | This is the full API: 33 | 34 | .. autofunction:: workflow.notify.notify 35 | :noindex: 36 | 37 | See the :ref:`API documentation ` for details of the other 38 | functions. 39 | -------------------------------------------------------------------------------- /docs/guide/rerun.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _guide-rerun: 3 | 4 | ========================== 5 | Re-running a Script Filter 6 | ========================== 7 | 8 | .. versionadded:: 1.23 9 | 10 | Alfred 3.2 introduced an new "rerun" feature. Emitting a ``rerun`` value 11 | from a Script Filter tells Alfred to run that script again after the 12 | specified number of seconds. Alfred will also pass back any variables 13 | set at the top-level of your feedback, i.e. via the 14 | :meth:`Workflow3.setvar() <~workflow.Workflow3>` method. 15 | 16 | Set :attr:`Workflow3.rerun <~workflow.Workflow3.rerun>` to instruct 17 | Alfred to re-run your Script Filter. 18 | 19 | This could be used, for example, to provide a seamless (to the user) status 20 | update in a workflow that reports on the status of a download client like 21 | `aria2`_ via its API. 22 | 23 | The refresh is invisible to the user, as Alfred doesn't update its UI unless 24 | the Script Filter output changes. 25 | 26 | In terms of Alfred-Workflow more generally, you might use this feature to 27 | re-run your Script Filter if its data are still being updated in the background. 28 | For example: 29 | 30 | 31 | .. code-block:: python 32 | :linenos: 33 | 34 | from workflow import Workflow3 35 | from workflow.background import is_running, run_in_background 36 | 37 | wf = Workflow3() 38 | 39 | # Unconditionally load data from cache 40 | data = wf.cached_data('data', ..., max_age=0) 41 | 42 | # Update cached data if they're stale 43 | if not wf.cached_data_fresh('data', max_age=30): 44 | run_in_background('update_cache', ...) 45 | 46 | # Tell Alfred to re-run the Script Filter if the cache is 47 | # currently being updated. 48 | if is_running('update_cache'): 49 | wf.rerun = 1 50 | 51 | # Show (stale) cached data 52 | for d in data: 53 | wf.add_item(**d) 54 | 55 | wf.send_feedback() 56 | 57 | 58 | In this way, the results shown to the user will be updated only if the 59 | background update changes the cached data. 60 | 61 | 62 | .. _aria2: https://aria2.github.io 63 | -------------------------------------------------------------------------------- /docs/guide/serialization.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _guide-serialization: 3 | 4 | =================================== 5 | Serialization of stored/cached data 6 | =================================== 7 | 8 | .. contents:: 9 | :local: 10 | 11 | .. currentmodule:: workflow 12 | 13 | By default, both cache and data files (created using the APIs described in 14 | :ref:`guide-persistent-data`) are cached using :mod:`cPickle`. This provides 15 | a great compromise in terms of speed and the ability to store arbitrary objects. 16 | 17 | When changing or specifying a serializer, use the name under which the 18 | serializer is registered with the :ref:`workflow.manager ` 19 | object. 20 | 21 | .. warning:: 22 | 23 | When it comes to cache data, it is *strongly recommended* to stick with the 24 | default. :mod:`cPickle` is *very* fast and fully supports standard Python 25 | data structures (``dict``, ``list``, ``tuple``, ``set`` etc.). 26 | 27 | If you really must customise the cache data format, you can change the 28 | default cache serialization format to :mod:`pickle` thus: 29 | 30 | .. code-block:: python 31 | :linenos: 32 | 33 | wf = Workflow() 34 | wf.cache_serializer = 'pickle' 35 | 36 | Unlike the stored data API, the cached data API can't determine the format 37 | of the cached data. If you change the serializer without clearing the 38 | cache, errors will probably result as the serializer tries to load data 39 | in a foreign format. 40 | 41 | In the case of stored data, you are free to specify either a global default 42 | serializer or one for each individual datastore: 43 | 44 | .. code-block:: python 45 | :linenos: 46 | 47 | wf = Workflow() 48 | # Use `pickle` as the global default serializer 49 | wf.data_serializer = 'pickle' 50 | 51 | # Use the JSON serializer only for these data 52 | wf.store_data('name', data, serializer='json') 53 | 54 | This is primarily so you can create files that are human-readable or useable 55 | by other software. The generated JSON is formatted to make it readable. 56 | 57 | The :meth:`stored_data() ` method can 58 | automatically determine the serialization of the stored data (based on the file 59 | extension, which is the same as the name the serializer is registered under), 60 | provided the corresponding serializer is registered. If it isn't, a 61 | :class:`ValueError` will be raised. 62 | 63 | 64 | Built-in serializers 65 | ==================== 66 | 67 | There are 3 built-in, pre-configured serializers: 68 | 69 | - :class:`cpickle ` — the default serializer 70 | for both cached and stored data, with very good support for native Python 71 | data types; 72 | - :class:`pickle ` — a more flexible, but 73 | much slower alternative to ``cpickle``; and 74 | - :class:`json ` — a very common data format, 75 | but with limited support for native Python data types. 76 | 77 | See the built-in :mod:`cPickle`, :mod:`pickle` and :mod:`json` libraries for 78 | more information on the serialization formats. 79 | 80 | 81 | .. _managing-serializers: 82 | 83 | Managing serializers 84 | ==================== 85 | 86 | You can add your own serializer, or replace the built-in ones, using the 87 | configured instance of :class:`~workflow.SerializerManager` at 88 | ``workflow.manager``, e.g. ``from workflow import manager``. 89 | 90 | A ``serializer`` object must have ``load()`` and ``dump()`` methods that work 91 | the same way as in the built-in :mod:`json` and :mod:`pickle` libraries, i.e.: 92 | 93 | .. code-block:: python 94 | :linenos: 95 | 96 | # Reading 97 | obj = serializer.load(open('filename', 'rb')) 98 | # Writing 99 | serializer.dump(obj, open('filename', 'wb')) 100 | 101 | To register a new serializer, call the 102 | :meth:`~workflow.workflow.SerializerManager.register` method of the 103 | ``workflow.manager`` object with the name of the serializer and the object 104 | that performs serialization: 105 | 106 | .. code-block:: python 107 | :linenos: 108 | :emphasize-lines: 14 109 | 110 | from workflow import Workflow, manager 111 | 112 | 113 | class MySerializer(object): 114 | 115 | @classmethod 116 | def load(cls, file_obj): 117 | # load data from file_obj 118 | 119 | @classmethod 120 | def dump(cls, obj, file_obj): 121 | # serialize obj to file_obj 122 | 123 | manager.register('myformat', MySerializer()) 124 | 125 | .. note:: 126 | 127 | The name you specify for your serializer will be the file extension of the 128 | stored files. 129 | 130 | 131 | Serializer interface 132 | ==================== 133 | 134 | A serializer **must** conform to this interface (like :mod:`json` and 135 | :mod:`pickle`): 136 | 137 | .. code-block:: python 138 | :linenos: 139 | 140 | serializer.load(file_obj) 141 | serializer.dump(obj, file_obj) 142 | 143 | 144 | See the :ref:`api-serializers` section of the API documentation for more 145 | information. 146 | -------------------------------------------------------------------------------- /docs/guide/setup.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _setup: 3 | 4 | =========================== 5 | Workflow setup and skeleton 6 | =========================== 7 | 8 | Alfred-Workflow is aimed particularly at authors of so-called 9 | **Script Filters**. These are activated by a keyword in Alfred, receive 10 | user input and return results to Alfred. 11 | 12 | To write a Script Filter with Alfred-Workflow, make sure your Script Filter 13 | is set to use ``/bin/bash`` as the **Language**, and select the 14 | following (and only the following) **Escaping** options: 15 | 16 | - Backquotes 17 | - Double Quotes 18 | - Dollars 19 | - Backslashes 20 | 21 | The **Script** field should contain the following:: 22 | 23 | /usr/bin/python yourscript.py "{query}" 24 | 25 | 26 | where ``yourscript.py`` is the name of your script [#]_. 27 | 28 | Your workflow should start out like this. This enables :class:`Workflow` 29 | to capture any errors thrown by your scripts: 30 | 31 | .. code-block:: python 32 | :linenos: 33 | 34 | #!/usr/bin/python 35 | # encoding: utf-8 36 | 37 | import sys 38 | 39 | from workflow import Workflow 40 | 41 | log = None 42 | 43 | 44 | def main(wf): 45 | # The Workflow instance will be passed to the function 46 | # you call from `Workflow.run` 47 | 48 | # Your imports here if you want to catch import errors 49 | import somemodule 50 | import anothermodule 51 | 52 | # Get args from Workflow as normalized Unicode 53 | args = wf.args 54 | 55 | # Do stuff here ... 56 | 57 | # Add an item to Alfred feedback 58 | wf.add_item('Item title', 'Item subtitle') 59 | 60 | # Send output to Alfred 61 | wf.send_feedback() 62 | 63 | 64 | if __name__ == '__main__': 65 | wf = Workflow() 66 | # Assign Workflow logger to a global variable for convenience 67 | log = wf.logger 68 | sys.exit(wf.run(main)) 69 | 70 | 71 | .. [#] It's better to specify ``/usr/bin/python`` over just ``python``. This 72 | ensures that the script will always be run with the system default 73 | Python regardless of what ``PATH`` might be. -------------------------------------------------------------------------------- /docs/guide/third-party.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _thirdparty: 3 | 4 | ============================= 5 | Including 3rd party libraries 6 | ============================= 7 | 8 | It's a Very Bad Idea™ to install (or ask users to install) 3rd-party 9 | libraries in the macOS system Python. Alfred-Workflow makes it easy to 10 | include them in your Workflow. 11 | 12 | Simply create a ``lib`` subdirectory under your Workflow's root directory 13 | and install your dependencies there. You can call the directory whatever you 14 | want, but in the following explanation, I'll assume you used ``lib``. 15 | 16 | To install libraries in your dependencies directory, use: 17 | 18 | .. code-block:: bash 19 | 20 | pip install --target=path/to/my/workflow/lib python-lib-name 21 | 22 | The path you pass as the ``--target`` argument should be the path to 23 | the directory under your Workflow's root directory in which you want to install 24 | your libraries. ``python-lib-name`` should be the "pip name" (i.e. the name the 25 | library has on `PyPI `_) of the library you want 26 | to install, e.g. ``requests`` or ``feedparser``. 27 | 28 | This name is usually, but not always, the same as the name you use with ``import``. 29 | 30 | For example, to install Alfred-Workflow, you would run 31 | ``pip install Alfred-Workflow`` but use ``import workflow`` to import it. 32 | 33 | **An example:** You're in a shell in Terminal.app in the Workflow's root directory 34 | and you're using ``lib`` as the directory for your Python libraries. You want to 35 | install `requests `_. You would run: 36 | 37 | .. code-block:: bash 38 | 39 | pip install --target=lib requests 40 | 41 | This will install the ``requests`` library into the ``lib`` subdirectory of the 42 | current working directory. 43 | 44 | Then you instantiate :class:`Workflow ` 45 | with the ``libraries`` argument: 46 | 47 | .. code-block:: python 48 | :linenos: 49 | 50 | from workflow import Workflow 51 | 52 | def main(wf): 53 | import requests # Imported from ./lib 54 | 55 | if __name__ == '__main__': 56 | wf = Workflow(libraries=['./lib']) 57 | sys.exit(wf.run(main)) 58 | 59 | When using this feature you **do not** need to create an ``__init__.py`` file in 60 | the ``lib`` subdirectory. ``Workflow(…, libraries=['./lib'])`` and creating 61 | ``./lib/__init__.py`` are effectively equal alternatives. 62 | 63 | Instead of using ``Workflow(…, libraries=['./lib'])``, you can add an empty 64 | ``__init__.py`` file to your ``lib`` subdirectory and import the libraries 65 | installed therein using: 66 | 67 | .. code-block:: python 68 | 69 | from lib import requests 70 | 71 | instead of simply: 72 | 73 | 74 | .. code-block:: python 75 | 76 | import requests 77 | 78 | -------------------------------------------------------------------------------- /docs/guide/web.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _guide-web: 3 | 4 | ============================ 5 | Retrieving data from the web 6 | ============================ 7 | 8 | The `unit tests`_ in the source repository contain examples of pretty 9 | much everything :mod:`workflow.web` can do: 10 | 11 | * `GET`_ and `POST`_ variables 12 | * `Retrieve and decode JSON`_ 13 | * `Post JSON`_ 14 | * `Post forms`_ 15 | * Automatically handle encoding for `HTML`_ and `XML`_ 16 | * `Basic authentication`_ 17 | * File uploads `with forms`_ and `without forms`_ 18 | * `Download large files`_ 19 | * `Variable timeouts`_ 20 | * `Ignore redirects`_ 21 | 22 | See the :ref:`API documentation ` for more information. 23 | 24 | .. _GET: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L128 25 | .. _POST: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L76 26 | .. _unit tests: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py 27 | .. _Ignore redirects: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L67 28 | .. _Variable timeouts: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L101 29 | .. _Post forms: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L76 30 | .. _Post JSON: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L86 31 | .. _HTML: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L106 32 | .. _XML: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L121 33 | .. _Basic authentication: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L137 34 | .. _with forms: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L153 35 | .. _without forms: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L173 36 | .. _Retrieve and decode JSON: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L189 37 | .. _Download large files: https://github.com/deanishe/alfred-workflow/blob/fdc7c001c2cb76a41aee3e5a755486a977a36b20/tests/test_web.py#L197 38 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _overview: 3 | 4 | ========================== 5 | Welcome to Alfred-Workflow 6 | ========================== 7 | 8 | .. include:: badges.rst.inc 9 | 10 | Go to :ref:`quickindex`. 11 | 12 | 13 | Alfred-Workflow is a Python helper library for `Alfred 2, 3 and 4`_ workflow 14 | authors, developed and hosted on `GitHub`_. 15 | 16 | Alfred workflows typically take user input, fetch data from the Web or 17 | elsewhere, filter them and display results to the user. Alfred-Workflow takes 18 | care of a lot of the details for you, allowing you to concentrate your efforts 19 | on your workflow's functionality. 20 | 21 | Alfred-Workflow supports macOS 10.7+ (Python 2.7). 22 | 23 | 24 | Features 25 | ======== 26 | 27 | - Fuzzy, :ref:`Alfred-like search/filtering ` with 28 | :ref:`diacritic folding ` 29 | - :ref:`Simple, persistent settings ` 30 | - Simple, auto-expiring :ref:`data caching ` 31 | - :ref:`Keychain support ` for secure storage (and syncing) of 32 | passwords, API keys etc. 33 | - Simple generation of Alfred feedback (XML and JSON) 34 | - :ref:`Lightweight web ` API with `requests`_-like interface 35 | - Easily launch :ref:`background tasks ` (daemons) to 36 | keep your workflow responsive 37 | - :ref:`Check for and install new workflow versions ` using 38 | GitHub releases 39 | - :ref:`Post notifications ` with Notification Center 40 | (10.8+ only) 41 | - Error handling and logging for easier development and support 42 | - :ref:`"Magic" arguments ` to help development, 43 | debugging and management of the workflow 44 | 45 | 46 | Alfred 3+ features 47 | ------------------ 48 | 49 | - Set :ref:`workflows variables ` from code 50 | - Advanced modifiers 51 | - Alfred version-aware updates (ignores incompatible updates) 52 | - :ref:`Automatic re-running of Script Filters `. 53 | 54 | 55 | Quick example 56 | ============= 57 | 58 | Here's how to show recent `Pinboard.in `_ posts in Alfred. 59 | 60 | Create a new workflow in Alfred's preferences. Add a **Script Filter** with 61 | Language ``/usr/bin/python`` and paste the following into the **Script** 62 | box (changing ``API_KEY``): 63 | 64 | .. code-block:: python 65 | :linenos: 66 | :emphasize-lines: 4 67 | 68 | import sys 69 | from workflow import Workflow, ICON_WEB, web 70 | # To use Alfred 3+ feedback mechanism: 71 | # from workflow import Workflow3 72 | 73 | API_KEY = 'your-pinboard-api-key' 74 | 75 | def main(wf): 76 | url = 'https://api.pinboard.in/v1/posts/recent' 77 | params = dict(auth_token=API_KEY, count=20, format='json') 78 | r = web.get(url, params) 79 | r.raise_for_status() 80 | for post in r.json()['posts']: 81 | wf.add_item(post['description'], post['href'], arg=post['href'], 82 | uid=post['hash'], valid=True, icon=ICON_WEB) 83 | wf.send_feedback() 84 | 85 | 86 | if __name__ == u"__main__": 87 | wf = Workflow() 88 | sys.exit(wf.run(main)) 89 | 90 | 91 | Add an **Open URL** action to your workflow with ``{query}`` as the **URL**, 92 | connect your **Script Filter** to it, and you can now hit **ENTER** on a 93 | Pinboard item in Alfred to open it in your browser. 94 | 95 | .. warning:: 96 | 97 | Using the above example code as a workflow will likely get 98 | you banned by the Pinboard API. See the :ref:`tutorial` if you want to 99 | build an API terms-compliant (and super-fast) Pinboard workflow. 100 | 101 | 102 | .. include:: toc.rst.inc 103 | 104 | 105 | 106 | .. _GitHub: https://github.com/deanishe/alfred-workflow/ 107 | .. _requests: http://docs.python-requests.org/en/latest/ 108 | .. _Alfred 2, 3 and 4: https://www.alfredapp.com/ 109 | -------------------------------------------------------------------------------- /docs/indices.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _indices: 3 | 4 | Indices 5 | ======= 6 | 7 | Quick Index 8 | ^^^^^^^^^^^ 9 | 10 | The :ref:`quickindex` contains links to the most useful parts of the 11 | documentation. Useful on a phone or in Dash, when you can't see the 12 | normal navigation. 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | quickindex 18 | 19 | 20 | Full Index 21 | ^^^^^^^^^^ 22 | 23 | :ref:`genindex` containing *all* functions, classes and methods. 24 | 25 | 26 | Module Index 27 | ^^^^^^^^^^^^ 28 | 29 | The :ref:`modindex` contains links to all the modules in Alfred-Workflow. 30 | 31 | Search 32 | ^^^^^^ 33 | 34 | You can search the documentation on the :ref:`search`. 35 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _installation: 3 | 4 | ============ 5 | Installation 6 | ============ 7 | 8 | Alfred-Workflow can be installed from the `Python Package Index`_ with 9 | `pip`_ or from the source code on `GitHub`_. 10 | 11 | .. tip:: 12 | 13 | If you don't know how to create a new workflow and add files to it, 14 | have a look at :ref:`the tutorial `. 15 | 16 | 17 | .. _installation-pip: 18 | 19 | pip / PyPi 20 | ========== 21 | 22 | You can install Alfred-Workflow directly into your workflow with:: 23 | 24 | pip install --target=/path/to/my/workflow Alfred-Workflow 25 | 26 | 27 | .. important:: 28 | 29 | If you intend to distribute your workflow to other users, you should 30 | include Alfred-Workflow (and other non-standard Python libraries your 31 | workflow requires) within your workflow as described above. **Do not** ask 32 | users to install anything into their system Python. That way lies broken 33 | software. 34 | 35 | .. _installation-github: 36 | 37 | GitHub 38 | ====== 39 | 40 | Download the ``alfred-workflow-X.X.X.zip`` file from the `GitHub releases`_ 41 | page and extract the ZIP to the root directory of your workflow (where 42 | ``info.plist`` is). 43 | 44 | 45 | Alternatively, you can download `the source code`_ from the 46 | `GitHub repository`_ and copy the ``workflow`` subfolder to the root directory 47 | of your workflow. 48 | 49 | Your Workflow directory should look something like this (where 50 | ``yourscript.py`` contains your workflow code and ``info.plist`` is 51 | the workflow information file generated by Alfred):: 52 | 53 | Your Workflow/ 54 | info.plist 55 | icon.png 56 | workflow/ 57 | __init__.py 58 | background.py 59 | notify.py 60 | Notify.tgz 61 | update.py 62 | version 63 | web.py 64 | workflow.py 65 | workflow3.py 66 | yourscript.py 67 | etc. 68 | 69 | 70 | .. _GitHub releases: https://github.com/deanishe/alfred-workflow/releases 71 | .. _the source code: https://github.com/deanishe/alfred-workflow/archive/master.zip 72 | .. _GitHub repository: https://github.com/deanishe/alfred-workflow 73 | .. _pip: https://pypi.python.org/pypi/pip 74 | .. _Python Package Index: https://pypi.python.org/pypi/Alfred-Workflow 75 | .. _GitHub: https://github.com/deanishe/alfred-workflow/releases 76 | -------------------------------------------------------------------------------- /docs/licence.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _licence: 3 | 4 | Licence 5 | ======= 6 | 7 | Alfred-Workflow is released under the :ref:`mit-licence`. 8 | 9 | 10 | .. _mit-licence: 11 | 12 | MIT Licence 13 | ^^^^^^^^^^^ 14 | 15 | .. include:: ../LICENCE.txt 16 | 17 | 18 | Contributors 19 | ^^^^^^^^^^^^ 20 | 21 | - `Dean Jackson`_ 22 | - `Stephen Margheim`_ 23 | - `Fabio Niephaus`_ 24 | - `Owen Min`_ 25 | 26 | 27 | 28 | .. _Dean Jackson: https://github.com/deanishe 29 | .. _Fabio Niephaus: https://github.com/fniephaus 30 | .. _Owen Min: https://github.com/owenwater 31 | .. _Stephen Margheim: https://github.com/fractaledmind 32 | -------------------------------------------------------------------------------- /docs/quickindex.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _quickindex: 3 | 4 | =========== 5 | Quick Index 6 | =========== 7 | 8 | Quick links to the important parts of the documentation. 9 | 10 | .. toctree:: 11 | :maxdepth: 3 12 | 13 | installation.rst 14 | guide/index.rst 15 | api/index 16 | tutorial_1.rst 17 | tutorial_2.rst 18 | -------------------------------------------------------------------------------- /docs/supported-versions.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _supported-versions: 3 | 4 | ================== 5 | Supported versions 6 | ================== 7 | 8 | Alfred-Workflow supports all versions of Alfred 2–4. It works with 9 | Python 2.6 and 2.7, but *not yet* Python 3. 10 | 11 | Some features are not available on older versions of macOS. 12 | 13 | .. contents:: 14 | :local: 15 | 16 | 17 | Alfred versions 18 | =============== 19 | 20 | Alfred-Workflow works with all versions of Alfred 2, 3 & 4, but you must 21 | own the `Powerpack`_ to use Alfred's workflow feature. 22 | 23 | All Script Filter features provided by Alfred 2 as of v2.8.3 and by Alfred 4 as of v4.0.9 are supported in the latest version of Alfred-Workflow. 24 | 25 | The :class:`~workflow.Workflow` class is compatible with Alfred 2+. 26 | The :class:`~workflow.Workflow3` class is only compatible with Alfred 3+. 27 | 28 | .. important:: 29 | 30 | Versions 3.4.1 altered the way :ref:`workflow variables ` are set via Script Filter feedback, and :class:`~workflow.Workflow3` as of version 1.27 of Alfred-Workflow uses the new mechanism. 31 | 32 | As a result, versions 1.27+ of Alfred-Workflow are not compatible with versions of Alfred older than 3.4.1. 33 | 34 | :class:`~workflow.Workflow3` uses the JSON feedback format in Alfred 3+. It supports :ref:`workflow variables ` and more advanced modifiers than :class:`~workflow.Workflow`/Alfred 2. 35 | 36 | 37 | macOS versions 38 | ============== 39 | 40 | .. warning:: 41 | 42 | Versions of macOS before High Sierra have an extremely old version of OpenSSL, which is not compatible with many servers' SSL configurations, including GitHub's. :mod:`workflow.web` cannot connect to these servers, which also means that the `update mechanism `_ does not work on macOS 10.12/Sierra and older. 43 | 44 | Alfred-Workflow supports the same macOS versions as Alfred, namely 10.6 (Snow Leopard) and later (Alfred 3 is 10.9+ only). 45 | 46 | .. note:: 47 | 48 | :ref:`Notifications`, added in version 1.15 of Alfred-Workflow, are only available on Mountain Lion (10.8) and above. 49 | 50 | 51 | Python versions 52 | =============== 53 | 54 | Alfred-Workflow only officially supports the system Pythons that come with macOS (i.e. ``/usr/bin/python``), which is 2.6 on 10.6/Snow Leopard and 2.7 on later versions. 55 | 56 | .. important:: 57 | 58 | Other Pythons (e.g. Homebrew, conda, pyenv etc.) are *not* supported. 59 | 60 | This is a deliberate design choice, so please do not submit feature requests for support of, or bug reports concerning issues with any non-system Pythons. 61 | 62 | **This includes Python 3**. 63 | 64 | Python 3 support will be added in a new major version of the library when Catalina is more popular. 65 | 66 | 67 | Here is the `full list of new features in Python 2.7`_, but the most important things if you want your workflow to run on Snow Leopard/Python 2.6 are: 68 | 69 | - :mod:`argparse` is not available in 2.6. Use :mod:`getopt` or 70 | `include argparse in your workflow`_. Personally, I'm a big fan of 71 | `docopt`_ for parsing command-line arguments, but :mod:`argparse` 72 | is better for certain use cases. 73 | - You must specify field numbers for :meth:`str.format`, i.e. 74 | ``'{0}.{1}'.format(first, second)`` not just 75 | ``'{}.{}'.format(first, second)``. 76 | - No :class:`~collections.Counter` or 77 | :class:`~collections.OrderedDict` in :mod:`collections`. 78 | - No dictionary views in 2.6. 79 | - No set literals. 80 | - No dictionary or set comprehensions. 81 | 82 | Python 2.6 is still included in later versions of macOS (up to and including El Capitan), so run your Python scripts with ``/usr/bin/python2.6`` in addition to ``/usr/bin/python`` (2.7) to make sure they will run on Snow Leopard. 83 | 84 | 85 | Why no Python 3 support? 86 | ------------------------ 87 | 88 | Alfred-Workflow is targeted at the system Python on macOS. Its goal is to enable developers to build workflows that will "just work" for users on any vanilla installation of macOS since Snow Leopard. 89 | 90 | As such, it :ref:`strongly discourages developers ` from requiring users of their workflows to bugger about with their OS in order to get a workflow to work. This naturally includes requiring the installation of some non-default Python. 91 | 92 | Version 2 of Alfred-Workflow, which will be a complete rewrite, will support Python 3 and Alfred 4+ only. 93 | 94 | 95 | .. _full list of new features in Python 2.7: https://docs.python.org/3/whatsnew/2.7.html 96 | .. _include argparse in your workflow: https://pypi.python.org/pypi/argparse 97 | .. _docopt: http://docopt.org/ 98 | .. _Powerpack: https://buy.alfredapp.com/ 99 | -------------------------------------------------------------------------------- /docs/toc.rst.inc: -------------------------------------------------------------------------------- 1 | 2 | 3 | .. _usage: 4 | 5 | Using Alfred-Workflow 6 | ===================== 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | supported-versions 12 | installation 13 | tutorial 14 | guide/index 15 | api/index 16 | 17 | 18 | Miscellaneous 19 | ============= 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | contributing 25 | dash 26 | licence 27 | aw-workflows 28 | indices 29 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _tutorial: 3 | 4 | ======== 5 | Tutorial 6 | ======== 7 | 8 | This is a two-part tutorial on writing an Alfred 2 workflow with 9 | Alfred-Workflow, taking you through the basics to a full-featured workflow 10 | ready to share with the world. 11 | 12 | Part 1: A Basic Pinboard Workflow 13 | ================================= 14 | 15 | In which we build an Alfred workflow to view recent posts to 16 | `Pinboard `_. 17 | 18 | If you're new to Alfred and/or coding in general, start here. 19 | 20 | .. toctree:: 21 | :maxdepth: 3 22 | 23 | tutorial_1 24 | 25 | Part 2: A Distribution-Ready Pinboard Workflow 26 | ============================================== 27 | 28 | In which we make our `Pinboard `_ workflow ready for 29 | the masses. 30 | 31 | Demonstrates more advanced usage of Alfred-Workflow and a few workflow 32 | tricks that might also be of interest to intermediate Pythonistas. 33 | 34 | .. toctree:: 35 | :maxdepth: 3 36 | 37 | tutorial_2 38 | -------------------------------------------------------------------------------- /extras/Notify.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleAllowMixedLocalizations 6 | 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleExecutable 10 | applet 11 | CFBundleIconFile 12 | applet 13 | CFBundleIdentifier 14 | net.deanishe.alfred-workflow.notifier 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | Alfred-Workflow Notifier 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleSignature 24 | aplt 25 | LSMinimumSystemVersionByArchitecture 26 | 27 | x86_64 28 | 10.6 29 | 30 | LSRequiresCarbon 31 | 32 | WindowState 33 | 34 | bundleDividerCollapsed 35 | 36 | bundlePositionOfDivider 37 | 609 38 | dividerCollapsed 39 | 40 | eventLogLevel 41 | 2 42 | name 43 | ScriptWindowState 44 | positionOfDivider 45 | 486 46 | savedFrame 47 | 827 572 907 737 0 0 2560 1577 48 | selectedTab 49 | result 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /extras/Notify.app/Contents/MacOS/applet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/Notify.app/Contents/MacOS/applet -------------------------------------------------------------------------------- /extras/Notify.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPLaplt -------------------------------------------------------------------------------- /extras/Notify.app/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/Notify.app/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /extras/Notify.app/Contents/Resources/applet.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/Notify.app/Contents/Resources/applet.icns -------------------------------------------------------------------------------- /extras/Notify.app/Contents/Resources/applet.rsrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/Notify.app/Contents/Resources/applet.rsrc -------------------------------------------------------------------------------- /extras/Notify.app/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf110 2 | {\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | } -------------------------------------------------------------------------------- /extras/Notify.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/Notify.tgz -------------------------------------------------------------------------------- /extras/README.md: -------------------------------------------------------------------------------- 1 | 2 | Extras 3 | ====== 4 | 5 | Helper scripts, icons and other stuff. 6 | 7 | 8 | Components 9 | ---------- 10 | 11 | - `Notify.app` — AppleScript application to post notifications. 12 | - `Notify.tgz` — The app compressed for distribution. 13 | 14 | 15 | Docs 16 | ---- 17 | 18 | - `gen_icon_table.py` — Generate the ReST table of system icons for the User Manual. Also generates PNGs in the `docs/_static/` directory. 19 | - `generate_workflow_list.py` — Generate a Markdown/ReST list of workflows based on Alfred-Workflow from the Packal repository. 20 | - `library_workflows.tsv` — Non-Packal workflows to include in the workflow list. 21 | 22 | 23 | Icons 24 | ----- 25 | 26 | - `icons` — Generated and source files for the Alfred-Workflow icon. 27 | - `Alfred-Workflow.icns` — Generated Alfred-Workflow icon. 28 | - `Alfred-Workflow.iconset` — Generated Alfred-Workflow icon set. 29 | - `Alfred-Workflow.iconsproj` — Alfred-Workflow icon ICNS project file. 30 | - `Alfred-Workflow.png` — Generated Alfred-Workflow icon. 31 | - `Alfred-Workflow.sketch` — Alfred-Workflow icon source file. 32 | 33 | -------------------------------------------------------------------------------- /extras/benchmarks/00-python-interpreter-only/info.plist: -------------------------------------------------------------------------------- 1 | ../../../tests/data/info.plist.alfred3 -------------------------------------------------------------------------------- /extras/benchmarks/00-python-interpreter-only/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/python -c '' 4 | -------------------------------------------------------------------------------- /extras/benchmarks/01-read-info-plist/info.plist: -------------------------------------------------------------------------------- 1 | ../../../tests/data/info.plist.alfred3 -------------------------------------------------------------------------------- /extras/benchmarks/01-read-info-plist/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/python ./script.py 4 | -------------------------------------------------------------------------------- /extras/benchmarks/01-read-info-plist/script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2016 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2016-07-9 9 | # 10 | 11 | """ 12 | """ 13 | 14 | from __future__ import print_function, unicode_literals, absolute_import 15 | 16 | 17 | import sys 18 | 19 | from workflow import Workflow 20 | 21 | log = None 22 | 23 | 24 | def main(wf): 25 | """Do nothing.""" 26 | log.debug('datadir=%r', wf.datadir) 27 | 28 | 29 | if __name__ == '__main__': 30 | wf = Workflow() 31 | log = wf.logger 32 | sys.exit(wf.run(main)) 33 | -------------------------------------------------------------------------------- /extras/benchmarks/02-large-info-plist/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/python ./script.py 4 | -------------------------------------------------------------------------------- /extras/benchmarks/02-large-info-plist/script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2016 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2016-07-9 9 | # 10 | 11 | """ 12 | """ 13 | 14 | from __future__ import print_function, unicode_literals, absolute_import 15 | 16 | 17 | import sys 18 | 19 | from workflow import Workflow 20 | 21 | log = None 22 | 23 | 24 | def main(wf): 25 | """Do nothing.""" 26 | log.debug('datadir=%r', wf.datadir) 27 | 28 | 29 | if __name__ == '__main__': 30 | wf = Workflow() 31 | log = wf.logger 32 | sys.exit(wf.run(main)) 33 | -------------------------------------------------------------------------------- /extras/benchmarks/03-read-envvars/info.plist: -------------------------------------------------------------------------------- 1 | ../../../tests/data/info.plist.alfred3 -------------------------------------------------------------------------------- /extras/benchmarks/03-read-envvars/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # export alfred_preferences="$HOME/Dropbox/Config/Alfred 2/Alfred.alfredpreferences" 4 | export alfred_workflow_data="$HOME/Library/Application Support/Alfred 2/Workflow Data/net.deanishe.alfred-workflow" 5 | # export alfred_workflow_cache="$HOME/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/net.deanishe.alfred-workflow" 6 | # export alfred_workflow_bundleid="net.deanishe.alfred-workflow" 7 | # export alfred_workflow_name="Alfred Workflow" 8 | # export alfred_workflow_version="1.1.1" 9 | 10 | /usr/bin/python ./script.py 11 | -------------------------------------------------------------------------------- /extras/benchmarks/03-read-envvars/script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2016 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2016-07-9 9 | # 10 | 11 | """ 12 | """ 13 | 14 | from __future__ import print_function, unicode_literals, absolute_import 15 | 16 | 17 | import sys 18 | 19 | from workflow import Workflow 20 | 21 | log = None 22 | 23 | 24 | def main(wf): 25 | """Do nothing.""" 26 | log.debug('datadir=%r', wf.datadir) 27 | 28 | 29 | if __name__ == '__main__': 30 | wf = Workflow() 31 | log = wf.logger 32 | sys.exit(wf.run(main)) 33 | -------------------------------------------------------------------------------- /extras/gen_icon_table.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-03-07 9 | # 10 | 11 | """ 12 | 13 | Generate a ReST table of icons in :mod:`workflow.workflow` with previews. 14 | 15 | """ 16 | 17 | from __future__ import print_function, unicode_literals 18 | 19 | import os 20 | import subprocess 21 | import workflow 22 | 23 | outdir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 24 | 'docs', '_static') 25 | 26 | 27 | def make_thumbnail(infile, outfile): 28 | cmd = ['sips', '-Z', '64', '-s', 'format', 'png', infile, '--out', outfile] 29 | # print(cmd) 30 | subprocess.call(cmd) 31 | 32 | 33 | entries = [] 34 | 35 | col1 = col2 = 0 36 | 37 | for name in dir(workflow): 38 | if name.startswith('ICON_'): 39 | const = getattr(workflow, name) 40 | # print('{} : {}'.format(name, const)) 41 | filename = '{}.png'.format(name) 42 | make_thumbnail(const, os.path.join(outdir, filename)) 43 | image = '.. image:: ../_static/{}'.format(filename) 44 | entries.append((name, image)) 45 | if len(name) > col1: 46 | col1 = len(name) 47 | if len(image) > col2: 48 | col2 = len(image) 49 | 50 | col1 += 5 51 | 52 | 53 | print('+' + ('-' * col1) + '+' + ('-' * col2) + '+') 54 | print('| Name'.ljust(col1 + 1) + '| Preview'.ljust(col2 + 1) + '|') 55 | print('+' + ('=' * col1) + '+' + ('=' * col2) + '+') 56 | for name, image in entries: 57 | print('|``{}``'.format(name).ljust(col1 + 1) + '|' + 58 | image.ljust(col2) + '|') 59 | print('+' + ('-' * col1) + '+' + ('-' * col2) + '+') 60 | 61 | -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.icns -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_128x128.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_16x16.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_256x256.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_32x32.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_512x512.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.iconsproj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.iconsproj -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.png -------------------------------------------------------------------------------- /extras/icons/Alfred-Workflow.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/extras/icons/Alfred-Workflow.sketch -------------------------------------------------------------------------------- /extras/library_workflows.tsv: -------------------------------------------------------------------------------- 1 | name url author author_url description 2 | AppStorrent https://github.com/jag-k/alfred-appstorrent jag-k https://github.com/jag-k Search and download programs and games to Mac OS 3 | TodoList https://github.com/ecmadao/Alfred-TodoList ecmadao https://github.com/ecmadao A simple todo-workflow lets you add, complete or delete todo in to-do lists. 4 | Pomodoro Alfred https://github.com/ecbrodie/pomodoro-alfred Evan Brodie https://github.com/ecbrodie Track Pomodoros through Alfred. 5 | ZotHero https://github.com/deanishe/ZotHero deanishe https://github.com/deanishe Rapidly search and cite Zotero entries from Alfred 6 | Video Conferences https://www.deanishe.net/post/2020/05/workflow-video-conferences/ deanishe https://www.deanishe.net Show upcoming video conferences from Calendar.app 7 | HeWeather和风天气 https://github.com/zhuozhiyongde/Alfred_HeWeather_Workflow zhuozhiyongde https://github.com/zhuozhiyongde/ 一个采用和风天气API的查询天气的workflow(available in China) 8 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/icon.png -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | # Rebuild docs when any ReST or Python files change 2 | modd.conf 3 | docs/**/*.rst docs/**/*.rst.inc docs/**/*.py 4 | workflow/*.py docs/_templates/* docs/_static/* extras/alabaster/**/* { 5 | prep: " 6 | # build docs 7 | cd docs && make html 8 | " 9 | } 10 | 11 | modd.conf 12 | workflow/web.py 13 | tests/test_web*.py { 14 | prep: " 15 | # test web.py 16 | ./run-tests.sh -c workflow.web tests/test_web*.py 17 | " 18 | } 19 | 20 | modd.conf 21 | workflow/workflow.py 22 | tests/test_workflow*.py 23 | !tests/test_workflow3.py { 24 | prep: " 25 | # test workflow.py 26 | ./run-tests.sh -c workflow.workflow tests/test_workflow*.py 27 | " 28 | } 29 | 30 | modd.conf 31 | workflow/workflow3.py 32 | tests/test_workflow3.py { 33 | prep: " 34 | # test workflow3.py 35 | ./run-tests.sh -c workflow.workflow3 tests/test_workflow3.py 36 | " 37 | } 38 | 39 | modd.conf 40 | workflow/util.py 41 | tests/test_util*.py { 42 | prep: " 43 | # test util.py 44 | ./run-tests.sh -c workflow.util tests/test_util*.py 45 | " 46 | } 47 | 48 | modd.conf 49 | workflow/update.py 50 | tests/test_update*.py { 51 | prep: " 52 | # test update.py 53 | ./run-tests.sh -c workflow.update tests/test_update*.py 54 | " 55 | } 56 | 57 | modd.conf 58 | workflow/notify.py 59 | tests/test_notify*.py { 60 | prep: " 61 | # test notify.py 62 | ./run-tests.sh -c workflow.notify tests/test_notify.py 63 | " 64 | } 65 | 66 | modd.conf 67 | workflow/background.py 68 | tests/test_background*.py { 69 | prep: " 70 | # test background.py 71 | ./run-tests.sh -c workflow.background tests/test_background.py 72 | " 73 | } 74 | 75 | # Reload browser when any generated HTML files change 76 | modd.conf 77 | docs/_build/**/* { 78 | daemon: " 79 | # serve docs on http://127.0.0.1:8000 80 | devd -lm docs/_build/html/ 81 | " 82 | } 83 | -------------------------------------------------------------------------------- /requirements-ci.txt: -------------------------------------------------------------------------------- 1 | codacy-coverage==1.3.11 2 | coveralls==1.11.1 3 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | -e git+https://github.com/deanishe/alabaster/@a5ec3ee9827bcebe6112fb1b4319443a340a33c5#egg=alabaster 2 | doc2dash==2.3.0 3 | Sphinx==1.8.5 4 | sphinxcontrib-napoleon2==1.0 5 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pyobjc-framework-Cocoa==5.3 2 | pytest==4.6.10 3 | pytest-cov==2.8.1 4 | pytest-httpbin==1.0.0 5 | pytest-localserver==0.5.0 6 | # tox==3.15.1 7 | # twine==1.15.0 8 | flake8==3.8.1 9 | flake8-docstrings==1.5.0 10 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rootdir="$( cd "$( dirname "$0" )"; pwd )" 4 | 5 | usage() { 6 | cat > /dev/stderr <] [...] 8 | 9 | Run test script(s) with coverage for one package. 10 | 11 | Usage: 12 | run-tests.sh [-v|-V] [-c ] [-l] [-t] [...] 13 | run-tests.sh -h 14 | 15 | Options: 16 | -c coverage report package 17 | -l run linter 18 | -t run tests (default) 19 | -v verbose output 20 | -V very verbose output 21 | -h show this message and exit 22 | 23 | Example: 24 | run-tests.sh -c workflow.notify tests/test_notify.py 25 | 26 | EOS 27 | } 28 | 29 | if [ -t 1 ]; then 30 | red='\033[0;31m' 31 | green='\033[0;32m' 32 | nc='\033[0m' 33 | else 34 | red= 35 | green= 36 | nc= 37 | fi 38 | 39 | function log() { 40 | echo "$@" 41 | } 42 | 43 | function fail() { 44 | printf "${red}$@${nc}\n" 45 | } 46 | 47 | function success() { 48 | printf "${green}$@${nc}\n" 49 | } 50 | 51 | coverpkg=workflow 52 | vopts= 53 | dolint=1 54 | dotest=0 55 | forcetest=1 56 | while getopts ":c:hltvV" opt; do 57 | case $opt in 58 | c) 59 | coverpkg="$OPTARG" 60 | ;; 61 | l) 62 | dolint=0 63 | ;; 64 | t) 65 | forcetest=0 66 | ;; 67 | h) 68 | usage 69 | exit 0 70 | ;; 71 | v) 72 | vopts="-v" 73 | ;; 74 | V) 75 | vopts="-vv" 76 | ;; 77 | \?) 78 | log "Invalid option: -$OPTARG" 79 | exit 1 80 | ;; 81 | esac 82 | done 83 | shift $((OPTIND-1)) 84 | 85 | # Set test options and run tests 86 | #------------------------------------------------------------------------- 87 | 88 | unset alfred_version alfred_workflow_version alfred_workflow_bundleid 89 | unset alfred_workflow_name alfred_workflow_cache alfred_workflow_data 90 | 91 | files=(tests) 92 | if [[ $# -gt 0 ]]; then 93 | files=$@ 94 | fi 95 | 96 | if [[ "$dolint" -eq 0 ]]; then 97 | dotest=1 98 | fi 99 | 100 | if [[ "$forcetest" -eq 0 ]]; then 101 | dotest=0 102 | fi 103 | 104 | coverage erase 105 | # command rm -fv .coverage.* 106 | 107 | if [[ $dotest -eq 0 ]]; then 108 | # More options are in tox.ini 109 | export PYTEST_ADDOPTS="--cov-report=html" 110 | pytest $vopts --cov="$coverpkg" $files 111 | ret1=${PIPESTATUS[0]} 112 | echo 113 | 114 | case "$ret1" in 115 | 0) success "TESTS OK" ;; 116 | *) fail "TESTS FAILED" ;; 117 | esac 118 | if [[ "$ret1" -ne 0 ]]; then 119 | exit $ret1 120 | fi 121 | echo 122 | fi 123 | 124 | 125 | if [[ $dolint -eq 0 ]]; then 126 | flake8 $files 127 | ret2=${PIPESTATUS[0]} 128 | 129 | case "$ret2" in 130 | 0) success "LINTING OK" ;; 131 | *) fail "LINTING FAILED" ;; 132 | esac 133 | fi 134 | 135 | if [[ "$ret2" -ne 0 ]]; then 136 | exit $ret2 137 | fi 138 | 139 | if [[ "$dotest" -eq 1 ]]; then 140 | exit 0 141 | fi 142 | 143 | # Test coverage 144 | coverage xml 145 | coverage report --fail-under 100 --show-missing 146 | ret3=${PIPESTATUS[0]} 147 | 148 | echo 149 | 150 | case "$ret3" in 151 | 0) success "COVERAGE OK" ;; 152 | *) fail "COVERAGE FAILED" ;; 153 | esac 154 | 155 | exit $ret3 156 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | detailed-errors=1 3 | with-coverage=1 4 | cover-package=workflow 5 | verbosity=2 6 | -------------------------------------------------------------------------------- /setup.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-08-17 9 | # 10 | 11 | """Alfred-Workflow library for building Alfred 3/4 workflows.""" 12 | 13 | import os 14 | from os.path import dirname, join 15 | import subprocess 16 | from setuptools import setup 17 | from setuptools.command.test import test as TestCommand 18 | 19 | 20 | def read(fname): 21 | """Return contents of file `fname` in this directory.""" 22 | return open(join(dirname(__file__), fname)).read() 23 | 24 | 25 | class PyTestCommand(TestCommand): 26 | """Enable running tests with `python setup.py test`.""" 27 | 28 | def finalize_options(self): 29 | """Implement TestCommand.""" 30 | TestCommand.finalize_options(self) 31 | self.test_args = [] 32 | self.test_suite = True 33 | 34 | def run_tests(self): 35 | """Implement TestCommand.""" 36 | subprocess.call( 37 | ['/bin/bash', join(dirname(__file__), 'run-tests.sh')]) 38 | 39 | 40 | version = read('workflow/version') 41 | long_description = read('README_PYPI.rst') 42 | 43 | name = 'Alfred-Workflow' 44 | author = 'Dean Jackson' 45 | author_email = 'deanishe@deanishe.net' 46 | url = 'http://www.deanishe.net/alfred-workflow/' 47 | description = 'Full-featured helper library for writing Alfred 2/3/4 workflows' 48 | keywords = 'alfred workflow alfred4' 49 | packages = ['workflow'] 50 | package_data = {'workflow': ['version', 'Notify.tgz']} 51 | classifiers = [ 52 | 'Development Status :: 5 - Production/Stable', 53 | 'License :: OSI Approved :: MIT License', 54 | 'Operating System :: MacOS :: MacOS X', 55 | 'Intended Audience :: Developers', 56 | 'Natural Language :: English', 57 | 'Programming Language :: Python :: 2.7', 58 | 'Topic :: Software Development :: Libraries', 59 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 60 | ] 61 | tests_require = [ 62 | 'coverage', 63 | 'pytest', 64 | 'pytest_cov', 65 | 'pytest_httpbin', 66 | 'pytest_localserver', 67 | ] 68 | zip_safe = False 69 | 70 | setup( 71 | name=name, 72 | version=version, 73 | description=description, 74 | long_description=long_description, 75 | keywords=keywords, 76 | author=author, 77 | author_email=author_email, 78 | url=url, 79 | packages=packages, 80 | package_data=package_data, 81 | include_package_data=True, 82 | classifiers=classifiers, 83 | tests_require=tests_require, 84 | cmdclass={'test': PyTestCommand}, 85 | zip_safe=zip_safe, 86 | ) 87 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | 2 | Test suite 3 | ========== 4 | 5 | Unit tests and data for Alfred-Workflow. Currently an unholy mix of `unittest` and `py.test` (moving towards the latter). 6 | 7 | 8 | Running the full suite with coverage 9 | ------------------------------------ 10 | 11 | ```bash 12 | ./run-tests.sh 13 | ``` 14 | in the project root to run the full test suite in place with coverage. 15 | 16 | ```bash 17 | tox 18 | ``` 19 | in the project root to build, install and test with Python 2.6 and 2.7. 20 | 21 | 22 | Testing a single module with coverage 23 | ------------------------------------- 24 | 25 | ```bash 26 | extras/testone ... 27 | ``` 28 | 29 | to run test script(s) with coverage for a single module. 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.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-05-06 9 | # 10 | 11 | """Common pytest fixtures.""" 12 | 13 | from __future__ import print_function, absolute_import 14 | 15 | from contextlib import contextmanager 16 | import os 17 | from shutil import rmtree 18 | from tempfile import mkdtemp 19 | 20 | import pytest 21 | 22 | from workflow.workflow import Workflow 23 | 24 | 25 | from .util import ( 26 | INFO_PLIST_TEST, 27 | INFO_PLIST_TEST3, 28 | InfoPlist, 29 | ) 30 | 31 | BUNDLE_ID = 'net.deanishe.alfred-workflow' 32 | WORKFLOW_NAME = 'Alfred-Workflow Test' 33 | WORKFLOW_VERSION = '1.1.1' 34 | 35 | ENV_V2 = dict( 36 | alfred_version='2.4', 37 | alfred_version_build='277', 38 | alfred_workflow_version=WORKFLOW_VERSION, 39 | alfred_workflow_bundleid=BUNDLE_ID, 40 | alfred_workflow_name=WORKFLOW_NAME, 41 | alfred_workflow_cache=os.path.expanduser( 42 | '~/Library/Caches/com.runningwithcrayons.Alfred-2/' 43 | 'Workflow Data/' + BUNDLE_ID), 44 | alfred_workflow_data=os.path.expanduser( 45 | '~/Library/Application Support/Alfred 2/' 46 | 'Workflow Data/' + BUNDLE_ID), 47 | alfred_preferences=os.path.expanduser( 48 | '~/Library/Application Support/Alfred 2/' 49 | 'Alfred.alfredpreferences'), 50 | ) 51 | 52 | ENV_V3 = dict( 53 | alfred_version='3.8.1', 54 | alfred_version_build='961', 55 | alfred_workflow_version=WORKFLOW_VERSION, 56 | alfred_workflow_bundleid=BUNDLE_ID, 57 | alfred_workflow_name=WORKFLOW_NAME, 58 | alfred_workflow_cache=os.path.expanduser( 59 | '~/Library/Caches/com.runningwithcrayons.Alfred-3/' 60 | 'Workflow Data/' + BUNDLE_ID), 61 | alfred_workflow_data=os.path.expanduser( 62 | '~/Library/Application Support/Alfred 3/' 63 | 'Workflow Data/' + BUNDLE_ID), 64 | alfred_preferences=os.path.expanduser( 65 | '~/Library/Application Support/Alfred 3/' 66 | 'Alfred.alfredpreferences'), 67 | ) 68 | 69 | ENV_V4 = dict( 70 | alfred_version='4.0', 71 | alfred_version_build='1061', 72 | alfred_workflow_version=WORKFLOW_VERSION, 73 | alfred_workflow_bundleid=BUNDLE_ID, 74 | alfred_workflow_name=WORKFLOW_NAME, 75 | alfred_workflow_cache=os.path.expanduser( 76 | '~/Library/Caches/com.runningwithcrayons.Alfred/' 77 | 'Workflow Data/' + BUNDLE_ID), 78 | alfred_workflow_data=os.path.expanduser( 79 | '~/Library/Application Support/Alfred/' 80 | 'Workflow Data/' + BUNDLE_ID), 81 | alfred_preferences=os.path.expanduser( 82 | '~/Library/Application Support/Alfred/' 83 | 'Alfred.alfredpreferences'), 84 | ) 85 | 86 | COMMON = dict( 87 | alfred_debug='1', 88 | alfred_preferences_localhash='adbd4f66bc3ae8493832af61a41ee609b20d8705', 89 | alfred_theme='alfred.theme.yosemite', 90 | alfred_theme_background='rgba(255,255,255,0.98)', 91 | alfred_theme_subtext='3', 92 | alfred_workflow_uid='user.workflow.B0AC54EC-601C-479A-9428-01F9FD732959', 93 | ) 94 | 95 | 96 | @contextmanager 97 | def env(**kwargs): 98 | """Context manager to alter and restore system environment.""" 99 | prev = os.environ.copy() 100 | for k, v in kwargs.items(): 101 | if v is None: 102 | if k in os.environ: 103 | del os.environ[k] 104 | else: 105 | if isinstance(v, unicode): 106 | v = v.encode('utf-8') 107 | else: 108 | v = str(v) 109 | os.environ[k] = v 110 | 111 | yield 112 | 113 | os.environ = prev 114 | 115 | 116 | @pytest.fixture 117 | def wf(alfred4, infopl): 118 | """Provide a Workflow using Alfred 4 configuration.""" 119 | wf = Workflow() 120 | yield wf 121 | wf.reset() 122 | 123 | 124 | def setenv(*dicts): 125 | """Update ``os.environ`` from ``dict``s.""" 126 | for d in dicts: 127 | os.environ.update(d) 128 | 129 | 130 | def cleanenv(): 131 | """Remove Alfred variables from ``os.environ``.""" 132 | for k in os.environ.keys(): 133 | if k.startswith('alfred_'): 134 | del os.environ[k] 135 | 136 | 137 | @pytest.fixture(scope='function') 138 | def alfred3(): 139 | """Context manager that sets Alfred 3 environment variables.""" 140 | cleanenv() 141 | setenv(COMMON, ENV_V3) 142 | yield 143 | cleanenv() 144 | 145 | 146 | @pytest.fixture(scope='function') 147 | def alfred4(): 148 | """Context manager that sets Alfred 4 environment variables.""" 149 | cleanenv() 150 | setenv(COMMON, ENV_V4) 151 | yield 152 | cleanenv() 153 | 154 | 155 | @pytest.fixture(scope='function') 156 | def tempdir(): 157 | """Create (and delete) a temporary directory.""" 158 | path = mkdtemp() 159 | yield path 160 | if os.path.exists(path): 161 | rmtree(path) 162 | 163 | 164 | @pytest.fixture() 165 | def infopl2(): 166 | """Ensure ``info.plist`` exists in the working directory.""" 167 | with InfoPlist(INFO_PLIST_TEST): 168 | yield 169 | 170 | 171 | @pytest.fixture() 172 | def infopl(): 173 | """Ensure ``info.plist`` exists in the working directory.""" 174 | with InfoPlist(INFO_PLIST_TEST3): 175 | yield 176 | -------------------------------------------------------------------------------- /tests/data/Dummy-6.0.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/Dummy-6.0.alfredworkflow -------------------------------------------------------------------------------- /tests/data/baidu.html.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/baidu.html.gz -------------------------------------------------------------------------------- /tests/data/cönfüsed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/cönfüsed.gif -------------------------------------------------------------------------------- /tests/data/cönfüsed.gif.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/cönfüsed.gif.gz -------------------------------------------------------------------------------- /tests/data/cönfüsed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/cönfüsed.gif -------------------------------------------------------------------------------- /tests/data/cönfüsed.gif.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/cönfüsed.gif.gz -------------------------------------------------------------------------------- /tests/data/fubar.txt: -------------------------------------------------------------------------------- 1 | fübar -------------------------------------------------------------------------------- /tests/data/gh-releases-4plus.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/gh-releases-4plus.json.gz -------------------------------------------------------------------------------- /tests/data/gh-releases.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/gh-releases.json.gz -------------------------------------------------------------------------------- /tests/data/info.plist.alfred2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | net.deanishe.alfred-workflow 7 | connections 8 | 9 | createdby 10 | Dean Jackson 11 | description 12 | Test alfred-workflow library 13 | disabled 14 | 15 | name 16 | Alfred-Workflow Test 17 | objects 18 | 19 | 20 | config 21 | 22 | argumenttype 23 | 0 24 | escaping 25 | 102 26 | keyword 27 | wftest 28 | runningsubtext 29 | Doin' stuff… 30 | script 31 | python test.py "{query}" 32 | subtext 33 | Test alfred-workflow Python lib 34 | title 35 | Alfred-Workflow Test 36 | type 37 | 0 38 | withspace 39 | 40 | 41 | type 42 | alfred.workflow.input.scriptfilter 43 | uid 44 | 5F480F88-2088-4D34-B621-ACEBCB5E6753 45 | version 46 | 0 47 | 48 | 49 | readme 50 | 51 | uidata 52 | 53 | 5F480F88-2088-4D34-B621-ACEBCB5E6753 54 | 55 | ypos 56 | 10 57 | 58 | 59 | webaddress 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/data/info.plist.alfred3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | net.deanishe.alfred-workflow 7 | connections 8 | 9 | createdby 10 | Dean Jackson 11 | description 12 | Test alfred-workflow library 13 | disabled 14 | 15 | name 16 | Alfred-Workflow Test 17 | objects 18 | 19 | 20 | config 21 | 22 | alfredfiltersresults 23 | 24 | argumenttype 25 | 0 26 | escaping 27 | 102 28 | keyword 29 | wftest 30 | queuedelaycustom 31 | 1 32 | queuedelayimmediatelyinitially 33 | 34 | queuedelaymode 35 | 0 36 | queuemode 37 | 1 38 | runningsubtext 39 | Doin' stuff… 40 | script 41 | python test.py "$@" 42 | scriptargtype 43 | 1 44 | scriptfile 45 | 46 | subtext 47 | Test alfred-workflow Python lib 48 | title 49 | Alfred-Workflow Test 50 | type 51 | 0 52 | withspace 53 | 54 | 55 | type 56 | alfred.workflow.input.scriptfilter 57 | uid 58 | 5F480F88-2088-4D34-B621-ACEBCB5E6753 59 | version 60 | 2 61 | 62 | 63 | readme 64 | 65 | uidata 66 | 67 | 5F480F88-2088-4D34-B621-ACEBCB5E6753 68 | 69 | xpos 70 | 30 71 | ypos 72 | 30 73 | 74 | 75 | version 76 | 1.1.1 77 | webaddress 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /tests/data/no-encoding.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hi! 4 | 5 | -------------------------------------------------------------------------------- /tests/data/no-encoding.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/no-encoding.xml.gz -------------------------------------------------------------------------------- /tests/data/us-ascii.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | Wake up to WonderWidgets! 14 | 15 | 16 | 17 | 18 | Overview 19 | Why WonderWidgets are great 20 | 21 | Who buys WonderWidgets 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/data/us-ascii.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/us-ascii.xml.gz -------------------------------------------------------------------------------- /tests/data/utf8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UTF-8 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/data/utf8.json: -------------------------------------------------------------------------------- 1 | { 2 | "args": {}, 3 | "headers": { 4 | "Accept": "*/*", 5 | "Accept-Encoding": "gzip, deflate", 6 | "Host": "eu.httpbin.org", 7 | "User-Agent": "HTTPie/0.9.2" 8 | }, 9 | "origin": "188.100.137.91", 10 | "url": "http://eu.httpbin.org/get" 11 | } 12 | -------------------------------------------------------------------------------- /tests/data/utf8.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/utf8.json.gz -------------------------------------------------------------------------------- /tests/data/utf8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | text 4 | -------------------------------------------------------------------------------- /tests/data/utf8.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/tests/data/utf8.xml.gz -------------------------------------------------------------------------------- /tests/lib/youcanimportme.py: -------------------------------------------------------------------------------- 1 | """Do-nothing module to test `sys.path` adjustment""" 2 | 3 | 4 | def noop(): 5 | """Do nothing""" 6 | pass 7 | -------------------------------------------------------------------------------- /tests/test_background.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-05-06 9 | # 10 | 11 | """Unit tests for :mod:`workflow.background`.""" 12 | 13 | from __future__ import print_function, absolute_import 14 | 15 | import os 16 | from time import sleep 17 | 18 | import pytest 19 | 20 | from workflow import Workflow 21 | from workflow.background import is_running, kill, run_in_background 22 | 23 | 24 | def _pidfile(name): 25 | return Workflow().cachefile('{0}.pid'.format(name)) 26 | 27 | 28 | def _write_pidfile(name, pid): 29 | pidfile = _pidfile(name) 30 | with open(pidfile, 'wb') as file: 31 | file.write('{0}'.format(pid)) 32 | 33 | 34 | def _delete_pidfile(name): 35 | pidfile = _pidfile(name) 36 | if os.path.exists(pidfile): 37 | os.unlink(pidfile) 38 | 39 | 40 | @pytest.mark.usefixtures('infopl') 41 | class TestBackground(object): 42 | """Unit tests for background jobs.""" 43 | 44 | def test_no_pidfile(self): 45 | """No PID file for non-existent job""" 46 | assert not is_running('boomstick') 47 | 48 | def test_non_existent_process(self): 49 | """Non-existent process""" 50 | _write_pidfile('test', 9999999) 51 | assert not is_running('test') 52 | assert not os.path.exists(_pidfile('test')) 53 | 54 | def test_existing_process(self): 55 | """Existing process""" 56 | _write_pidfile('test', os.getpid()) 57 | try: 58 | assert is_running('test') 59 | assert os.path.exists(_pidfile('test')) 60 | finally: 61 | _delete_pidfile('test') 62 | 63 | def test_run_in_background(self): 64 | """Run in background""" 65 | cmd = ['sleep', '1'] 66 | assert run_in_background('test', cmd) == 0 67 | assert is_running('test') 68 | assert os.path.exists(_pidfile('test')) 69 | # Already running 70 | assert run_in_background('test', cmd) is None 71 | sleep(1.1) # wait for job to finish 72 | assert not is_running('test') 73 | assert not os.path.exists(_pidfile('test')) 74 | 75 | def test_kill(self): 76 | """Kill""" 77 | assert kill('test') is False 78 | cmd = ['sleep', '1'] 79 | assert run_in_background('test', cmd) == 0 80 | assert is_running('test') 81 | assert kill('test') is True 82 | sleep(0.3) # give process time to exit 83 | assert not is_running('test') 84 | 85 | 86 | if __name__ == '__main__': # pragma: no cover 87 | pytest.main([__file__]) 88 | -------------------------------------------------------------------------------- /tests/test_notify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2016 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2016-02-22 9 | # 10 | 11 | """Unit tests for notifications.""" 12 | 13 | from __future__ import print_function 14 | 15 | import hashlib 16 | import logging 17 | import os 18 | import plistlib 19 | import shutil 20 | import stat 21 | 22 | import pytest 23 | 24 | from workflow import notify 25 | from workflow.workflow import Workflow 26 | 27 | from conftest import BUNDLE_ID 28 | from util import ( 29 | FakePrograms, 30 | WorkflowMock, 31 | ) 32 | 33 | DATADIR = os.path.expanduser( 34 | '~/Library/Application Support/Alfred/' 35 | 'Workflow Data/' + BUNDLE_ID) 36 | APP_PATH = os.path.join(DATADIR, 'Notify.app') 37 | APPLET_PATH = os.path.join(APP_PATH, 'Contents/MacOS/applet') 38 | ICON_PATH = os.path.join(APP_PATH, 'Contents/Resources/applet.icns') 39 | INFO_PATH = os.path.join(APP_PATH, 'Contents/Info.plist') 40 | 41 | # Alfred-Workflow icon (present in source distribution) 42 | PNG_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 43 | 'icon.png') 44 | 45 | 46 | @pytest.fixture 47 | def applet(): 48 | """Ensure applet doesn't exist.""" 49 | if os.path.exists(APP_PATH): 50 | shutil.rmtree(APP_PATH) 51 | yield 52 | if os.path.exists(APP_PATH): 53 | shutil.rmtree(APP_PATH) 54 | 55 | 56 | def test_log_wf(infopl, alfred4): 57 | """Workflow and Logger objects correct""" 58 | wf = notify.wf() 59 | assert isinstance(wf, Workflow), "not Workflow" 60 | # Always returns the same objects 61 | wf2 = notify.wf() 62 | assert wf is wf2, "not same Workflow" 63 | 64 | log = notify.log() 65 | assert isinstance(log, logging.Logger), "not Logger" 66 | log2 = notify.log() 67 | assert log is log2, "not same Logger" 68 | 69 | 70 | def test_paths(infopl, alfred4): 71 | """Module paths are correct""" 72 | assert DATADIR == notify.wf().datadir, "unexpected datadir" 73 | assert APPLET_PATH == notify.notifier_program(), "unexpected applet path" 74 | assert ICON_PATH == notify.notifier_icon_path(), "unexpected icon path" 75 | 76 | 77 | def test_install(infopl, alfred4, applet): 78 | """Notify.app is installed correctly""" 79 | assert os.path.exists(APP_PATH) is False, "APP_PATH exists" 80 | notify.install_notifier() 81 | for p in (APP_PATH, APPLET_PATH, ICON_PATH, INFO_PATH): 82 | assert os.path.exists(p) is True, "path not found" 83 | # Ensure applet is executable 84 | assert (os.stat(APPLET_PATH).st_mode & stat.S_IXUSR), \ 85 | "applet not executable" 86 | # Verify bundle ID was changed 87 | data = plistlib.readPlist(INFO_PATH) 88 | bid = data.get('CFBundleIdentifier') 89 | assert bid != BUNDLE_ID, "bundle IDs identical" 90 | assert bid.startswith(BUNDLE_ID) is True, "bundle ID not prefix" 91 | 92 | 93 | def test_sound(): 94 | """Good sounds work, bad ones fail""" 95 | # Good values 96 | for s in ('basso', 'GLASS', 'Purr', 'tink'): 97 | sound = notify.validate_sound(s) 98 | assert sound is not None 99 | assert sound == s.title(), "unexpected title" 100 | # Bad values 101 | for s in (None, 'SPOONS', 'The Hokey Cokey', ''): 102 | sound = notify.validate_sound(s) 103 | assert sound is None 104 | 105 | 106 | def test_invalid_notifications(infopl, alfred4): 107 | """Invalid notifications""" 108 | with pytest.raises(ValueError): 109 | notify.notify() 110 | # Is not installed yet 111 | assert os.path.exists(APP_PATH) is False 112 | assert notify.notify('Test Title', 'Test Message') is True 113 | # A notification should appear now, but there's no way of 114 | # checking whether it worked 115 | assert os.path.exists(APP_PATH) is True 116 | 117 | 118 | def test_notifyapp_called(infopl, alfred4): 119 | """Notify.app is called""" 120 | c = WorkflowMock() 121 | notify.install_notifier() 122 | with c: 123 | assert notify.notify('Test Title', 'Test Message') is False 124 | assert c.cmd[0] == APPLET_PATH 125 | 126 | 127 | def test_iconutil_fails(infopl, alfred4, tempdir): 128 | """`iconutil` throws RuntimeError""" 129 | with FakePrograms('iconutil'): 130 | icns_path = os.path.join(tempdir, 'icon.icns') 131 | with pytest.raises(RuntimeError): 132 | notify.png_to_icns(PNG_PATH, icns_path) 133 | 134 | 135 | def test_sips_fails(infopl, alfred4, tempdir): 136 | """`sips` throws RuntimeError""" 137 | with FakePrograms('sips'): 138 | icon_path = os.path.join(tempdir, 'icon.png') 139 | with pytest.raises(RuntimeError): 140 | notify.convert_image(PNG_PATH, icon_path, 64) 141 | 142 | 143 | def test_image_conversion(infopl, alfred4, tempdir, applet): 144 | """PNG to ICNS conversion""" 145 | assert os.path.exists(APP_PATH) is False 146 | notify.install_notifier() 147 | assert os.path.exists(APP_PATH) is True 148 | icns_path = os.path.join(tempdir, 'icon.icns') 149 | assert os.path.exists(icns_path) is False 150 | notify.png_to_icns(PNG_PATH, icns_path) 151 | assert os.path.exists(icns_path) is True 152 | with open(icns_path, 'rb') as fp: 153 | h1 = hashlib.md5(fp.read()) 154 | with open(ICON_PATH, 'rb') as fp: 155 | h2 = hashlib.md5(fp.read()) 156 | assert h1.digest() == h2.digest() 157 | 158 | 159 | if __name__ == '__main__': # pragma: no cover 160 | pytest.main([__file__]) 161 | -------------------------------------------------------------------------------- /tests/test_update_versions.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-08-16 9 | # 10 | 11 | """Test `update.Version` class.""" 12 | 13 | from __future__ import print_function 14 | 15 | import unittest 16 | 17 | import pytest 18 | 19 | from workflow.update import Version 20 | 21 | 22 | class VersionTests(unittest.TestCase): 23 | """Unit tests for Version.""" 24 | 25 | def setUp(self): 26 | """Initialise unit test data.""" 27 | self.invalid_versions = [ 28 | '', 29 | 'bob', 30 | '1.x.8', 31 | '1.0b', 32 | '1.0.3a', 33 | '1.0.0.0', 34 | '1.2.3.4', 35 | 'v.1.1', 36 | '.1.2.1', 37 | ] 38 | self.valid_versions = [ 39 | ('1', '1.0.0'), 40 | ('1.9', '1.9.0'), 41 | ('10.0', '10.0.0'), 42 | '1.0.1', 43 | '2.2.1', 44 | '10.11.12', 45 | '9.99.9999', 46 | '12.333.0-alpha', 47 | '8.10.11', 48 | '9.4.3+20144353453', 49 | '3.1.4-beta+20144334', 50 | ] 51 | 52 | def test_invalid_versions(self): 53 | """Versions: invalid versions""" 54 | for v in self.invalid_versions: 55 | self.assertRaises(ValueError, Version, v) 56 | 57 | def test_valid_versions(self): 58 | """Versions: valid versions""" 59 | for v in self.valid_versions: 60 | if isinstance(v, tuple): 61 | vin, vout = v 62 | else: 63 | vin = vout = v 64 | self.assertEqual(str(Version(vin)), vout) 65 | self.assertEqual(str(Version('v{0}'.format(vin))), vout) 66 | 67 | def test_compare_bad_objects(self): 68 | """Versions: invalid comparisons""" 69 | v = Version('1.0.0') 70 | t = (1, 0, 0) 71 | self.assertRaises(ValueError, lambda v, t: v == t, v, t) 72 | self.assertRaises(ValueError, lambda v, t: v >= t, v, t) 73 | self.assertRaises(ValueError, lambda v, t: v <= t, v, t) 74 | self.assertRaises(ValueError, lambda v, t: v != t, v, t) 75 | self.assertRaises(ValueError, lambda v, t: v > t, v, t) 76 | self.assertRaises(ValueError, lambda v, t: v < t, v, t) 77 | 78 | def test_compare_versions(self): 79 | """Versions: comparisons""" 80 | self.assertTrue(Version('1') == Version('1.0') == Version('1.0.0')) 81 | self.assertTrue(Version('1.0.0') == Version('01.0.00')) 82 | self.assertTrue(Version('1.10.0') > Version('1.9.9')) 83 | self.assertTrue(Version('1.10.0') > Version('1.10.0-alpha')) 84 | self.assertTrue(Version('1.9.9') < Version('1.10.0')) 85 | self.assertTrue(Version('1.10.0-alpha') < Version('1.10.0')) 86 | self.assertTrue(Version('1.10.0') >= Version('1.9.9')) 87 | self.assertTrue(Version('1.10.0') >= Version('1.10.0-alpha')) 88 | self.assertTrue(Version('1.9.9') <= Version('1.10.0')) 89 | self.assertTrue(Version('1.10.0-alpha') <= Version('1.10.0')) 90 | self.assertTrue(Version('1.10.0-alpha') < Version('1.10.0-beta')) 91 | self.assertTrue(Version('1.10.0-beta') > Version('1.10.0-alpha')) 92 | self.assertTrue(Version('1.10.0-beta') != Version('1.10.0-alpha')) 93 | self.assertTrue(Version('1.10.0-alpha') != Version('1.10.0')) 94 | self.assertTrue(Version('2.10.20') > Version('1.20.30')) 95 | self.assertTrue(Version('2.10.20') == Version('2.10.20+2342345345')) 96 | # With v prefix 97 | self.assertTrue(Version('v1.0.0') == Version('01.0.00')) 98 | self.assertTrue(Version('v1.10.0') > Version('1.9.9')) 99 | self.assertTrue(Version('v1.10.0') > Version('1.10.0-alpha')) 100 | self.assertTrue(Version('v1.9.9') < Version('1.10.0')) 101 | self.assertTrue(Version('v1.10.0-alpha') < Version('1.10.0')) 102 | self.assertTrue(Version('v1.10.0') >= Version('1.9.9')) 103 | self.assertTrue(Version('v1.10.0') >= Version('1.10.0-alpha')) 104 | self.assertTrue(Version('v1.9.9') <= Version('1.10.0')) 105 | self.assertTrue(Version('v1.10.0-alpha') <= Version('1.10.0')) 106 | self.assertTrue(Version('v1.10.0-alpha') < Version('1.10.0-beta')) 107 | self.assertTrue(Version('v1.10.0-beta') > Version('1.10.0-alpha')) 108 | self.assertTrue(Version('v1.10.0-beta') != Version('1.10.0-alpha')) 109 | self.assertTrue(Version('v1.10.0-alpha') != Version('1.10.0')) 110 | self.assertTrue(Version('v2.10.20') > Version('1.20.30')) 111 | self.assertTrue(Version('v2.10.20') == Version('2.10.20+2342345345')) 112 | # With and without suffixes 113 | self.assertTrue(Version('v1.10.0') > Version('1.10.0-beta')) 114 | self.assertTrue(Version('v1.10.0-alpha') < Version('1.10.0-beta')) 115 | # Complex suffixes 116 | self.assertTrue(Version('1.0.0-alpha') < Version('1.0.0-alpha.1')) 117 | self.assertTrue(Version('1.0.0-alpha.1') < Version('1.0.0-alpha.beta')) 118 | self.assertTrue(Version('1.0.0-alpha.beta') < Version('1.0.0-beta')) 119 | self.assertTrue(Version('1.0.0-beta') < Version('1.0.0-beta.2')) 120 | self.assertTrue(Version('1.0.0-beta.2') < Version('1.0.0-beta.11')) 121 | self.assertTrue(Version('1.0.0-beta.11') < Version('1.0.0-rc.1')) 122 | self.assertTrue(Version('1.0.0-rc.1') < Version('1.0.0')) 123 | 124 | 125 | if __name__ == '__main__': # pragma: no cover 126 | pytest.main([__file__]) 127 | -------------------------------------------------------------------------------- /tests/test_util_atomic.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-05-06 9 | # 10 | 11 | """Unit tests for :func:`~workflow.util.atomic_writer`.""" 12 | 13 | from __future__ import print_function 14 | 15 | import json 16 | import os 17 | 18 | import pytest 19 | 20 | from util import DEFAULT_SETTINGS 21 | 22 | from workflow.util import atomic_writer 23 | 24 | 25 | def _settings(tempdir): 26 | """Path to ``settings.json`` file.""" 27 | return os.path.join(tempdir, 'settings.json') 28 | 29 | 30 | def test_write_file_succeed(tempdir): 31 | """Succeed, no temp file left""" 32 | p = _settings(tempdir) 33 | with atomic_writer(p, 'wb') as fp: 34 | json.dump(DEFAULT_SETTINGS, fp) 35 | 36 | assert len(os.listdir(tempdir)) == 1 37 | assert os.path.exists(p) 38 | 39 | 40 | def test_failed_before_writing(tempdir): 41 | """Exception before writing""" 42 | p = _settings(tempdir) 43 | 44 | def write(): 45 | with atomic_writer(p, 'wb'): 46 | raise Exception() 47 | 48 | with pytest.raises(Exception): 49 | write() 50 | 51 | assert not os.listdir(tempdir) 52 | 53 | 54 | def test_failed_after_writing(tempdir): 55 | """Exception after writing""" 56 | p = _settings(tempdir) 57 | 58 | def write(): 59 | with atomic_writer(p, 'wb') as fp: 60 | json.dump(DEFAULT_SETTINGS, fp) 61 | raise Exception() 62 | 63 | with pytest.raises(Exception): 64 | write() 65 | 66 | assert not os.listdir(tempdir) 67 | 68 | 69 | def test_failed_without_overwriting(tempdir): 70 | """AtomicWriter: Exception after writing won't overwrite the old file""" 71 | p = _settings(tempdir) 72 | mockSettings = {} 73 | 74 | def write(): 75 | with atomic_writer(p, 'wb') as fp: 76 | json.dump(mockSettings, fp) 77 | raise Exception() 78 | 79 | with atomic_writer(p, 'wb') as fp: 80 | json.dump(DEFAULT_SETTINGS, fp) 81 | 82 | assert len(os.listdir(tempdir)) == 1 83 | assert os.path.exists(p) 84 | 85 | with pytest.raises(Exception): 86 | write() 87 | 88 | assert len(os.listdir(tempdir)) == 1 89 | assert os.path.exists(p) 90 | 91 | with open(p, 'rb') as fp: 92 | real_settings = json.load(fp) 93 | 94 | assert DEFAULT_SETTINGS == real_settings 95 | 96 | 97 | if __name__ == '__main__': # pragma: no cover 98 | pytest.main([__file__]) 99 | -------------------------------------------------------------------------------- /tests/test_util_lockfile.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-04-01 9 | # 10 | 11 | """Test LockFile functionality.""" 12 | 13 | from __future__ import print_function 14 | 15 | from collections import namedtuple 16 | from multiprocessing import Pool 17 | import os 18 | import shutil 19 | import sys 20 | import tempfile 21 | import traceback 22 | 23 | import pytest 24 | 25 | from workflow.util import AcquisitionError, LockFile 26 | from workflow.workflow import Settings 27 | 28 | 29 | Paths = namedtuple('Paths', 'testfile lockfile') 30 | 31 | 32 | @pytest.fixture(scope='function') 33 | def paths(request): 34 | """Test and lock file paths.""" 35 | tempdir = tempfile.mkdtemp() 36 | testfile = os.path.join(tempdir, 'myfile.txt') 37 | 38 | def rm(): 39 | shutil.rmtree(tempdir) 40 | 41 | request.addfinalizer(rm) 42 | 43 | return Paths(testfile, testfile + '.lock') 44 | 45 | 46 | def test_lockfile_created(paths): 47 | """Lock file created and deleted.""" 48 | assert not os.path.exists(paths.testfile) 49 | assert not os.path.exists(paths.lockfile) 50 | 51 | with LockFile(paths.testfile, timeout=0.2) as lock: 52 | assert lock.locked 53 | assert os.path.exists(paths.lockfile) 54 | 55 | assert not os.path.exists(paths.lockfile) 56 | 57 | 58 | def test_sequential_access(paths): 59 | """Sequential access to locked file.""" 60 | assert not os.path.exists(paths.testfile) 61 | assert not os.path.exists(paths.lockfile) 62 | 63 | lock = LockFile(paths.testfile, 0.1) 64 | 65 | with lock: 66 | assert lock.locked 67 | assert not lock.acquire(False) 68 | with pytest.raises(AcquisitionError): 69 | lock.acquire(True) 70 | 71 | assert lock.release() is False # lock already released 72 | 73 | assert not os.path.exists(paths.lockfile) 74 | 75 | 76 | def _write_test_data(args): 77 | """Write 10 lines to the test file.""" 78 | paths, data = args 79 | for i in range(10): 80 | with LockFile(paths.testfile, 0.5) as lock: 81 | assert lock.locked 82 | with open(paths.testfile, 'a') as fp: 83 | fp.write(data + '\n') 84 | 85 | 86 | def test_concurrent_access(paths): 87 | """Concurrent access to locked file is serialised.""" 88 | assert not os.path.exists(paths.testfile) 89 | assert not os.path.exists(paths.lockfile) 90 | 91 | # Create a pool of threads that each write a line 92 | # of 20 digits to the locked file. 93 | # Then verify that each line of the file only 94 | # consists of one character (i.e. writes do no overlap) 95 | lock = LockFile(paths.testfile, 0.5) 96 | 97 | pool = Pool(5) 98 | pool.map(_write_test_data, 99 | [(paths, str(i) * 20) for i in range(1, 6)]) 100 | 101 | assert not lock.locked 102 | assert not os.path.exists(paths.lockfile) 103 | 104 | with open(paths.testfile) as fp: 105 | lines = [line.strip() for line in fp.readlines()] 106 | 107 | for line in lines: 108 | assert len(set(line)) == 1 109 | 110 | 111 | def _write_settings(args): 112 | """Write a new value to the Settings.""" 113 | paths, key, value = args 114 | try: 115 | s = Settings(paths.testfile) 116 | s[key] = value 117 | print('Settings[{0}] = {1}'.format(key, value)) 118 | except Exception as err: 119 | print('error opening settings (%s): %s' % (key, 120 | traceback.format_exc()), 121 | file=sys.stderr) 122 | return err 123 | 124 | 125 | def test_concurrent_settings(paths): 126 | """Concurrent access to Settings is serialised.""" 127 | assert not os.path.exists(paths.testfile) 128 | assert not os.path.exists(paths.lockfile) 129 | 130 | defaults = {'foo': 'bar'} 131 | # initialise file 132 | Settings(paths.testfile, defaults) 133 | 134 | data = [(paths, 'thread_{0}'.format(i), 'value_{0}'.format(i)) 135 | for i in range(1, 10)] 136 | 137 | pool = Pool(5) 138 | errs = pool.map(_write_settings, data) 139 | errs = [e for e in errs if e is not None] 140 | 141 | assert errs == [] 142 | 143 | # Check settings file is still valid JSON 144 | # and that *something* was added to it. 145 | # The writing processes will have trampled 146 | # over each other, so there's no way to know 147 | # which one won and wrote its value. 148 | s = Settings(paths.testfile) 149 | assert s['foo'] == 'bar' 150 | assert len(s) > 1 151 | 152 | 153 | if __name__ == '__main__': # pragma: no cover 154 | pytest.main([__file__]) 155 | -------------------------------------------------------------------------------- /tests/test_util_uninterruptible.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-05-06 9 | # 10 | 11 | """Unit tests for ``uninterruptible`` decorator.""" 12 | 13 | from __future__ import print_function, absolute_import 14 | 15 | import os 16 | import signal 17 | 18 | import pytest 19 | 20 | from workflow.util import uninterruptible 21 | 22 | 23 | class Target(object): 24 | """Object to be manipulated by :func:`fakewrite`.""" 25 | 26 | def __init__(self, kill=True, finished=False): 27 | """Create new `Target`.""" 28 | self.kill = kill 29 | self.finished = finished 30 | self.handled = False 31 | 32 | def handler(self, signum, frame): 33 | """Alternate signal handler.""" 34 | self.handled = True 35 | 36 | 37 | @uninterruptible 38 | def fakewrite(target): 39 | """Mock writer. 40 | 41 | Sets ``target.finished`` if it completes. 42 | 43 | Args: 44 | target (Target): Object to set status on 45 | """ 46 | if target.kill: 47 | target.kill = False 48 | os.kill(os.getpid(), signal.SIGTERM) 49 | target.finished = True 50 | 51 | 52 | @pytest.fixture(scope='function') 53 | def target(): 54 | """Create a `Target`.""" 55 | # restore default handlers 56 | signal.signal(signal.SIGTERM, signal.SIG_DFL) 57 | yield Target() 58 | # restore default handlers 59 | signal.signal(signal.SIGTERM, signal.SIG_DFL) 60 | 61 | 62 | def test_normal(target): 63 | """Normal writing operator""" 64 | target.kill = False 65 | fakewrite(target) 66 | assert target.finished 67 | 68 | 69 | def test_sigterm_signal(target): 70 | """Process is killed, but call completes""" 71 | with pytest.raises(SystemExit): 72 | fakewrite(target) 73 | 74 | # call has completed 75 | assert target.finished 76 | assert not target.kill 77 | 78 | 79 | def test_old_signal_handler(target): 80 | """Kill with different signal handler registered""" 81 | signal.signal(signal.SIGTERM, target.handler) 82 | 83 | fakewrite(target) 84 | 85 | assert target.finished 86 | assert target.handled 87 | assert not target.kill 88 | 89 | 90 | def test_old_signal_handler_restore(target): 91 | """Restore previous signal handler after write""" 92 | signal.signal(signal.SIGTERM, target.handler) 93 | target.kill = False 94 | 95 | fakewrite(target) 96 | 97 | assert target.finished 98 | assert signal.getsignal(signal.SIGTERM) == target.handler 99 | 100 | 101 | if __name__ == '__main__': # pragma: no cover 102 | pytest.main([__file__]) 103 | -------------------------------------------------------------------------------- /tests/test_web_http_encoding.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-08-17 9 | # 10 | 11 | """HTTP unit tests.""" 12 | 13 | from __future__ import print_function 14 | 15 | import os 16 | 17 | import pytest 18 | import pytest_localserver # noqa: F401 19 | 20 | 21 | from workflow import web 22 | 23 | TEST_DATA = [ 24 | # Baidu sends charset header *and* uses meta HTML tag 25 | ('baidu.html', {'Content-Type': 'text/html; charset=utf-8'}, 'utf-8'), 26 | 27 | # Document specifies us-ascii 28 | ('us-ascii.xml', {'Content-Type': 'application/xml'}, 'us-ascii'), 29 | 30 | # Document specifies UTF-8 31 | ('utf8.xml', {'Content-Type': 'application/xml'}, 'utf-8'), 32 | 33 | # Document specifies no encoding; application/xml defaults to UTF-8 34 | # (in this library. There is no standard encoding defined.) 35 | ('no-encoding.xml', {'Content-Type': 'application/xml'}, 'utf-8'), 36 | 37 | # application/json is UTF-8 38 | ('utf8.json', {'Content-Type': 'application/json'}, 'utf-8'), 39 | ] 40 | 41 | 42 | def test_web_encoding(httpserver): 43 | """Test web encoding""" 44 | test_data = [] 45 | for filename, headers, encoding in TEST_DATA: 46 | p = os.path.join(os.path.dirname(__file__), 'data', filename) 47 | test_data.append((p, headers, encoding)) 48 | p2 = '{0}.gz'.format(p) 49 | if os.path.exists(p2): 50 | h2 = headers.copy() 51 | h2['Content-Encoding'] = 'gzip' 52 | test_data.append((p2, h2, encoding)) 53 | 54 | for filepath, headers, encoding in test_data: 55 | print('filepath={0!r}, headers={1!r}, encoding={2!r}'.format( 56 | filepath, headers, encoding)) 57 | 58 | content = open(filepath).read() 59 | 60 | httpserver.serve_content(content, headers=headers) 61 | r = web.get(httpserver.url) 62 | r.raise_for_status() 63 | assert r.encoding == encoding 64 | 65 | 66 | if __name__ == '__main__': # pragma: no cover 67 | pytest.main([__file__]) 68 | -------------------------------------------------------------------------------- /tests/test_workflow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/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-03-01 9 | # 10 | 11 | """Unit tests for :mod:`workflow.Workflow`.""" 12 | 13 | from __future__ import print_function, unicode_literals 14 | 15 | import logging 16 | import os 17 | import sys 18 | 19 | from unicodedata import normalize 20 | 21 | import pytest 22 | 23 | from workflow import Workflow 24 | 25 | from .conftest import env 26 | 27 | 28 | def test_args(alfred4): 29 | """ARGV""" 30 | args = ['arg1', 'arg2', 'füntíme'] 31 | oargs = sys.argv[:] 32 | sys.argv = [oargs[0]] + [s.encode('utf-8') for s in args] 33 | wf = Workflow() 34 | try: 35 | assert wf.args == args 36 | finally: 37 | sys.argv = oargs[:] 38 | 39 | 40 | def test_arg_normalisation(alfred4): 41 | """ARGV normalisation""" 42 | def nfdme(s): 43 | """NFD-normalise string""" 44 | return normalize('NFD', s) 45 | 46 | args = [nfdme(s) for s in ['arg1', 'arg2', 'füntíme']] 47 | oargs = sys.argv[:] 48 | sys.argv = [oargs[0]] + [s.encode('utf-8') for s in args] 49 | 50 | wf = Workflow(normalization='NFD') 51 | try: 52 | assert wf.args == args 53 | finally: 54 | sys.argv = oargs[:] 55 | 56 | 57 | def test_magic_args(alfred4): 58 | """Magic args""" 59 | # cache original sys.argv 60 | oargs = sys.argv[:] 61 | 62 | # delsettings 63 | sys.argv = [oargs[0]] + [b'workflow:delsettings'] 64 | try: 65 | wf = Workflow(default_settings={'arg1': 'value1'}) 66 | assert wf.settings['arg1'] == 'value1' 67 | assert os.path.exists(wf.settings_path) 68 | with pytest.raises(SystemExit): 69 | wf.args 70 | assert not os.path.exists(wf.settings_path) 71 | finally: 72 | sys.argv = oargs[:] 73 | 74 | # delcache 75 | sys.argv = [oargs[0]] + [b'workflow:delcache'] 76 | 77 | def somedata(): 78 | return {'arg1': 'value1'} 79 | 80 | try: 81 | wf = Workflow() 82 | cachepath = wf.cachefile('somedir') 83 | os.makedirs(cachepath) 84 | wf.cached_data('test', somedata) 85 | assert os.path.exists(wf.cachefile('test.cpickle')) 86 | with pytest.raises(SystemExit): 87 | wf.args 88 | assert not os.path.exists(wf.cachefile('test.cpickle')) 89 | finally: 90 | sys.argv = oargs[:] 91 | 92 | 93 | def test_logger(wf): 94 | """Logger""" 95 | assert isinstance(wf.logger, logging.Logger) 96 | logger = logging.Logger('') 97 | wf.logger = logger 98 | assert wf.logger == logger 99 | 100 | 101 | def test_icons(): 102 | """Icons""" 103 | import workflow 104 | for name in dir(workflow): 105 | if name.startswith('ICON_'): 106 | path = getattr(workflow, name) 107 | print(name, path) 108 | assert os.path.exists(path) 109 | 110 | 111 | def test_debugging(alfred4): 112 | """Debugging""" 113 | tests = [ 114 | ('', False), 115 | ('0', False), 116 | ('1', True), 117 | ] 118 | for s, wanted in tests: 119 | with env(alfred_debug=s): 120 | wf = Workflow() 121 | assert wf.debugging == wanted, "unexpected debugging" 122 | 123 | 124 | if __name__ == '__main__': # pragma: no cover 125 | pytest.main([__file__]) 126 | -------------------------------------------------------------------------------- /tests/test_workflow_encoding.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright (c) 2019 Dean Jackson 3 | # MIT Licence applies http://opensource.org/licenses/MIT 4 | # 5 | # Created 2019-05-05 6 | 7 | """Unit tests for serializers.""" 8 | 9 | from __future__ import print_function, unicode_literals 10 | 11 | import pytest 12 | 13 | 14 | def test_unicode_paths(wf): 15 | """Workflow paths are Unicode""" 16 | s = b'test.txt' 17 | u = u'über.txt' 18 | assert isinstance(wf.datadir, unicode) 19 | assert isinstance(wf.datafile(s), unicode) 20 | assert isinstance(wf.datafile(u), unicode) 21 | assert isinstance(wf.cachedir, unicode) 22 | assert isinstance(wf.cachefile(s), unicode) 23 | assert isinstance(wf.cachefile(u), unicode) 24 | assert isinstance(wf.workflowdir, unicode) 25 | assert isinstance(wf.workflowfile(s), unicode) 26 | assert isinstance(wf.workflowfile(u), unicode) 27 | 28 | 29 | if __name__ == '__main__': # pragma: no cover 30 | pytest.main([__file__]) 31 | -------------------------------------------------------------------------------- /tests/test_workflow_env.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright (c) 2019 Dean Jackson 3 | # MIT Licence applies http://opensource.org/licenses/MIT 4 | # 5 | # Created 2019-05-05 6 | 7 | """Unit tests for environment/info.plist.""" 8 | 9 | from __future__ import print_function, unicode_literals 10 | 11 | import logging 12 | import os 13 | 14 | import pytest 15 | 16 | from workflow.workflow import Workflow 17 | 18 | from .conftest import ( 19 | env, COMMON, ENV_V4, 20 | BUNDLE_ID, WORKFLOW_NAME, 21 | ) 22 | from .util import INFO_PLIST_PATH, dump_env 23 | 24 | 25 | def test_file(infopl): 26 | """info.plist""" 27 | wf = Workflow() 28 | assert wf.name == WORKFLOW_NAME 29 | assert wf.bundleid == BUNDLE_ID 30 | 31 | 32 | def test_file_missing(): 33 | """Info.plist missing""" 34 | wf = Workflow() 35 | assert not os.path.exists(INFO_PLIST_PATH) 36 | with pytest.raises(IOError): 37 | wf.workflowdir 38 | 39 | 40 | def test_env(wf): 41 | """Alfred environmental variables""" 42 | env = COMMON.copy() 43 | env.update(ENV_V4) 44 | for k, v in env.items(): 45 | k = k.replace('alfred_', '') 46 | if k in ('debug', 'version_build', 'theme_subtext'): 47 | assert int(v) == wf.alfred_env[k] 48 | else: 49 | assert isinstance(wf.alfred_env[k], unicode) 50 | assert unicode(v) == wf.alfred_env[k] 51 | 52 | assert wf.datadir == env['alfred_workflow_data'] 53 | assert wf.cachedir == env['alfred_workflow_cache'] 54 | assert wf.bundleid == env['alfred_workflow_bundleid'] 55 | assert wf.name == env['alfred_workflow_name'] 56 | 57 | 58 | def test_alfred_debugger(alfred4): 59 | """Alfred debugger status""" 60 | # With debugger on 61 | with env(alfred_debug='1'): 62 | dump_env() 63 | wf = Workflow() 64 | assert wf.debugging, "Alfred's debugger not open" 65 | assert wf.logger.getEffectiveLevel() == logging.DEBUG 66 | wf.reset() 67 | 68 | # With debugger off 69 | with env(alfred_debug=None): 70 | dump_env() 71 | wf = Workflow() 72 | assert not wf.debugging, "Alfred's debugger is not closed" 73 | assert wf.logger.getEffectiveLevel() == logging.INFO 74 | wf.reset() 75 | -------------------------------------------------------------------------------- /tests/test_workflow_import.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright (c) 2019 Dean Jackson 3 | # MIT Licence applies http://opensource.org/licenses/MIT 4 | # 5 | # Created 2019-05-05 6 | 7 | """Unit tests for sys.path manipulation.""" 8 | 9 | from __future__ import print_function, unicode_literals 10 | 11 | import os 12 | import sys 13 | 14 | import pytest 15 | 16 | from workflow.workflow import Workflow 17 | 18 | 19 | LIBS = [os.path.join(os.path.dirname(__file__), b'lib')] 20 | 21 | 22 | def test_additional_libs(alfred4, infopl): 23 | """Additional libraries""" 24 | wf = Workflow(libraries=LIBS) 25 | for path in LIBS: 26 | assert path in sys.path 27 | 28 | assert sys.path[0:len(LIBS)] == LIBS 29 | import youcanimportme 30 | youcanimportme.noop() 31 | wf.reset() 32 | 33 | 34 | if __name__ == '__main__': # pragma: no cover 35 | pytest.main([__file__]) 36 | -------------------------------------------------------------------------------- /tests/test_workflow_keychain.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright (c) 2019 Dean Jackson 3 | # MIT Licence applies http://opensource.org/licenses/MIT 4 | # 5 | # Created 2019-05-05 6 | 7 | """Unit tests for Keychain API.""" 8 | 9 | from __future__ import print_function, unicode_literals 10 | 11 | import pytest 12 | 13 | from workflow.workflow import PasswordNotFound, KeychainError 14 | 15 | from .conftest import BUNDLE_ID 16 | 17 | 18 | ACCOUNT = 'this-is-my-test-account' 19 | PASSWORD = 'hunter2' 20 | PASSWORD2 = 'hunter2ing' 21 | PASSWORD3 = 'hünter\\“2”' 22 | 23 | 24 | def test_keychain(wf): 25 | """Save/get/delete password""" 26 | # ensure password is unset 27 | try: 28 | wf.delete_password(ACCOUNT) 29 | except PasswordNotFound: 30 | pass 31 | 32 | with pytest.raises(PasswordNotFound): 33 | wf.delete_password(ACCOUNT) 34 | with pytest.raises(PasswordNotFound): 35 | wf.get_password(ACCOUNT) 36 | 37 | wf.save_password(ACCOUNT, PASSWORD) 38 | assert wf.get_password(ACCOUNT) == PASSWORD 39 | assert wf.get_password(ACCOUNT, BUNDLE_ID) 40 | 41 | # set same password 42 | wf.save_password(ACCOUNT, PASSWORD) 43 | assert wf.get_password(ACCOUNT) == PASSWORD 44 | 45 | # set different password 46 | wf.save_password(ACCOUNT, PASSWORD2) 47 | assert wf.get_password(ACCOUNT) == PASSWORD2 48 | 49 | # set non-ASCII password 50 | wf.save_password(ACCOUNT, PASSWORD3) 51 | assert wf.get_password(ACCOUNT) == PASSWORD3 52 | 53 | # bad call to _call_security 54 | with pytest.raises(KeychainError): 55 | wf._call_security('pants', BUNDLE_ID, ACCOUNT) 56 | -------------------------------------------------------------------------------- /tests/test_workflow_magic_alfred2.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright (c) 2019 Dean Jackson 3 | # MIT Licence applies http://opensource.org/licenses/MIT 4 | # 5 | # Created on 2019-05-05 6 | 7 | """Unit tests for Alfred 2 magic argument handling.""" 8 | 9 | from __future__ import print_function 10 | 11 | import pytest 12 | 13 | from workflow import Workflow 14 | 15 | from .conftest import env 16 | from .util import VersionFile, WorkflowMock 17 | 18 | 19 | def test_version_magic(infopl2): 20 | """Magic: version magic (Alfred 2)""" 21 | vstr = '1.9.7' 22 | # Version from version file 23 | with env(alfred_workflow_version=None): 24 | # Versioned 25 | with WorkflowMock(['script', 'workflow:version']) as c: 26 | with VersionFile(vstr): 27 | wf = Workflow() 28 | # Process magic arguments 29 | wf.args 30 | assert not c.cmd 31 | wf.reset() 32 | 33 | # Unversioned 34 | with WorkflowMock(['script', 'workflow:version']) as c: 35 | wf = Workflow() 36 | # Process magic arguments 37 | wf.args 38 | assert not c.cmd 39 | wf.reset() 40 | 41 | # Version from environment variable 42 | with env(alfred_workflow_version=vstr): 43 | with WorkflowMock(['script', 'workflow:version']) as c: 44 | wf = Workflow() 45 | # Process magic arguments 46 | wf.args 47 | assert not c.cmd 48 | wf.reset() 49 | 50 | 51 | if __name__ == '__main__': # pragma: no cover 52 | pytest.main([__file__]) 53 | -------------------------------------------------------------------------------- /tests/test_workflow_run.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright (c) 2019 Dean Jackson 3 | # MIT Licence applies http://opensource.org/licenses/MIT 4 | # 5 | # Created 2019-05-05 6 | 7 | """Unit tests for Workflow.run.""" 8 | 9 | from __future__ import print_function, unicode_literals 10 | 11 | from StringIO import StringIO 12 | import sys 13 | 14 | import pytest 15 | 16 | from workflow.workflow import Workflow 17 | 18 | from conftest import env 19 | 20 | 21 | def test_run_fails(infopl): 22 | """Run fails""" 23 | wf = Workflow() 24 | 25 | def cb(wf2): 26 | assert wf2 is wf 27 | raise ValueError('Have an error') 28 | 29 | wf.help_url = 'http://www.deanishe.net/alfred-workflow/' 30 | ret = wf.run(cb) 31 | assert ret == 1 32 | 33 | # read name from info.plist 34 | with env(alfred_workflow_name=None): 35 | wf = Workflow() 36 | wf.name 37 | ret = wf.run(cb) 38 | assert ret == 1 39 | 40 | # named after bundleid 41 | wf = Workflow() 42 | wf.bundleid 43 | ret = wf.run(cb) 44 | assert ret == 1 45 | 46 | wf.reset() 47 | 48 | 49 | def test_run_fails_with_xml_output(wf): 50 | """Run fails with XML output""" 51 | error_text = 'Have an error' 52 | stdout = sys.stdout 53 | buf = StringIO() 54 | sys.stdout = buf 55 | 56 | def cb(wf2): 57 | assert wf2 is wf 58 | raise ValueError(error_text) 59 | 60 | ret = wf.run(cb) 61 | 62 | sys.stdout = stdout 63 | output = buf.getvalue() 64 | buf.close() 65 | 66 | assert ret == 1 67 | assert error_text in output 68 | assert ' 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2017-05-06 9 | # 10 | 11 | """Unit tests for serializer classes.""" 12 | 13 | from __future__ import print_function, absolute_import 14 | 15 | import os 16 | 17 | import pytest 18 | 19 | from workflow.workflow import ( 20 | SerializerManager, 21 | JSONSerializer, 22 | CPickleSerializer, 23 | PickleSerializer, 24 | manager as default_manager, 25 | ) 26 | 27 | 28 | # default serializers 29 | SERIALIZERS = ('json', 'cpickle', 'pickle') 30 | 31 | 32 | @pytest.fixture(scope='function') 33 | def manager(): 34 | """Create a `SerializerManager` with the default config.""" 35 | m = SerializerManager() 36 | m.register('cpickle', CPickleSerializer) 37 | m.register('pickle', PickleSerializer) 38 | m.register('json', JSONSerializer) 39 | yield m 40 | 41 | 42 | def is_serializer(obj): 43 | """Verify that ``obj`` implements serializer API.""" 44 | return hasattr(obj, 'load') and hasattr(obj, 'dump') 45 | 46 | 47 | def test_default_serializers(): 48 | """Default serializers.""" 49 | for name in SERIALIZERS: 50 | assert is_serializer(default_manager.serializer(name)) 51 | 52 | assert set(SERIALIZERS) == set(default_manager.serializers) 53 | 54 | 55 | def test_serialization(tempdir, manager): 56 | """Dump/load data.""" 57 | data = {'arg1': 'value1', 'arg2': 'value2'} 58 | 59 | for name in SERIALIZERS: 60 | serializer = manager.serializer(name) 61 | path = os.path.join(tempdir, 'test.{0}'.format(name)) 62 | assert not os.path.exists(path) 63 | 64 | with open(path, 'wb') as file_obj: 65 | serializer.dump(data, file_obj) 66 | 67 | assert os.path.exists(path) 68 | 69 | with open(path, 'rb') as file_obj: 70 | data2 = serializer.load(file_obj) 71 | 72 | assert data == data2 73 | 74 | os.unlink(path) 75 | 76 | 77 | def test_register_unregister(manager): 78 | """Register/unregister serializers.""" 79 | serializers = {} 80 | for name in SERIALIZERS: 81 | serializer = manager.serializer(name) 82 | assert is_serializer(serializer) 83 | 84 | for name in SERIALIZERS: 85 | serializer = manager.unregister(name) 86 | assert is_serializer(serializer) 87 | serializers[name] = serializer 88 | 89 | for name in SERIALIZERS: 90 | assert manager.serializer(name) is None 91 | 92 | for name in SERIALIZERS: 93 | with pytest.raises(ValueError): 94 | manager.unregister(name) 95 | 96 | for name in SERIALIZERS: 97 | serializer = serializers[name] 98 | manager.register(name, serializer) 99 | 100 | 101 | class InvalidSerializer(object): 102 | """Bad serializer.""" 103 | 104 | 105 | def test_register_invalid(manager): 106 | """Register invalid serializer.""" 107 | invalid1 = InvalidSerializer() 108 | invalid2 = InvalidSerializer() 109 | setattr(invalid2, 'load', lambda x: x) 110 | 111 | with pytest.raises(AttributeError): 112 | manager.register('bork', invalid1) 113 | with pytest.raises(AttributeError): 114 | manager.register('bork', invalid2) 115 | 116 | 117 | if __name__ == '__main__': # pragma: no cover 118 | pytest.main([__file__]) 119 | -------------------------------------------------------------------------------- /tests/test_workflow_settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2016 Dean Jackson 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2016-02-24 9 | # 10 | 11 | """Unit tests for Workflow.settings API.""" 12 | 13 | from __future__ import print_function, unicode_literals, absolute_import 14 | 15 | import json 16 | import os 17 | import shutil 18 | import time 19 | import tempfile 20 | import unittest 21 | 22 | from workflow.workflow import Settings 23 | 24 | from tests.util import DEFAULT_SETTINGS 25 | 26 | 27 | class SettingsTests(unittest.TestCase): 28 | """Test suite for `workflow.workflow.Settings`.""" 29 | 30 | def setUp(self): 31 | """Initialise unit test environment.""" 32 | self.tempdir = tempfile.mkdtemp() 33 | self.settings_file = os.path.join(self.tempdir, 'settings.json') 34 | with open(self.settings_file, 'wb') as file_obj: 35 | json.dump(DEFAULT_SETTINGS, file_obj) 36 | 37 | def tearDown(self): 38 | """Reset unit test environment.""" 39 | if os.path.exists(self.tempdir): 40 | shutil.rmtree(self.tempdir) 41 | 42 | def test_defaults(self): 43 | """Default settings""" 44 | if os.path.exists(self.settings_file): 45 | os.unlink(self.settings_file) 46 | s = Settings(self.settings_file, {'key1': 'value2'}) 47 | self.assertEqual(s['key1'], 'value2') 48 | 49 | def test_load_settings(self): 50 | """Load saved settings""" 51 | s = Settings(self.settings_file, {'key1': 'value2'}) 52 | for key in DEFAULT_SETTINGS: 53 | self.assertEqual(DEFAULT_SETTINGS[key], s[key]) 54 | 55 | def test_save_settings(self): 56 | """Settings saved""" 57 | s = Settings(self.settings_file) 58 | self.assertEqual(s['key1'], DEFAULT_SETTINGS['key1']) 59 | s['key1'] = 'spoons!' 60 | s2 = Settings(self.settings_file) 61 | self.assertEqual(s['key1'], s2['key1']) 62 | 63 | def test_delete_settings(self): 64 | """Settings deleted""" 65 | s = Settings(self.settings_file) 66 | self.assertEqual(s['key1'], DEFAULT_SETTINGS['key1']) 67 | del s['key1'] 68 | s2 = Settings(self.settings_file) 69 | self.assertEqual(s2.get('key1'), None) 70 | 71 | def test_dict_methods(self): 72 | """Settings dict methods""" 73 | other = {'key1': 'spoons!'} 74 | s = Settings(self.settings_file) 75 | self.assertEqual(s['key1'], DEFAULT_SETTINGS['key1']) 76 | s.update(other) 77 | s.setdefault('alist', []) 78 | s2 = Settings(self.settings_file) 79 | self.assertEqual(s['key1'], s2['key1']) 80 | self.assertEqual(s['key1'], 'spoons!') 81 | self.assertEqual(s2['alist'], []) 82 | 83 | def test_settings_not_rewritten(self): 84 | """Settings not rewritten for same value""" 85 | s = Settings(self.settings_file) 86 | mt = os.path.getmtime(self.settings_file) 87 | time.sleep(1) # wait long enough to register changes in `time.time()` 88 | now = time.time() 89 | for k, v in DEFAULT_SETTINGS.items(): 90 | s[k] = v 91 | self.assertTrue(os.path.getmtime(self.settings_file) == mt) 92 | s['finished_at'] = now 93 | s2 = Settings(self.settings_file) 94 | self.assertEqual(s['finished_at'], s2['finished_at']) 95 | self.assertTrue(os.path.getmtime(self.settings_file) > mt) 96 | 97 | def test_mutable_objects_updated(self): 98 | """Updated mutable objects cause save""" 99 | s = Settings(self.settings_file) 100 | mt1 = os.path.getmtime(self.settings_file) 101 | time.sleep(1) 102 | seq = s['mutable1'] 103 | seq.append('another string') 104 | s['mutable1'] = seq 105 | mt2 = os.path.getmtime(self.settings_file) 106 | self.assertTrue(mt2 > mt1) 107 | s2 = Settings(self.settings_file) 108 | self.assertTrue('another string' in s2['mutable1']) 109 | 110 | 111 | if __name__ == '__main__': # pragma: no cover 112 | unittest.main() 113 | -------------------------------------------------------------------------------- /tests/test_workflow_versions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Copyright (c) 2019 Dean Jackson 3 | # MIT Licence applies http://opensource.org/licenses/MIT 4 | # 5 | # Created 2019-05-05 6 | 7 | """Unit tests for workflow version determination.""" 8 | 9 | from __future__ import print_function, unicode_literals 10 | 11 | import pytest 12 | 13 | from workflow.update import Version 14 | from workflow.workflow import Workflow 15 | from workflow.workflow3 import Workflow3 16 | 17 | from .conftest import env, WORKFLOW_VERSION 18 | from .util import VersionFile 19 | 20 | 21 | def test_info_plist(infopl): 22 | """Version from info.plist.""" 23 | wf = Workflow3() 24 | assert wf.version == Version('1.1.1'), "unexpected version" 25 | 26 | 27 | def test_envvar(infopl): 28 | """Version from environment variable.""" 29 | v = '1.1.2' 30 | with env(alfred_workflow_version=v): 31 | wf = Workflow3() 32 | assert wf.version == Version(v), "unexpected version" 33 | # environment variables have priority 34 | wf = Workflow3(update_settings={'version': '1.1.3'}) 35 | assert wf.version == Version(v), "unexpected version" 36 | 37 | 38 | def test_update_settings(infopl): 39 | """Version from update_settings.""" 40 | v = '1.1.3' 41 | wf = Workflow3(update_settings={'version': v}) 42 | assert wf.version == Version(v), "unexpected version" 43 | 44 | 45 | def test_versions_from_settings(alfred4, infopl2): 46 | """Workflow: version from `update_settings`""" 47 | vstr = '1.9.7' 48 | d = { 49 | 'github_slug': 'deanishe/alfred-workflow', 50 | 'version': vstr, 51 | } 52 | with env(alfred_workflow_version=None): 53 | wf = Workflow(update_settings=d) 54 | assert str(wf.version) == vstr 55 | assert isinstance(wf.version, Version) 56 | assert wf.version == Version(vstr) 57 | 58 | 59 | def test_versions_from_file(alfred4, infopl2): 60 | """Workflow: version from `version` file""" 61 | vstr = '1.9.7' 62 | with env(alfred_workflow_version=None): 63 | with VersionFile(vstr): 64 | wf = Workflow() 65 | assert str(wf.version) == vstr 66 | assert isinstance(wf.version, Version) 67 | assert wf.version == Version(vstr) 68 | 69 | 70 | def test_versions_from_info(alfred4, infopl): 71 | """Workflow: version from info.plist""" 72 | with env(alfred_workflow_version=None): 73 | wf = Workflow() 74 | assert str(wf.version) == WORKFLOW_VERSION 75 | assert isinstance(wf.version, Version) 76 | assert wf.version == Version(WORKFLOW_VERSION) 77 | 78 | 79 | def test_first_run_no_version(alfred4, infopl2): 80 | """Workflow: first_run fails on no version""" 81 | with env(alfred_workflow_version=None): 82 | wf = Workflow() 83 | try: 84 | with pytest.raises(ValueError): 85 | wf.first_run 86 | finally: 87 | wf.reset() 88 | 89 | 90 | def test_first_run_with_version(alfred4, infopl): 91 | """Workflow: first_run""" 92 | vstr = '1.9.7' 93 | with env(alfred_workflow_version=vstr): 94 | wf = Workflow() 95 | assert wf.first_run is True 96 | wf.reset() 97 | 98 | 99 | def test_first_run_with_previous_run(alfred4, infopl): 100 | """Workflow: first_run with previous run""" 101 | vstr = '1.9.7' 102 | last_vstr = '1.9.6' 103 | with env(alfred_workflow_version=vstr): 104 | wf = Workflow() 105 | wf.set_last_version(last_vstr) 106 | assert wf.first_run is True 107 | assert wf.last_version_run == Version(last_vstr) 108 | wf.reset() 109 | 110 | 111 | def test_last_version_empty(wf): 112 | """Workflow: last_version_run empty""" 113 | assert wf.last_version_run is None 114 | 115 | 116 | def test_last_version_on(alfred4, infopl): 117 | """Workflow: last_version_run not empty""" 118 | vstr = '1.9.7' 119 | 120 | with env(alfred_workflow_version=vstr): 121 | wf = Workflow() 122 | wf.set_last_version(vstr) 123 | assert Version(vstr) == wf.last_version_run 124 | wf.reset() 125 | 126 | # Set automatically 127 | with env(alfred_workflow_version=vstr): 128 | wf = Workflow() 129 | wf.set_last_version() 130 | assert Version(vstr) == wf.last_version_run 131 | wf.reset() 132 | 133 | 134 | def test_versions_no_version(alfred4, infopl2): 135 | """Workflow: version is `None`""" 136 | with env(alfred_workflow_version=None): 137 | wf = Workflow() 138 | assert wf.version is None 139 | wf.reset() 140 | 141 | 142 | def test_last_version_no_version(alfred4, infopl2): 143 | """Workflow: last_version no version""" 144 | with env(alfred_workflow_version=None): 145 | wf = Workflow() 146 | assert wf.set_last_version() is False 147 | wf.reset() 148 | 149 | 150 | def test_last_version_explicit_version(alfred4, infopl): 151 | """Workflow: last_version explicit version""" 152 | vstr = '1.9.6' 153 | wf = Workflow() 154 | assert wf.set_last_version(vstr) is True 155 | assert wf.last_version_run == Version(vstr) 156 | wf.reset() 157 | 158 | 159 | def test_last_version_auto_version(alfred4, infopl): 160 | """Workflow: last_version auto version""" 161 | vstr = '1.9.7' 162 | with env(alfred_workflow_version=vstr): 163 | wf = Workflow() 164 | assert wf.set_last_version() is True 165 | assert wf.last_version_run == Version(vstr) 166 | wf.reset() 167 | 168 | 169 | def test_last_version_set_after_run(alfred4, infopl): 170 | """Workflow: last_version set after `run()`""" 171 | vstr = '1.9.7' 172 | 173 | def cb(wf): 174 | return 175 | 176 | with env(alfred_workflow_version=vstr): 177 | wf = Workflow() 178 | assert wf.last_version_run is None 179 | wf.run(cb) 180 | 181 | wf = Workflow() 182 | assert wf.last_version_run == Version(vstr) 183 | wf.reset() 184 | 185 | 186 | def test_alfred_version(wf): 187 | """Workflow: alfred_version correct.""" 188 | assert wf.alfred_version == Version('4.0') 189 | -------------------------------------------------------------------------------- /tests/test_workflow_xml.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-05-06 9 | # 10 | 11 | """Unit tests for Workflow's XML feedback generation.""" 12 | 13 | from __future__ import print_function 14 | 15 | from contextlib import contextmanager 16 | from StringIO import StringIO 17 | import sys 18 | from xml.etree import ElementTree as ET 19 | 20 | import pytest 21 | 22 | from workflow import Workflow 23 | 24 | 25 | @pytest.fixture(scope='function') 26 | def wf(infopl): 27 | """Create a :class:`~workflow.Workflow` object.""" 28 | yield Workflow() 29 | 30 | 31 | @contextmanager 32 | def stdout(): 33 | """Capture output to STDOUT.""" 34 | old = sys.stdout 35 | sio = StringIO() 36 | sys.stdout = sio 37 | yield sio 38 | sio.close() 39 | sys.stdout = old 40 | 41 | 42 | def test_item_creation(wf): 43 | """XML generation""" 44 | wf.add_item( 45 | 'title', 'subtitle', arg='arg', 46 | autocomplete='autocomplete', 47 | valid=True, uid='uid', icon='icon.png', 48 | icontype='fileicon', 49 | type='file', largetext='largetext', 50 | copytext='copytext', 51 | quicklookurl='http://www.deanishe.net/alfred-workflow') 52 | with stdout() as sio: 53 | wf.send_feedback() 54 | output = sio.getvalue() 55 | 56 | root = ET.fromstring(output) 57 | item = list(root)[0] 58 | 59 | assert item.attrib['uid'] == 'uid' 60 | assert item.attrib['autocomplete'] == 'autocomplete' 61 | assert item.attrib['valid'] == 'yes' 62 | assert item.attrib['uid'] == 'uid' 63 | 64 | title, subtitle, arg, icon, \ 65 | largetext, copytext, quicklookurl = list(item) 66 | 67 | assert title.text == 'title' 68 | assert title.tag == 'title' 69 | 70 | assert subtitle.text == 'subtitle' 71 | assert subtitle.tag == 'subtitle' 72 | 73 | assert arg.text == 'arg' 74 | assert arg.tag == 'arg' 75 | 76 | assert largetext.tag == 'text' 77 | assert largetext.text == 'largetext' 78 | assert largetext.attrib['type'] == 'largetype' 79 | 80 | assert copytext.tag == 'text' 81 | assert copytext.text == 'copytext' 82 | assert copytext.attrib['type'] == 'copy' 83 | 84 | assert icon.text == 'icon.png' 85 | assert icon.tag == 'icon' 86 | assert icon.attrib['type'] == 'fileicon' 87 | 88 | assert quicklookurl.tag == 'quicklookurl' 89 | assert quicklookurl.text == 'http://www.deanishe.net/alfred-workflow' 90 | 91 | 92 | def test_item_creation_with_modifiers(wf): 93 | """XML generation (with modifiers).""" 94 | mod_subs = {} 95 | for mod in ('cmd', 'ctrl', 'alt', 'shift', 'fn'): 96 | mod_subs[mod] = mod 97 | wf.add_item('title', 'subtitle', 98 | mod_subs, 99 | arg='arg', 100 | autocomplete='autocomplete', 101 | valid=True, uid='uid', icon='icon.png', 102 | icontype='fileicon', 103 | type='file') 104 | with stdout() as sio: 105 | wf.send_feedback() 106 | output = sio.getvalue() 107 | 108 | root = ET.fromstring(output) 109 | item = list(root)[0] 110 | assert item.attrib['uid'] == 'uid' 111 | assert item.attrib['autocomplete'] == 'autocomplete' 112 | assert item.attrib['valid'] == 'yes' 113 | assert item.attrib['uid'] == 'uid' 114 | (title, subtitle, sub_cmd, sub_ctrl, sub_alt, sub_shift, sub_fn, arg, 115 | icon) = list(item) 116 | assert title.text == 'title' 117 | assert title.tag == 'title' 118 | assert subtitle.text == 'subtitle' 119 | assert sub_cmd.text == 'cmd' 120 | assert sub_cmd.attrib['mod'] == 'cmd' 121 | assert sub_ctrl.text == 'ctrl' 122 | assert sub_ctrl.attrib['mod'] == 'ctrl' 123 | assert sub_alt.text == 'alt' 124 | assert sub_alt.attrib['mod'] == 'alt' 125 | assert sub_shift.text == 'shift' 126 | assert sub_shift.attrib['mod'] == 'shift' 127 | assert sub_fn.text == 'fn' 128 | assert sub_fn.attrib['mod'] == 'fn' 129 | assert subtitle.tag == 'subtitle' 130 | assert arg.text == 'arg' 131 | assert arg.tag == 'arg' 132 | assert icon.text == 'icon.png' 133 | assert icon.tag == 'icon' 134 | assert icon.attrib['type'] == 'fileicon' 135 | 136 | 137 | def test_item_creation_no_optionals(wf): 138 | """XML generation (no optionals)""" 139 | wf.add_item('title') 140 | with stdout() as sio: 141 | wf.send_feedback() 142 | output = sio.getvalue() 143 | 144 | root = ET.fromstring(output) 145 | item = list(root)[0] 146 | for key in ['uid', 'arg', 'autocomplete']: 147 | assert key not in item.attrib 148 | 149 | assert item.attrib['valid'] == 'no' 150 | title, subtitle = list(item) 151 | assert title.text == 'title' 152 | assert title.tag == 'title' 153 | assert subtitle.text is None 154 | tags = [elem.tag for elem in list(item)] 155 | for tag in ['icon', 'arg']: 156 | assert tag not in tags 157 | 158 | 159 | if __name__ == '__main__': # pragma: no cover 160 | pytest.main([__file__]) 161 | -------------------------------------------------------------------------------- /workflow/Notify.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-workflow/70d04df5bded8e501ce3bb82fa55ecc1f947f240/workflow/Notify.tgz -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /workflow/version: -------------------------------------------------------------------------------- 1 | 1.40.0 --------------------------------------------------------------------------------