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