├── .gitignore ├── LICENSE ├── README.md ├── alfred.py ├── info.plist ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Eclipse 38 | .settings 39 | 40 | # Mac 41 | .DS_Store 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Dr. Jan Müller 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | alfred-python 2 | ============= 3 | 4 | Pythonic and lightweight access to the Alfred workflow API. If you need inspiration of how to use it, look at the following lines: 5 | 6 | ```python 7 | import alfred 8 | 9 | >>> import alfred 10 | >>> print alfred.bundleid 11 | nikipore.alfredpython 12 | >>> print alfred.preferences['description'] 13 | Python library for Alfred workflow API 14 | >>> alfred.bundleid 15 | 'nikipore.alfredpython' 16 | >>> alfred.preferences['description'] # access to info.plist 17 | 'Python library for Alfred workflow API' 18 | >>> alfred.work(volatile=True) # access to (and creation of) the recommended storage paths 19 | '/Users/jan/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/nikipore.alfredpython' 20 | >>> alfred.work(volatile=False) 21 | '/Users/jan/Library/Application Support/Alfred 2/Workflow Data/nikipore.alfredpython' 22 | >>> alfred.config() 23 | 'config' 24 | >>> item = alfred.Item({'uid': 1, 'arg': 'some arg'}, 'some title', 'some subtitle') 25 | >>> str(item) 26 | 'some titlesome subtitle' 27 | >>> item = alfred.Item({'uid': alfred.uid(1), 'arg': 'some arg', 'valid': 'no'}, 'some title', 'some subtitle', ('someicon.png', {'type': 'filetype'})) 28 | >>> str(item) 29 | 'some titlesome subtitlesomeicon.png' 30 | ``` 31 | 32 | The boilerplate for your Alfred workflow is reduced to something like this: 33 | 34 | ```python 35 | # -*- coding: utf-8 -*- 36 | (parameter, query) = alfred.args() # proper decoding and unescaping of command line arguments 37 | results = [item( 38 | attributes= {'uid': alfred.uid(0), 'arg': u'https://www.google.de/q=%s' % query}, 39 | title=parameter, 40 | subtitle=u'simple access to the Alfred workflow API' 41 | )] # a single Alfred result 42 | xml = alfred.xml(results) # compiles the XML answer 43 | alfred.write(xml) # writes the XML back to Alfred 44 | ``` 45 | 46 | You are also invited to look at the workflows implemented with alfred-python: 47 | 48 | * [Access to Firefox Bookmarks and User Input History](https://github.com/nikipore/alfred-firefoxbookmarks) 49 | * [File Action Add to Archive](https://github.com/nikipore/alfred-fileaction-zip) 50 | * [Call with Telephone App](https://github.com/nikipore/alfred-voipcall) 51 | * [MailTo: address emails from Alfred](https://github.com/deanishe/alfred-mailto) 52 | * [Smart Folders: search and browse Smart Folders/Saved Searches from Alfred](https://github.com/deanishe/alfred-smartfolders) 53 | * [Exchange rates: check exchange rates in Alfred](https://github.com/krzysztofr/alfred-currencies) 54 | 55 | Please feel free to contribute more workflows implemented with alfred-python here, or add functionality to/fix bugs on alfred-python. 56 | -------------------------------------------------------------------------------- /alfred.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import itertools 3 | import os 4 | import plistlib 5 | import unicodedata 6 | import sys 7 | 8 | from xml.etree.ElementTree import Element, SubElement, tostring 9 | 10 | """ 11 | You should run your script via /bin/bash with all escape options ticked. 12 | The command line should be 13 | 14 | python yourscript.py "{query}" arg2 arg3 ... 15 | """ 16 | UNESCAPE_CHARACTERS = u""" ;()""" 17 | 18 | _MAX_RESULTS_DEFAULT = 9 19 | 20 | preferences = plistlib.readPlist('info.plist') 21 | bundleid = preferences['bundleid'] 22 | 23 | class Item(object): 24 | @classmethod 25 | def unicode(cls, value): 26 | try: 27 | items = iter(value.items()) 28 | except AttributeError: 29 | return unicode(value) 30 | else: 31 | return dict(map(unicode, item) for item in items) 32 | 33 | def __init__(self, attributes, title, subtitle, icon=None): 34 | self.attributes = attributes 35 | self.title = title 36 | self.subtitle = subtitle 37 | self.icon = icon 38 | 39 | def __str__(self): 40 | return tostring(self.xml()).decode('utf-8') 41 | 42 | def xml(self): 43 | item = Element(u'item', self.unicode(self.attributes)) 44 | for attribute in (u'title', u'subtitle', u'icon'): 45 | value = getattr(self, attribute) 46 | if value is None: 47 | continue 48 | if len(value) == 2 and isinstance(value[1], dict): 49 | (value, attributes) = value 50 | else: 51 | attributes = {} 52 | SubElement(item, attribute, self.unicode(attributes)).text = self.unicode(value) 53 | return item 54 | 55 | def args(characters=None): 56 | return tuple(unescape(decode(arg), characters) for arg in sys.argv[1:]) 57 | 58 | def config(): 59 | return _create('config') 60 | 61 | def decode(s): 62 | return unicodedata.normalize('NFD', s.decode('utf-8')) 63 | 64 | def env(key): 65 | return os.environ['alfred_%s' % key] 66 | 67 | def uid(uid): 68 | return u'-'.join(map(str, (bundleid, uid))) 69 | 70 | def unescape(query, characters=None): 71 | for character in (UNESCAPE_CHARACTERS if (characters is None) else characters): 72 | query = query.replace('\\%s' % character, character) 73 | return query 74 | 75 | def work(volatile): 76 | path = { 77 | True: env('workflow_cache'), 78 | False: env('workflow_data') 79 | }[bool(volatile)] 80 | return _create(path) 81 | 82 | def write(text): 83 | sys.stdout.write(text) 84 | 85 | def xml(items, maxresults=_MAX_RESULTS_DEFAULT): 86 | root = Element('items') 87 | for item in itertools.islice(items, maxresults): 88 | root.append(item.xml()) 89 | return tostring(root, encoding='utf-8') 90 | 91 | def _create(path): 92 | if not os.path.isdir(path): 93 | os.mkdir(path) 94 | if not os.access(path, os.W_OK): 95 | raise IOError('No write access: %s' % path) 96 | return path 97 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | nikipore.alfredpython 7 | createdby 8 | Jan Müller 9 | description 10 | Python library for Alfred workflow API 11 | name 12 | alfred-python 13 | webaddress 14 | https://github.com/nikipore/alfred-python 15 | 16 | 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name='alfred-python', 4 | version='1.0.0', 5 | description='Simple Python access to the Alfred workflow API', 6 | author='Jan Müller', 7 | url='https://github.com/nikipore/alfred-python', 8 | py_modules=['alfred']) 9 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright © 2013 deanishe@deanishe.net. 5 | # 6 | # MIT Licence. See http://opensource.org/licenses/MIT 7 | # 8 | # Created on 2013-12-05 9 | # 10 | 11 | """ 12 | """ 13 | 14 | from __future__ import print_function 15 | 16 | import sys 17 | import os 18 | import unittest 19 | import unicodedata 20 | 21 | import alfred 22 | 23 | class AlfredTests(unittest.TestCase): 24 | 25 | _test_filename = 'üöäéøØÜÄÖÉàÀ.l11n' 26 | _unicode_test_filename = unicode(_test_filename, 'utf-8') 27 | 28 | def setUp(self): 29 | with open(self._test_filename, u'wb') as file: 30 | file.write(u'Testing!') 31 | 32 | def tearDown(self): 33 | if os.path.exists(self._test_filename): 34 | os.unlink(self._test_filename) 35 | 36 | def test_unicode_normalisation(self): 37 | """Ensure args are normalised in line with filesystem names""" 38 | self.assert_(os.path.exists(self._test_filename)) 39 | filenames = [f for f in os.listdir(u'.') if f.endswith('.l11n')] 40 | self.assert_(len(filenames) == 1) 41 | print(u'{!r}'.format(filenames)) 42 | fs_filename = filenames[0] 43 | self.assert_(fs_filename != self._test_filename) # path has been NFD normalised by filesystem 44 | alfred_filename = alfred.decode(self._test_filename) 45 | self.assert_(alfred_filename == fs_filename) 46 | 47 | def test_unicode_value_xml(self): 48 | """Ensure we can handle converting Items with unicode values to xml""" 49 | item = alfred.Item({}, u'\xb7', u'\xb7') 50 | expected = '\xc2\xb7\xc2\xb7' 51 | actual = alfred.xml([item]) 52 | self.assert_(expected == actual, '{!r} != {!r}'.format(expected, actual)) 53 | 54 | 55 | if __name__ == u'__main__': 56 | unittest.main() 57 | --------------------------------------------------------------------------------